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.
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.
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.
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.
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.
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.
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.
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.
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.
`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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
/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.
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.
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.'
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.