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.
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.
- 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 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.
- Remove framer-motion from the two components that pulled it into the homepage's
initial bundle: HeroSplash (click-to-enter wash -> CSS keyframe; reduced-motion
check moved into the handler) and ScrollReveal (whileInView -> IntersectionObserver
+ CSS transition). framer now only lives in the lazy nebula chunk, which never loads
during the splash. First Load JS on / : 174 kB -> 138 kB.
- Monitor terminal: blink ~7 times before typing 'such' (so it's catchable even when
the splash is skipped on refresh), then type/hold/backspace/blink/fade as before.
Desktop behavior unchanged; both converted components still respect reduced-motion.
- The high-priority <link rel=preload as=image> stole bandwidth from render-
blocking CSS on 4G, pushing FCP 1.1s -> 2.2s and the score 82 -> 70. Removed.
- TBT is ~30ms (not main-thread bound), so the FCP->LCP gap was the logo's
network fetch. Inline the mark as a data URI: zero request, paints with FCP.
LCP should drop toward FCP and stop swinging run-to-run.
- Logo is a plain <img fetchPriority=high> in a plain div (no framer-motion,
no drop-shadow filter) so it is a clean, statically-painted LCP candidate.
Water filter + a CSS zoom apply only on enter.
- Tracer target nudged to computerX/Y +1.5 (between the low-right +2.5 and the
high-left +0.5).
- Inline <svg> is not an LCP candidate; on mobile the tagline is hidden so the
logo was the only large element -> NO_LCP. Serve the mark as
/images/branding/splash-such.svg and render it via <img> (a valid LCP
candidate, painted from SSR HTML).
- Transition: start the cross-fade on click (shimmer plays as the splash fades
into the content) and shorten durations 1.2s -> 0.8s; snappier and blended.
- Tracer target back to the midpoint (computerX+0.5, computerY+0.5).
- Drop the two animated glow rings: stroke-dashoffset is non-composited, so
they repainted every frame next to the logo and prevented Lighthouse from
settling on an LCP (mobile NO_LCP). The static box + pulsing glow remain.
- Splash now cross-fades into the content: on enter it becomes a fixed overlay
and fades opacity to 0 over the revealed page, instead of a hard cut.
- Nebula tracer target moved up-and-left (computerX-1, computerY-1.5) so the
streamers converge on the monitor screen, not below-right of it.
- Removed the now-unused splash-* keyframes.
- Revert the nebula density/parallax change that disturbed the carefully
tuned streamer convergence (it gave no perf benefit; the nebula is behind
the splash and not in the load trace).
- Logo is now fully static (no float/breath) so it's a stable LCP candidate
on mobile; the rings, glow pulse, and staged button fade-ins still carry
the motion. (Desktop TBT win from the framer->CSS conversion is retained.)
- The splash's always-on animations (logo float/breath, the two traveling
glow rings) move from framer-motion (JS rAF every frame on the main thread)
to CSS keyframes (browser/compositor). framer now only drives the one-shot
click-to-enter water exit.
- Nebula: density 8 -> 4 and dropped the scroll parallax (the Forced-reflow
source); reduced-motion still gets the static background.
The logo stays painted at first render (LCP-safe); the tagline, corner
buttons, and Explore hint now stagger-fade in via Tailwind animate-in
(CSS/compositor, no JS main-thread cost). motion-safe only.
The splash logo (the LCP element) started at opacity:0 and only faded in
after framer-motion hydrated, pushing mobile LCP to ~7s. Render it visible
immediately; the float/breath animations still layer on top.
Drop the expanding circles. Clicking the logo now runs an SVG
feTurbulence + feDisplacementMap filter over the mark, ramped by a rAF
loop, so it ripples like a disturbed liquid surface and settles, with a
soft Cherenkov wash + backdrop blur, then enters. Reduced-motion still
enters instantly.
- Hide the cover tagline and subheader on mobile (logo + corner icons carry
it); only the labeled pills/tagline show at sm+.
- On enter, fade the logo, tagline, buttons, and Explore cue out while the
click-triggered wave plays.
- Waterier transition: backdrop-blur wash plus four expanding Cherenkov
ripples; reduced-motion still enters instantly.
- Mobile: corner nav buttons become icon-only (Portfolio/Apps/Services/About)
with aria-labels; subheader hidden on mobile so nothing overlaps the logo.
- More ambient motion: logo float plus a second counter-rotating glow ring.
- Clicking the logo (or Explore) triggers a Cherenkov wave (wash + expanding
rings) before entering; reduced-motion enters instantly.
- Beveled glass buttons.
- Splash: full-viewport animated SUCH logo cover with four corner nav
buttons; reveals the site once "entered" (scroll-through or Explore) and
does not resurrect on scroll-up. Original hero restored beneath it
(title-anchored monitor pulse + Cherenkov-blue background).
- Logo: traveling Cherenkov glow ring + gentle letter breath (SVG/Framer,
reduced-motion safe); beveled glass corner buttons.
- Services: new "Nuclear & Radiation Modeling" section + "Consulting"
umbrella (payments, crypto, nuclear), MCNP Google Scholar link, and a
not-a-licensed-PE clarification.
- About/founder bio: LLNL credibility + accessibility, art101.io link.
- Products: add Barns & Neutrons, Alpha Doc, Decay Theory (detail pages,
OG cards, SVG icons); client-side sortable grid with coming-soon at the
bottom. New Neroswap + Custom Websites icons (de-duplicated).
- SEO: game pages added to sitemap.
- Copy: dial back em-dashes to colons/commas/parens on home + services.