Commit Graph
171 Commits
Author SHA1 Message Date
jwinterm 3d7732c88b blog: studio voice on the PageSpeed post, and an update for the new splash
Title takes "Our" instead of "My", and the body follows it: fifteen instances of
first-person singular are now plural. The second "Our" in the title became "the"
to avoid the doubling.

Swept the same judgment language out of it. "I like it", "the obvious thing",
"The obvious suspect", "First gotcha worth knowing", "What I would actually tell
you", "Why I care about the boring version". A report states what happened and
lets the reader rank it. Kept the honest method reporting ("we got it wrong
twice"), which is evidence rather than editorial.

Swept the other posts too and deliberately changed nothing in them.
such-moon-launch-release uses "I" for a real authorship claim, "the very first
game I ever wrote", which becomes false as "we". The two hits in
vegan-iq-data-pipeline are an idiom and a quoted hypothetical. Site copy under
src/ came back clean.

NEW UPDATE. The post described a splash that no longer exists, so it now carries
the replacement and what measuring it turned up.

The pillar signature replaced the Cherenkov splash. Making it advance on its own
surfaced a defect that had been shipping: CLS 0 to 1.0, caused not by the new
animation but by the splash having always been a min-h-[100svh] block in document
flow that only became fixed on the way out. CLS excuses shifts within 500ms of
input, so clickers never contributed it and the headline 100 was accurate only
for them. Reduced-motion and returning-session visitors, who never click, scored
74 with CLS 1.0 against production.

Numbers are measured, not recalled: mobile 97 (LCP 1.2s, CLS 0.013, TBT 20ms),
desktop 98 (LCP 1.0s, CLS 0.005, TBT 0ms), run against production this week. The
reduced-motion path is 98 where it was 74.
2026-08-21 05:36:43 -04:00
jwinterm 037304b1ad fix(blog): render tables, drop typography's backticks, correct the transport claim
TABLES. next-mdx-remote had no remark plugins, so GFM tables rendered as literal
pipe characters in a paragraph. Four published posts were affected, not just the
new one: bauhaus-echo-launch, flutter-3d-physics-dice, vegan-iq-data-pipeline
and smirk-v0-3-0. remark-gfm is now wired into MDXRemote's mdxOptions.

BACKTICKS. @tailwindcss/typography wraps every inline <code> in literal backtick
characters via code::before / code::after. The served HTML was already clean;
the marks were injected by CSS. Monospace already marks code as code, so the
backticks were a redundant second marker that reads as a typo in running prose.
Suppressed across .prose.

TRANSPORT. The previous version's "transports" section covered only Grin slate
channels. Tor is a separate mechanism and was undocumented: the wallet accepts a
.onion backend origin over plain http, alongside private ranges and .local,
because onion services carry their own transport authentication and no CA issues
for them. The onion pattern matches the label immediately before .onion, so an
onion-lookalike public host is refused. On the backend, the admin plane binds to
loopback and TOR_ADMIN_ONION adds exactly one onion host to its Host allowlist
as a DNS-rebinding guard.

There is no bundled Tor daemon and no I2P support anywhere in either repo. The
post now says so rather than leaving it implied.

COPY. Removed the editorial voice throughout: no more "the part worth
explaining", "the instructive one", "worth dwelling on". A technical report
states what a thing does and lets the reader decide what matters. Headings are
now descriptive rather than argumentative.
2026-08-21 05:29:35 -04:00
jwinterm 55400cf6d9 blog: rewrite the Smirk post as a shipped release report, with diagrams
Rewritten as a technical report on what v0.3.0 does, written as released rather
than pending. Cut the "working notes" framing and the narrative voice.

Three inline SVG diagrams carry the parts that are awkward in prose:

  - Federation: two wallets on two independent backends, each delegating to its
    own chain nodes, with the Nostr relay layer joining the WALLETS rather than
    routing through a backend. The first draft drew that line from a wallet to a
    backend, which quietly asserted the opposite of the point.
  - Signing tiers: signEvent routed by kind into money / session-grantable /
    default, with the actual kind numbers.
  - Slate transports: the three Grin transports with their discovery keys, and
    which are federated versus same-instance.

They are inline SVG rather than images: text stays selectable and translatable,
they scale without artefacts, and structure drawn in currentColor inherits the
prose colour so both themes are correct by construction. Verified rendered in
light and dark rather than assumed. Accent colours are mid-tones chosen to read
on both grounds. Every figure carries role="img" and an aria-label describing
what it shows.

Sourced from the repos: docs/grin.md for the transport table and discovery
rules, packages/dapp-api/src/nostr-tiers.ts for the kinds, CHANGELOG.md and
smirk-backend-core's README for the rest.
2026-08-20 15:41:53 -04:00
jwinterm 91513bcb2a chore(webshops): say shop, not tenant, in the last place it slipped through
The page rewrite dropped the operator's word for a customer everywhere except
one blurb in the shared surfaces gallery, which the page renders.
2026-08-20 14:47:10 -04:00
jwinterm 26a2707cf6 blog: build notes on Smirk v0.3.0, Nostr identity, and self-hosting
Written as working notes from the release window rather than an announcement,
and framed as forthcoming: v0.3.0 is cut and in store review, not on anyone's
machine, so the post says so instead of implying availability.

Built around the design decision rather than the feature list. The interesting
part of the Nostr work is not that there is a NIP-07 provider, it is that a
signature is a bearer credential and so "remember this site" is a security
decision. Hence the three event-kind tiers, and specifically why NIP-98 (27235)
had to move to money-tier: the backend mints a full session from one, so leaving
it session-grantable let a connected site collect an auth token for the user's
own backend under a permission granted for social posts. Enforcement sits in the
wallet handler rather than the approval dialog, so the rule survives a hostile
UI.

Then the federation half, which is what makes smirk-backend-core worth writing
about: the wallet no longer assumes api.smirk.cash, which is only a real claim
if there is something to point it at.

Sourced from the repos, not from a summary: CHANGELOG.md, the tier table in
packages/dapp-api/src/nostr-tiers.ts, and smirk-backend-core's README. Both
linked repos verified public before publishing.

981 words, about a third shorter than the post it follows.
2026-08-20 14:45:00 -04:00
jwinterm 8587509eed rewrite(webshops): write the page for the client, not for us
The page led with our architecture. "Multi-tenant commerce platform", "from one
engine", "per-tenant theming is the whole point": all true, none of it what
someone deciding whether to hire us is trying to find out. It also called the
reader a "tenant" throughout, which is an operator's word for a customer.

It was long and it repeated itself. Theming was explained four times over (hero,
a dedicated section, the demos intro, the surfaces intro), and revenue share,
crypto, tax, and data ownership each appeared in a feature card AND an FAQ. The
feature grid ran to eleven cards, several of them vendor filler: "Flexible
Architecture", "Performance First".

Now: what it is, what you can sell, how you get paid, what it costs, who runs
it. Six feature cards instead of eleven. The standalone theming section is gone,
folded into the hero and the demos. Eight questions instead of ten, each one
something a buyer would actually ask, and the answers are shorter than the
questions deserve rather than longer.

The technical detail survives, moved to one block at the bottom under a heading
that says who it is for. That is where Medusa, Next.js, the payment rails, and
the hosting story live now.

Two things fixed while in here. The demos are labelled as demo storefronts,
because they are ours and not client work, and the caption no longer lets that
blur. And the CTA pointed at /contact, a 307 redirect hop that was fixed
elsewhere months ago and missed here.
2026-08-20 14:40:42 -04:00
jwinterm 0324c8fab9 fix(anim): drop <object> for <img>, and say cryptocurrency
THE GREY BOX. The decorative animations were embedded with <object>, which
creates a nested browsing context that paints its own canvas. On the dark pages
that canvas showed as a light grey square behind the mark. The SVG's own baked
tile had been hiding most of it, so only the rounded corners leaked, which is
why it read as "grey corners" rather than "grey box": removing the tile exposed
the whole thing and made the real cause obvious.

<img> has a transparent canvas and still runs CSS animations inside an SVG, as
the splash morph and the Hash Bags icon already demonstrate. Swapped on all
three pages that used the pattern, not just the one that was reported: /apps,
/products and /blog all had it. Verified on the rendered dark page, where the
pixels outside the mark, inside the old tile, and the page background are now
the same colour.

The marks themselves are now the tileless variants from such-graphics f457c37,
so nothing bakes a background colour that has to match ours.

COPY. "crypto payment tools" -> "cryptocurrency tools", and the supporting line
now names both ways of working with us rather than only the products.
2026-08-19 15:08:19 -04:00
jwinterm a354ed5b7b fix(splash): stop the handover re-locking scroll for the rest of the visit
First visit of a session ended with the page unscrollable. A hard refresh
appeared to fix it, which is what made it look like a caching oddity rather
than a bug: the session flag then skips the splash and neither lock runs.

Two things were managing html.style.overflow and they fought.

  1. The splash's own lock sets overflow:hidden while the cover is up, saving
     the previous value ("").
  2. enterFromButton then captured the previous value for the transition lock.
     By then it was already "hidden", from step 1.
  3. `entered` flips, the splash lock cleans up and restores "". Page scrolls.
  4. Up to 3s later the transition's release() restores its captured value,
     which is "hidden". Page is locked, and nothing ever clears it.

The release is now unconditional. By the time it runs the splash is gone and
the page must scroll, so restoring a captured value is wrong by construction:
there is no state worth preserving, only a lock to drop.

Verified with a real wheel event 5s after load, past both the 1337ms gate and
the 3s release cap where the re-lock used to land: overflow is empty and the
page scrolls.
2026-08-18 16:25:07 -04:00
jwinterm d3b2fa718a fix(icons): the Hash Bags # leans, as it does everywhere else
I shipped it upright. It is a typographic monospace hash and it leans, which is
how it renders across hash.boats (hero and every feature card) and how
hash.boats/splash.svg draws it by hand: "verticals lean right".

Where the upright one came from, because the wallet repo disagrees with itself.
assets/images/hash_wallet_app_icon_source.svg rebuilt the glyph as paths to
remove a font dependency and flattened the lean while doing it, and
store/play-icon-512.png is upright too: its verticals measure x=206 and x=306
on every row, no lean at all. I took those as canonical and explicitly rejected
the slanted splash.svg as wrong. That was backwards. The brand's own site is
the authority on the mark; an app-store export is downstream of it.

Both icons are now rebuilt from splash.svg's glyph, mapped onto a full-bleed
1024 canvas preserving its proportions exactly (glyph spans 57.5% of the tile,
stroke is 8.75%). The static PNG is rendered from the same geometry as the
animated SVG rather than being a separate export, so the two cannot drift: the
animation ends on precisely the still icon. Measured after: the left bar's
centre runs 225 -> 204 -> 182 down the tile, where the old one sat at 206 on
every row.

Still full-bleed, not splash.svg's inset rounded box, because the site rounds
icons with a 22% CSS radius and a built-in transparent margin would show card
background at the corners.
2026-08-18 10:39:21 -04:00
jwinterm dccf26e603 chore(apps): drop "AI-powered", sort the list reverse alphabetically
Suchoice's tagline was "AI-powered decision making", which named the
implementation instead of the thing you get. It now reads "Six ways to decide,
from a coin flip to 3D dice", which is what the product page already says and
is concrete enough to tell you whether you want it.

Order is now derived, not hand-maintained: sortedApps sorts on name descending
at render. Hand-ordering the array works right up until someone adds an app to
the end and the page is quietly wrong, which is the same class of problem as a
hand-maintained timing constant.

Reverse alphabetical gives Vegan IQ, Suchoice, Such Moon Launch, Hash Bags,
Bloomword, Bauhaus Echo. Note Suchoice sorts above Such Moon Launch: the
comparison is locale-aware, so the space in "Such Moon" does not simply win on
byte order the way it would with a raw string compare.
2026-08-18 10:23:47 -04:00
jwinterm b1590c3a2b feat(apps): the Hash Bags icon draws itself on
An animated icon has to still BE the icon, so the geometry is copied verbatim
from the app's own icon source (hash-wallet
assets/images/hash_wallet_app_icon_source.svg): full-bleed #1a5c38, four white
lines at stroke-width 80 with square caps, horizontals at y=412/612, verticals
at x=412/612, all spanning 212..812. It draws on in 850ms and settles into a
frame that is pixel-for-pixel the static PNG.

Not hash.boats/splash.svg, which was the obvious candidate and is wrong twice:
its # is slanted where the shipped icon is upright, and it insets its green box
inside a transparent margin. The site rounds icons with a 22% CSS radius, so
that margin would have shown card background at the corners, which is the exact
artifact we just finished removing from the icon set.

CSS rather than SMIL, for the reason the brand pack already documents:
prefers-reduced-motion is a media query and cannot reach inside a SMIL
<animate>. Under reduced motion this is just the finished icon.

Draws once and holds, rather than looping. Five icons looping forever on a
directory page is a distraction, and an app icon's resting state should be an
app icon.

`animatedIcon` is optional per app and `unoptimized` is set only when it is
used, because the image optimizer would rasterise the SVG and throw the
animation away.
2026-08-18 08:33:01 -04:00
jwinterm 0b1c4000c3 feat(hash-bags): add Hash Bags to the apps page, and give it a product page
Live on Google Play (com.suchsoftware.hashwallet); the iOS build is in review
and desktop follows, so both render through StoreButtons' existing null-href
"Coming Soon" state rather than claiming availability we do not have.

Content is taken from the project's own README and store-listing draft rather
than invented: open-source, non-custodial, Wownero-first, twelve chains, and
Ledger/Trezor/BitBox support. The "why it exists" section is the real reason,
which is the interesting part of this product: Cake Wallet ended Wownero
support in early 2026 and this fork keeps it first-class.

Icon is the shipped play-icon-512, already square and opaque, so it takes the
site's rounded-[22%] cleanly and cannot show a transparent corner wedge.

Also linked out to hash.boats, listed on /products, and added to the sitemap.
2026-08-17 14:20:11 -04:00
jwinterm 835c5c8084 feat(splash): a colour burst as the mark hands over to the page
Subtle and half-transparent, per the brief. Four blobs, one per monogram cell,
each in that letter's own colour, thrown outward from where the letter was: the
mark comes apart into its own palette rather than flashing something unrelated.
The peak opacity is 0.42 light / 0.5 dark and the gradients never reach full
alpha, so it stays a wash rather than a curtain.

Only transform and opacity animate. Both are compositor properties, so this
does not repaint per frame. That is deliberate: animating a filter is what
pegged a core on the matrix animation, because a filter change re-renders the
whole filter graph every frame. Soft edges come from a static radial-gradient
that is painted once and only ever transformed.

The overlay could no longer fade as one piece. Ground and mark fade in 260ms
while the burst runs 420ms plus a 25ms per-blob stagger, so fading the
container would have taken the burst down with it halfway through. They fade
independently now and the shell unmounts at 540ms instead of 300ms, which is
the burst's real length rather than the cross-fade's.

Hidden entirely under prefers-reduced-motion, though those visitors never
reach the splash anyway.
2026-08-17 14:19:56 -04:00
jwinterm 887b59629d feat(splash): the pillar signature, and no more flash on refresh
Two separate problems, one of them a real bug.

THE FLASH. The splash is server-rendered, so on a refresh the browser painted
it and only then did hydration read sessionStorage, decide this visitor had
already entered, and remove it. That one painted frame is the flash, and React
cannot prevent it: even useLayoutEffect runs after the server HTML is painted.

So the decision moved before paint. A tiny synchronous script in <head> reads
the session flag and prefers-reduced-motion and stamps the root element; a
plain CSS rule hides the overlay in the frame it would otherwise appear in.
The overlay is `fixed`, so hiding it shifts no layout. Verified with a
Playwright run: after reload the root is already stamped at readyState
"interactive", the overlay computes to display:none, and the splash is not
visible in any of twelve sampled frames.

THE SPLASH ITSELF. It is now only the pillar signature: four dots become the
four pillar symbols -- commerce, crypto, games, education -- become the word
SUCH, then the page takes over. One generated asset (morph_splash*.svg) whose
CSS timeline is scaled to land exactly on 1337ms, from such-graphics 628fcc1.
SPLASH_MS here and motion.splash.ms there are the same number by construction:
a gate that closes early cuts the last letter mid-draw.

Everything else on the splash is gone -- tagline, supporting line, the four
corner nav links, the chevron, the water-refraction filter and its pre-warm.
The corner links were unreachable inside a gate this short anyway, and the
header carries them. splash-logo-data.ts goes with them, now orphaned.

Perf, measured on a local production build: mobile 97, desktop 98, unchanged
from what production scores today. CLS stays ~0. LCP moves 0.9s -> 1.2s on
mobile, because the mark is now a 12KB request where the old logo was an
inlined data URI. Both theme variants are fetched (3.7KB each gzipped) since
`dark:hidden` hides but does not prevent the download, so half of that is
waste. Fixable by inlining as data URIs the way splash-logo-data.ts did, at
the cost of ~30KB in the bundle; not done here because the score did not move.
2026-08-17 12:11:36 -04:00
jwinterm 787753aef1 feat(icons): new Bauhaus Echo and Suchoice icons, and one radius everywhere
New store icons from bauhaus-echo/assets/store/app_icon_512.png and
suchoice/assets/icon_1024.png, both resampled to 512 and both square and fully
opaque. Bauhaus drops 320KB -> 72KB, Suchoice 259KB -> 142KB.

The corner artifact had a measurable cause. Five icons bake their own rounded
corners as transparency (suchoice 20.5%, smirk-wallet 20.1%, vegan-iq 19.3%,
mushroom-capital 17.0%, such-moon-launch 15.2% of their width); the rest are
square. CSS then rounded them again, at a radius that differed per page:
21.4% on /apps at 56px, 25% at 64px, and 20% at 80px.

20% is BELOW suchoice's 20.5% baked corner, so on /products/suchoice the CSS
clip fell inside the transparent corner and a wedge of card background showed
through at each corner. That is the "different coloured stuff next to the
rounded corners": not a rendering bug, an arithmetic one.

Every square app icon now uses one percentage radius, rounded-[22%]. It is
above every baked radius in the set, so no transparent corner can ever be
exposed, and being a percentage it stays identical at 56, 64 and 80px instead
of drifting with the pixel size. The two new icons are square, so they cannot
produce the artifact at any radius.

The non-square occupy-wallets banner keeps a fixed radius: a percentage on a
non-square box goes elliptical.
2026-08-17 11:44:40 -04:00
jwinterm 848deaf371 fix(brand): regenerate the games pulse so its arc clears the frame corners
Copied through from such-graphics db616f8. The launch arc's feet ran to y=178,
inside the frame's corner arcs (centred (26,174) and (174,174), r=12), so on
/apps the leg, its cap and its node dot all sat on the corner and the mark read
as broken. The feet now stop at the glyph baseline, y=164.

Generated, not hand-edited: the pack is canonical and a regen would have
overwritten a local fix.
2026-08-17 11:35:16 -04:00
jwinterm 1a66e2217a feat(splash): enter the site on its own, and stop the intro costing 26 points
The splash now runs its water-ripple enter 1.5s after paint instead of waiting
to be clicked. A click, scroll, or arrow key still fast-forwards it, and the
once-per-session flag and reduced-motion skip are unchanged.

Auto-advancing on its own would have cost the Lighthouse score badly: mobile
100 -> 73, desktop 97 -> 73. The cause was not LCP, which never moved. It was
CLS going 0 -> 1.0, and the reason is worth writing down.

The splash was `relative min-h-[100svh]`, a full viewport of height IN document
flow, and only became `fixed` on the way out. Dismissing it therefore pulled a
whole screen out of the flow and shoved the hero up: a layout shift of almost
exactly 1.0. Clicking hid that from CLS, because shifts within 500ms of user
input are excused. Nothing else did.

Which means this was already shipping to real people. Visitors with
prefers-reduced-motion, and anyone returning within a session, never click, so
they took the shift in full: production measured 74 on mobile with CLS 1.0.
The click-gate was masking a live bug, not preventing one.

The splash is now a fixed overlay from its first paint, so it is never in flow
and dismissing it cannot reflow anything. Measured on a local production build:

                          before -> after
  mobile, auto-advance     73 -> 98   (CLS 1.0   -> 0.005)
  mobile, reduced-motion   74 -> 98   (CLS 1.0   -> 0.005)
  desktop, auto-advance    73 -> 98   (CLS 0.702 -> 0.011)

Mobile gives up 2 points against today's click-gated 100, entirely in Speed
Index (1.9s -> 4.4s), which is the unavoidable cost of holding a cover at all.
Desktop ends up ahead of where it started. The hold is one constant if you want
a different beat.

The scroll-past-to-dismiss IntersectionObserver is gone: an overlay never
leaves the viewport, so it could not fire. Scroll intent still enters, via the
wheel/touch/key handlers already in the splash, and the shell now locks page
scroll while the cover is up.
2026-08-17 09:13:29 -04:00
jwinterm e1ea0ce18c chore(lint): migrate off next lint to the ESLint CLI and flat config
`next lint` is deprecated and disappears in Next 16. apps/web now runs
`eslint .` against eslint.config.mjs on ESLint 9, and the shared preset moves
to packages/config/eslint-preset.mjs. eslint-config-next 15 still ships only
an eslintrc-style config, so FlatCompat translates it; that keeps
next/core-web-vitals as the single source of rules.

The migration was done by hand rather than via @next/codemod, because the
codemod does not know about the shared @repo/config preset.

Two things worth knowing.

ESLint 9 only walks file extensions that some config names in `files`. The
first working version of this config linted three loose .js files, skipped
every .tsx in src/, and still exited 0 -- a green run that checked nothing.
The preset now names the extensions explicitly. Coverage went from 3 files to
82, and a deliberately broken canary component confirms rules still fire.

`@eslint/eslintrc` also needed a second scoped override: it does a default
import of minimatch and declares ^3.1.2, which the global ^9.0.7 audit
override broke. Same shape as the ajv fix in the previous commit.
2026-08-17 06:06:14 -04:00
jwinterm 1dc0c5f06a fix(lint): make the linter run again, and finish the em dash sweep
The linter had two faults stacked on top of each other.

First, the global `ajv: ^8.17.1` override starved @eslint/eslintrc, which
needs ajv 6 and passes it `missingRefs`; ajv 8 rejects that option, so ESLint
died before loading a single rule. Scoped overrides give eslint and
@eslint/eslintrc ajv 6.12.6 and leave the audit override in place everywhere
else.

Second, underneath that, `extends: ["@repo/config/eslint-preset.js"]` could
never resolve: ESLint's shareable-config naming convention rewrites it to
`@repo/eslint-config-config/...`. require.resolve hands ESLint a real path.

With the linter running it reported 35 react/no-unescaped-entities errors
across 7 files. Those are escaped rather than silenced, using ESLint's own
coordinates so only flagged text nodes were touched. Lint now exits 0. Two
@next/next/no-img-element warnings remain on the splash logos, which are
deliberate: theme-paired and fetchPriority="high".

Em dashes are also gone from the six blog posts and the README (105
occurrences), same rules as the site copy: colons for definition labels,
commas for appositives, semicolons for independent clauses.
2026-08-17 05:53:16 -04:00
jwinterm 0848fcabde feat(site): the tagline takes the title, and the em dashes go
The hero h1 is now "Precision Engineering for Everyone". The concrete offer
moves into the supporting line, which keeps the search terms the h1 gives up
and no longer restates the splash's opening sentence.

The title's gradient drifts instead of sweeping: a 300% ramp over 18s with
symmetric easing, so there is no hard turnaround at either end of the travel.

Em dashes are gone from every tsx/ts file (87 lines, 104 occurrences), per
AGENTS.md rule 9. Label lists and titles take a colon; appositives take
commas; independent clauses take a semicolon or a period. A few sentences
were restructured where no single mark read well.

Metadata keeps "Crypto" deliberately: it is a real service line and a real
search term, and it is no longer in the visible headline.
2026-08-17 04:18:18 -04:00
jwinterm 261cb04667 feat(site): one voice, brand gradients, a lighter splash gate, denser portfolio
- Voice unified on the title-tag register: the homepage h1 now says what we
  sell (Custom E-commerce, Apps & Crypto Tools); 'Precision engineering for
  everyone' is demoted to the supporting line. The structured-data and
  manifest descriptions now match the OG description instead of carrying
  their own registers.
- The off-palette cyan/blue gradients are gone: .hero-title and the new
  .brand-gradient-text use the four-colour system (light palette on light,
  dark palette on dark, per the pack). Splash cover text, services and
  portfolio headings, the hero aura, and button shadows all follow.
- The splash now dismisses once 40% scrolled past instead of requiring the
  whole screen to leave the viewport (it was already once-per-session and
  reduced-motion-skipping).
- Portfolio: heading 'Portfolio', subtitle 'Products & projects', meta title
  to match; grid grows to three columns on xl in a max-w-6xl container.
- anim3_matrix slimmed: single-layer glow, unfiltered crackle and orbs, and
  the border cycles stroke colour instead of animating filter:hue-rotate --
  the filter animation forced a full filter re-render per frame and pegged a
  core on the page hosting it.
2026-08-16 00:04:28 -04:00
jwinterm c73b9f1c04 feat(brand): regenerate every mark and animation from the such-graphics pack
All branding and animation assets now come from packs/such-software/pack.json
(such-graphics feat/brand-full-blast) instead of hand-maintained files:

- anim1_pulse / anim3_matrix / anim_type_v4 (both themes): regenerated with
  the corrected U bowl, measured pulse timing (the hand-authored node delays
  ran ~17% fast), and CSS animation with real prefers-reduced-motion support.
- clean marks, lockups (clean_*_w_text), favicons (svg + 192px png), and the
  OG banner: regenerated; the OG card is the pack's 1200x630 projection.
- splash-such: rebuilt from the pack -- the old one carried the divergent U,
  an off-palette cyan frame, and the pre-correction radius. Now theme-aware:
  two inlined data URIs (hero-splash shows the light palette on light, the
  dark palette on dark) since the pack's dark hues are tuned for near-black.
- new assets available: games_pulse (the launch arch with the rocket, wired
  into /apps), morph, ambient_{atom,mesh,leaf}, favicon_dots, favicon_live,
  icon-192/512 + maskable-512 (wired into manifest.json), /favicon.ico.
- animation embeds switched <img> -> <object>: Chrome freezes CSS animations
  inside SVG-as-img (the old files animated because they were SMIL); an
  object hosts a real SVG document where CSS animation and the
  reduced-motion query both work. Verified live in Chrome.
2026-08-15 23:18:44 -04:00
jw b6fb6b743d fix(moonlaunch): disclose the telemetry and analytics the game actually sends
The policy described leaderboards and cloud save as the only things that leave
the device, and stated outright: "This data is not transmitted to our servers
unless you use the leaderboard or cloud save features." That is not true.

- Gameplay telemetry POSTs to api.such.software on every platform, automatically
  rather than opt-in, tagged with the anonymous device ID, display name and
  platform. It carries level starts and completions with times and stars, how a
  run ended (cause, hazard, speed, fuel, progress through the level), rewarded-ad
  and share actions, purchase attempts, and error messages. None of it was
  disclosed.
- Google Firebase (Analytics and Crashlytics) ships in the mobile builds and
  receives the same device ID as a user property alongside app version, device
  model, OS version and language. It was not mentioned anywhere, including in the
  third-party recipients table.
- Cloud save uploads automatically on every save, not only on an explicit action,
  and its payload now also carries store purchase receipts (the ledger that stops
  a purchase being credited twice).
- The page claimed Google AdSense serves ads on the web build. It does not:
  web/custom_shell.html records that the AdSense placeholders were removed and
  web monetization is handled in-game. Removed the claim and the AdSense row from
  the recipients table.

Left as-is: the statement that iOS serves non-personalized ads and does not
access the IDFA. No code path presents the ATT prompt, so the identifier stays
zeroed and that claim holds.
2026-08-15 11:17:50 -04:00
jw a6ad27b3e2 fix(moonlaunch): stop advertising paid desktop builds as free
The product page claimed "Free on iOS, Android, web, desktop" in its meta
description, OpenGraph description, and SoftwareApplication JSON-LD, and backed
that with operatingSystem "Android, iOS, Web, Windows, macOS, Linux" alongside
price "0".

Desktop is not free. The Windows/macOS/Linux builds are itch.io downloads behind
a $1.99 minimum purchase; only iOS, Android, and the embedded browser build are
free. Declaring price 0 across an OS list that includes the paid desktop targets
misstates the offer to search engines, which consume the JSON-LD for rich results.

Scope the free-offer structured data to the platforms that are actually free. The
visible Cross-Platform card still lists all six targets, which is accurate: those
builds do exist, they are simply not all free.
2026-08-14 07:47:13 -04:00
jw c50f4d29f6 fix(bloomword): stop claiming browser play
The site said Bloomword was "playable free in the browser at bloomword.earth"
in four places. It is not: bloomword.earth is the marketing and transparency
site, and iOS and Android are the only platforms you can play on.

This is the full-surface release standard's web-app gate -- marketing copy
flips from "learn more" to "play" only after play.<domain> passes a parity
proof against the mobile core loop -- and that surface does not exist yet
(play.bloomword.earth does not resolve). Every claim becomes "learn more at
bloomword.earth" with iOS/Android named as the only playable platforms, per
the standing item in the Bloomword roadmap.

The VideoGame structured data already declared gamePlatform [iOS, Android]
and needed no change; the footer link carries no play claim and is untouched.

Not built or typechecked: this workstation has no node. The changes are
string literals inside existing quoted copy, with one JSX text edit that
keeps its {" "} separator and closes the sentence after the anchor.
2026-08-13 10:20:04 -04:00
jwinterm d3916fd0e8 bloomword: use the real app-icon PNG everywhere (was the flat SVG)
Swaps /images/products/bloomword.svg for bloomword.png across the apps
index, the products index, the Bloomword product page, the bento grid,
and both services.ts proof lists (games + education), so the icon
matches what actually ships on the App Store and Play listings.

The SVG is now unreferenced; leaving it in place for now.
2026-07-28 21:04:19 -04:00
jwinterm c7c3cb26eb fix(vegan-iq): rewrite privacy policy to match v1.4.6
The policy was stale (March) and materially inaccurate: it claimed data never
leaves the device and there are no leaderboards (false — device_id + display_name
sync to the backend), described AdMob ads + a Remove Ads purchase (the app shows
no ads as of v1.4), and omitted Firebase Analytics/Crashlytics/Messaging. Rewrite
to reflect actual data practices. Needs deploy for the in-app link to be accurate.
2026-07-26 05:08:26 -04:00
jwinterm f01f5b2e92 services CTA: link straight to /#contact (skip the /contact 307 redirect hop) 2026-07-13 12:34:56 -04:00
jwinterm 274b0d82f5 services hub: rework hero + de-sparse the middle sections
Hero: 'Four things, done well' -> 'What we build' (add pb-3 so the gradient-clip descenders
aren't cropped), and drop 'PhD-led radiation-science education' from the subtitle for
'education from radiation science to word games' (education now spans well past nuclear:
ZDD, Numchangers, Bloomword, Vegan IQ).

Middle: the near-empty 'Partnership-first' and 'Recent work' section-container cards (a
heading + one line inside p-20 padding) read sparse. Partnership-first is now a dense 3-step
row (Scope / Build / Run it together); Recent work drops its empty header box for a compact
heading above the Occupy Wallets card.
2026-07-13 12:16:55 -04:00
jwinterm fd1042ea96 services: cross-list Grin (->crypto), Barns & Numchangers (->games) for consistency
Apply the same 'list a product wherever it genuinely belongs' rule already used for
Evaluetron (commerce+crypto) and Bloomword/Vegan IQ (games+education): the Grin BTCPay
plugin is a crypto payment integration, and Barns & Neutrons + Numchangers are science/
math games we ship. Each keeps its original pillar and gains the second with a pillar-
angled blurb.
2026-07-13 12:11:36 -04:00
jwinterm 1fc2cce0e0 bloomword live on stores; add Zero-Day Defender + edutainment to education pillar
Bloomword is out on iOS + Android: added it to /apps (featured first) with store links,
put StoreButtons on /products/bloomword, and flipped all 'coming/launching' copy to 'free
on iOS and Android' (product page, products index, services games blurb, FAQ).

Education pillar: added Zero-Day Defender (cybersecurity edutainment, Coming soon) and
cross-listed Bloomword + Vegan IQ from Games & Apps — they're genuinely both a game we ship
and a game that teaches. Broadened the pillar lead + card to frame the edutainment shelf
(science, math, vocabulary, nutrition, cyber-safety) alongside the PhD radiation-science work.
2026-07-13 12:03:24 -04:00
jwinterm 87586eddd7 copy: Chart of Nuclides, not Table (correct term) — barns tile + all barns copy/SEO/OG 2026-07-12 14:21:21 -04:00
jwinterm 651be20100 home: drop Selected Works card + juice hero; per-pillar OG cards; sitemap freshness
Home: the big empty 'Selected Works' section-container card is gone in favour of a compact
centered heading, pulling the bento up ~300px so the work leads directly under the hero.
Hero juice (all LCP-safe + motion-safe): the H1 gets an animated gradient sheen (only the
gradient POSITION animates, so the largest paint is never delayed; verified opacity stays 1),
a soft breathing cyan aura sits behind it (aria-hidden), and the CTAs get a cyan glow + hover
lift.

Services: each pillar page (and the hub) now generates its own OG social card via the shared
gameOgImage helper with a 'Services' badge and the pillar accent, so sharing a pillar shows
a distinct card instead of the generic banner. Verified: distinct per-slug image URLs, valid
1200x630 PNGs.

Sitemap: bump lastModified 2026-06-01 -> 2026-07-12 (we've meaningfully changed the site).
2026-07-12 14:15:46 -04:00
jwinterm 1e5719a1da services: split the four pillars into dedicated subpages
/services becomes a hub (hero, four linked pillar cards, philosophy, recent work, FAQ,
CTA) and each pillar gets its own statically-generated page at /services/[pillar]
(commerce/games/crypto/education) with distinct SEO title+description+canonical, its own
H1, the products behind it, a pillar-scoped FAQ + FAQ JSON-LD, breadcrumb JSON-LD, and
'the other pillars' cross-links. dynamicParams=false so unknown slugs are a static 404.

The pillar/proof/FAQ data + colour maps move to lib/services.ts (single source of truth),
ProofCard/PillarCards/ServicesCta/EducationCredentials to components/services, so the hub,
subpages, home 'What We Do' preview, and sitemap can't drift. Home preview cards now link
to their pillar; sitemap gains the four subpage URLs. All pillar copy kept verbatim.

Verified: 4 subpages prebuilt, distinct titles/canonicals, right products per page,
click-through nav, education credentials block, bad slug -> 404.
2026-07-12 13:47:19 -04:00
jwinterm 20e0962fbc bento: smirk wallet 3D coin vignette
The flat lockup becomes a live tile: a 3D smirk coin (transparent, sg_outline inset ink,
dark rim) that rests smirk-up, tosses end-over-end on hover with the suchoice coin-flip
feel, lands $-up, and half-flips home for a seamless loop. 518KB webm + transparent
poster; web_qa gated.
2026-07-12 08:27:30 -04:00
jwinterm e1f7f3c49d home: scroll-enter lands at the very top; bloomword motion front-loaded; webshops copy
Scroll-enter: the triggering gesture's MOMENTUM outlives the 800ms wave (a trackpad
flick trails ~1.5s), and those ticks scrolled the revealed page so you landed below
the hero. The scroll lock now holds until the input goes quiet (250ms without a
wheel/touch event, 3s cap) and releases exactly at the top. Verified with a simulated
14-tick 1.5s flick: lands scrollY=0, and a fresh gesture afterwards scrolls normally.

Bloomword recut: the loop opened with a 1s bloom hold so hover felt dead (movement
backloaded). Now a 0.2s hold then a brisk 3x retract into the full regrow, closing bob
at the end; still frame0==frameN (seam 0.0000), 390KB.

Webshops tagline: 'One platform, unlimited possibilities.'
2026-07-12 08:19:45 -04:00
jwinterm e3ce7bdbba bento: 2x2 webshops layout + custom 1.2:1 reel crop + crisp vegan-iq re-render
New shape (drops Bauhaus): Webshops 2x2 feature / Smirk + Bloomword stacked right /
Barns 2x1 + Vegan IQ bottom. Grid gains md:row-span-2.

Webshops reel rebuilt at 1.2:1 (864x720) to match the 2x2 tile box with PER-SHOP crop
offsets (artisan's headline is left-anchored, coaching's box sits right of center, the
rest center) so no shop's hero text gets clipped by the cover fit.

Vegan IQ re-rendered at 1600x900 with the card at 95% height (text was landing on too
few yuv420p samples at 720p and read pixelated); webm 488KB, seam still 0.0000.
2026-07-12 08:06:44 -04:00
jwinterm 02b56acfe1 bento: overhaul — webshops morph reel, bloomword vignette, vegan-iq card flip
New lineup (drops Occupy Wallets + Wownerogue): Webshops (span 2) leads with the
theming-morph reel — the 5 re-snapped post-glowup demo shops crossfading, proof that
one platform yields completely different storefronts; Bloomword gets a transparent
chroma-keyed tree vignette (full bloom at rest, hover retracts + regrows, palindrome
seam 0.0); Vegan IQ gets a brand-panel card-flip reel from its own social pipeline
(logo -> real quiz card unanswered -> answered, seam 0.0); Barns/Smirk/Bauhaus stay.

HoverVignette grows fit=cover (opaque reels fill the tile, poster = tile art), an
optional H.264 mp4 source (Safari can't decode VP9), and on-screen autoplay for
no-hover devices (IntersectionObserver >= 0.5) so phones see the animations at all.
All four live tiles verified with Playwright: correct video plays on its tile's
hover, others stay paused.
2026-07-11 11:15:50 -04:00
jwinterm 62fee8b83a home: bento above What We Do; splash enters on scroll intent; pre-warm the water filter
The page after the splash was two screens of text before the first image. Selected
Works (the bento, with the live barns vignette) now leads and the services copy
supports it below, wrapped in the same scroll-reveal.

Splash: the first wheel tick, 24px touch drag, or scroll key now plays the SAME
water-ripple enter as a tap (scrolling past used to hard-cut after a full screen).
Listeners are passive, the entering guard dedupes, reduced-motion still gets the
instant dismiss.

Pre-warm: on requestIdleCallback (post-LCP, invisible to Lighthouse) a 1px element
paints through the splash-water filter so the graph compiles before first use (the
first-tap stutter), and the logo layer gets will-change ahead of the zoom.

Verified with Playwright against the production build: wheel/key/touch all enter,
session flag set, bento renders above What We Do.
2026-07-11 10:45:50 -04:00
jwinterm 679e1befdf fix(bento): barns wordmark — ink the letterform crease, not the solid's silhouette
The stroke was traced from the WHOLE extruded solid's alpha (front face + side walls),
so the line sat at the side wall's outer edge, offset from the glyphs — mushy letters.
Now a third render pass emits the FRONT FACES only (depth-0 TextMesh, z-seated at the
full mesh's front plane) and the stroke traces that mask: ink lands exactly on the
letterform crease and the darkened extrusion reads as the 3D body behind it. Also
gates the absorption flash out of mask passes (it could cross the mask threshold near
a strike and get inked). web_qa self-gate green (0.0% stripped, seam 0.0000, 512/600 KiB).
2026-07-11 09:53:42 -04:00
jwinterm 3d0c20d38e fix(bento): barns vignette — outline letters only + 3D crease, clean beta/nuclei
Per feedback: the whole-frame alpha stroke outlined the flying beta and the background
nuclei and only traced the outer silhouette (no internal creases). Now:
- Stroke from a WORDMARK-ONLY mask pass (nuclei + beta hidden) so only the letters get
  the ink line — beta stays a soft blue glow, nuclei stay clean.
- Darken the extruded SIDE WALLS so the front-face→side CREASE reads as a toon edge that
  a silhouette stroke can't reach.
outline2d gains an optional --mask (outline one element); mask-less still outlines
everything for a full cartoon look.
2026-07-11 09:02:33 -04:00
jwinterm 2b73a8b5ef fix(bento): barns vignette — real black outline + coherent blue beta, single theme-independent asset
Root causes (all 3D->web pipeline law violations):
- Beta 'flashing / getting deleted': the MatterWave beta was BLEND_ADD (writes ~no
  alpha) so it was stripped by the transparent capture, AND its off-screen auto-free
  killed the packet a third of the way through its flight. Now alpha-safe (MIX under
  BN_ALPHA), pinned alive for the full flight (no_free), bigger + slower + brightness-
  capped so it reads as a coherent BLUE de Broglie packet the whole way in.
- 'No black outline': the 3D inverted-hull shell was sub-pixel + washed by the fresnel
  rim (invisible on light, flecky on dark). Replaced with a 2D silhouette INK STROKE
  from the alpha matte (sg_outline, outline2d.py) — uniform, gap-free, and INSET so the
  black line sits ON the yellow fill and reads on BOTH themes from ONE asset.
- Softened the ripple pulse so the crest stays a blue tint, not a white clip.

Seam diff frame0==frameN = 0.0 (perfectly seamless).
2026-07-11 08:41:18 -04:00
jwinterm 9ae740c21c fix(bento): barns vignette — clean yellow rest, seam-clean loop, subtle blue beta
Clean rest (frame 0 no longer catches a wrapped ripple: 2 strikes decay before
the seam), no white blowout (pulse strength down + blue core + lower HDR-bloom),
thinner outline, bigger wordmark.
2026-07-08 12:42:13 -04:00
jwinterm 90e7cb58ea fix(bento): per-theme barns vignette + alpha-safe beta (survives transparent capture)
Two fixes to the barns hover vignette after seeing it live:
1. The beta was STRIPPED by the transparent capture: the flash/particle used
   additive blending, which writes ~no alpha, so it vanished in the alpha webm
   (only the opaque letters survived = that brief white "lightning"). Switched the
   flash to alpha-blend and pushed the absorption onto the opaque letters (strong
   blue surface-ripple pulse), so the beta now survives on BOTH light and dark.
2. Light + dark mode need different ink: a near-black outline vanishes on a dark
   tile; a light outline vanishes on a light tile. Render two baked assets
   (barns-light = dark ink, barns-dark = cream ink) and HoverVignette picks by
   resolved theme (next-themes). Added a clean rest poster (no active strike) via
   a BN_POSTER render so at-rest looks right.
2026-07-08 12:21:32 -04:00
jwinterm d4f65489b5 feat(bento): barns vignette v2 — toon outline + recurring beta strikes
Extruded ChakraPetch wordmark now has an inverted-hull ink outline (crisp toon
edges) and 3 staggered beta strikes across the 5s loop with overlapping ripples,
so the tile stays alive instead of flashing once. Rendered on the 1080 Ti.
2026-07-08 11:50:26 -04:00
jwinterm a238412103 feat(bento): quiet tiles at rest — chrome fades, title + render float
At rest the card chrome (opaque bg, 64px frost, border, shadow) is now nearly
invisible so tiles read as just the title + the 3D render/image floating on the
page (especially clean on mobile, where every tile is 'at rest'). The glass
lights up + the colour tint paints on hover / focus / tap. Title and image keep
their visibility.
2026-07-08 11:15:01 -04:00
jwinterm c93421c083 feat(bento): live hover vignettes + Barns & Neutrons feature tile
Add HoverVignette: a transparent alpha-WebM 3D render that plays over a tile on
hover/focus. Poster-first, preload=none, reduced-motion-safe, Safari-degrades to
the poster (no VP9-alpha). Feature tiles drop the static screenshot (the poster
is the rest state) and grow + lift above neighbours on hover so the animation is
the focus.

Rework the grid to six gap-free tiles: drop Neroswap, shrink Wownerogue to span 1.

First vignette: Barns & Neutrons — extruded 3D "BARNS & NEUTRONS" in the brand
ChakraPetch face with ambient drifting nuclei and a beta strike that fires a
surface-ripple + blue absorption flash across the letters. Rendered transparent
on the 1080 Ti via such-graphics.
2026-07-07 13:18:24 -04:00
jwinterm a5647735d4 feat(services): restructure into 4 pillars + seamless morph loop
Services page was a long undifferentiated scroll of six ad-hoc sections.
Reframe it as four clear pillars, each led by an intro and backed by the
real products behind it:
- Commerce & web: Webshops, custom builds, Grin BTCPay plugin, Evaluetron
- Games & apps: Suchoice, Vegan IQ, Bloomword, Bauhaus Echo, Such Moon Launch
- Crypto & payments: consulting, Smirk Wallet, Hash Bags, Evaluetron
- Education & science: Barns & Neutrons, nuclear training, Numchangers, creds

Add a pillar jump-nav so the page reads as four areas, and a ProofCard that
links shipped products to their detail pages (image tile) and shows
pre-release ones as informational tiles with a status pill.

Also refresh the theming-morph reel with the seamless-loop build (crossfades
the last shop back to shop #1 instead of hard-cutting at the <video loop>).
2026-07-07 09:02:39 -04:00
jwinterm c41c7f28ee feat(webshops): refresh demo shots (now genuinely distinct) + varied surfaces gallery + booking callout
The five demo storefronts were re-themed to look like different companies
(distinct hero styles, card styles, density, grid, and homepage composition),
so re-snap the hero cards and regenerate the theming-morph reel from the
fresh shots.

Add a "Every surface, not just the homepage" gallery: six live surfaces
across different shops (yoga + coaching booking calendars, an editorial
product page, a dense catalog grid, a crypto product, a themed cart) so the
page shows platform depth the homepage crops hide.

Call out booking/scheduling explicitly: a new "Booking & Scheduling" feature
card, a booking FAQ (feeds FAQ JSON-LD), and a booking-calendar line in the
surfaces lead.

Demo images downsized to lean 1280w JPGs (png -> jpg).
2026-07-06 12:35:46 -04:00
jwinterm 99ce876cdd feat(site): theming-morph showcase hero on /products/webshops
The 'render once, serve cheap pixels' showcase, live. A 7s reel of the same
platform themed as five real demo brands (artisan -> merch -> crypto -> yoga
-> coaching), inside a decorative browser frame, at the top of the webshops
page.

- ShowcaseVideo: poster-first, preload='none' (no download until played),
  plays only while in the viewport (IntersectionObserver), pauses offscreen,
  and prefers-reduced-motion -> poster only (video never loads). muted +
  playsInline for mobile autoplay-on-view.
- BrowserFrame: static decorative chrome so the reel reads as a real shop.
- 16:9 assets ~330-355KB (webm/mp4) + 50KB poster, all within the motion budget.
2026-07-06 10:39:40 -04:00