/* world.css — scroll-scrub stage (pinned sequence, no three.js), then a normal
   paper sheet. Paper/black-line only, no boxed panel.
   Home page is one grey surface (owner direction, 2026-09-23): --paper is
   set to the same very light neutral grey as theme-v2.css (#F7F7F7) here —
   this file only loads on index.html, kept as its own copy of the token
   rather than relying on cascade order with theme-v2.css. */
:root{
  --paper:#F7F7F7;
  --black:#0B0B0A;
  --grey-1:#6E6C66;
  --grey-2:#9C9A93;
  --face-display:'Inter', sans-serif;
  --face:'Roboto', sans-serif;
  --fs-display: clamp(56px, 9.4vw, 148px);
}
*{box-sizing:border-box;}
html,body{
  margin:0; padding:0;
  background:var(--paper);
  color:var(--black);
  font-family:var(--face);
  /* overflow-x:clip, NOT hidden — hidden forces overflow-y:auto per spec, which
     turns body into its own scroll container and silently breaks every
     position:sticky descendant (the whole #world-viewport pin mechanism). clip
     achieves the same "no horizontal scrollbar" result without that side effect. */
  overflow-x:clip;
}

/* ---------- scrub stage: tall block, sticky inner viewport ---------- */
#world-stage{
  /* belt-and-suspenders: --green-ink/--green-deep/--accent are already set
     on :root by theme-v2.css (loaded before this file) and merge cleanly
     with this file's own :root block above, so this was never actually
     unresolved in testing — but green3's brief calls for the token to be
     defined inside #world-stage's own scope explicitly, so it can never go
     missing here even if a future edit reorders/removes the theme import
     on index.html. */
  --green-deep:#085041;
  --green-ink:var(--green-deep);
  --accent:var(--green-ink);
  position:relative;
  /* dark room fix (2026-09-27): this element's own --green-ink/--accent
     above are a fixed light-room value ("belt-and-suspenders" note above),
     which — since a locally-declared custom property always wins over an
     inherited one, regardless of selector specificity — silently shadowed
     theme-v2.css's html[data-room="dark"] swap for every descendant that
     reads var(--green-ink)/var(--accent) (.hero-cta border+arrow, the
     beat-num active label, .learn-cta border). They stayed stuck on
     --green-deep (made for a paper background) even in the dark room,
     reading as a dim, low-contrast green on black. Re-declare it here so
     descendants of #world-stage get the same swap the rest of the page
     gets. */
  /* ~12.5 screens: was 1000vh (0-1 old), extended 25% for the 04 "get in
     touch" beat. G.wtHold..G.vuHold fractions in viz.js are UNCHANGED
     numbers, so everything up to the old vuHold start (0.72) still plays out
     at exactly the same relative pace, just proportionally slower/longer —
     the old end-of-scroll (progress 1.0) now lands at ~0.80 of this taller
     stage, inside the extended vuHold — the VU settles back a little
     (G.settle) then holds calm through the "get in touch." beat. */
  /* rew lane: 1430vh — the forward 1250vh (wtHold..desk03Hold) plays at
     exactly the same absolute scroll length as before, plus a reverse leg
     back to the opening (rev2/scopeHoldRev/rev1/finalWtHold, see the G
     table + virtualP in viz.js for the vh breakdown). */
  height:1175vh; /* fix-story 2026-10-21 (was 1300vh): must equal STAGE_VH in viz.js (see the G table there) */
  background:var(--paper); /* pure white stage (owner direction) */
}
html[data-room="dark"] #world-stage{ --green-ink:var(--accent-text); }
#world-viewport{
  position:sticky;
  top:0;
  /* mobile viewport-cutoff fix (2026-09-28): 100vh on mobile Safari/Chrome
     resolves to the LARGE viewport (toolbars collapsed), which is taller
     than what's actually visible when the address bar / tab bar are showing
     (true on first load and most scroll positions). Since the hero text
     lives inside this sticky box (.beat-hero fills it, top:0;bottom:0), that
     mismatch pushed the subline/CTA below the real fold. svh (SMALL
     viewport — toolbars assumed visible) guarantees the box never claims
     more height than is actually on screen; dvh keeps it in sync as the
     toolbar animates. vh stays first as the fallback for browsers that
     don't support the newer units — later declarations win where supported,
     so the safest unit (svh) applies last. Desktop is unaffected (vh/svh/dvh
     are identical with no toolbar to hide). */
  height:100vh;
  height:100dvh;
  height:100svh;
  overflow:hidden;
  background:var(--paper);
}
#world-canvas{
  position:absolute; inset:0;
  width:100%; height:100%;
  display:block;
  z-index:1;
  background:var(--paper);
}

/* nav — the real site nav (theme-v2.css .nav / .nav-links / .nav-panel), solid
   paper bar with a hairline below, loaded unchanged; no world-specific override. */

/* text overlays — always float over the scene, never a solid column. This
   stage is white (media), text is ink-black, no glow. */
.beat{
  position:absolute;
  left:0; right:0;
  z-index:10;
  padding:0 var(--page-x);
  pointer-events:none;
  opacity:0;
  visibility:hidden;
  transition:opacity .3s ease-out;
}
/* 2026-09-23 stage9 fix: a beat that isn't the current one must be
   genuinely out of the paint tree, not just opacity:0 — see the
   revealBeat()/hide() comment in viz.js (a faded label ghost behind the VU
   at scroll ~0.75 was two labels' opacity states disagreeing with which one
   was "current"). visibility only flips once the beat's own fade-out tween
   completes (JS removes .is-in in onComplete), so the fade itself still
   plays — this only guarantees the settled end-state can never be a ghost. */
.beat.is-in{ visibility:visible; }

/* mask-wrapper: overflow:hidden clips glyph ink outside the line box, so pad
   the box out and pull it back with a negative margin to keep the surrounding
   spacing exactly where it was (same pattern as components.css .split-line). */
.beat .mask{
  display:block;
  overflow:hidden;
  padding:.12em 0;
  margin:-.12em 0;
}
.beat .mask > span,
.beat .mask > a{
  display:block;
  transform:translateY(115%);
  transition:transform .7s cubic-bezier(.16,1,.3,1);
}
.beat.is-in .mask > span,
.beat.is-in .mask > a{ transform:translateY(0); }

/* hero — hero-scale name floating over the scene */
.beat-hero{
  top:0; bottom:0;
  display:flex; flex-direction:column; justify-content:center; align-items:flex-start;
  opacity:1; /* visible at rest, scroll fades it out — never starts blank */
}
/* fix-story (2026-10-21): viz.js fades the hero TEXT (--hero-text-op) and
   its clean-zone scrim (--hero-scrim-op) separately, so the scrim stays
   solid while the name fades and the wave can never draw through it. */
.beat-hero > *{ opacity:var(--hero-text-op, 1); }
.beat-hero::before{ opacity:var(--hero-scrim-op, 1); }
.beat-hero h1{
  font-family:var(--face-display);
  font-weight:700;
  letter-spacing:-.04em;
  font-size:var(--fs-display);
  line-height:.92;
  margin:0 0 .18em;
  color:var(--black);
}
.beat-hero p{
  font-size:clamp(16px,1.3vw,18px);
  color:var(--grey-1);
  margin:0;
  max-width:32em;
}
.beat-hero .hero-cta{
  pointer-events:auto; /* parent .beat is pointer-events:none — opt this control back in */
  display:inline-flex; align-items:center; gap:.5em;
  margin-top:clamp(20px,2.6vw,30px);
  padding:clamp(13px,1.5vw,17px) clamp(26px,2.8vw,34px);
  font-family:var(--face); font-weight:600; font-size:clamp(14px,1.05vw,16px);
  letter-spacing:0; text-transform:lowercase; text-decoration:none;
  color:var(--black); background:transparent;
  border:1px solid var(--green-ink, #085041);
  border-radius:2px;
  transition:background .2s ease-out, color .2s ease-out, border-color .2s ease-out;
}
.beat-hero .hero-cta .arr{color:var(--green-ink, #085041); transition:color .2s ease-out, transform .35s cubic-bezier(.22,1,.36,1);}
.beat-hero .hero-cta:hover, .beat-hero .hero-cta:focus-visible{background:none; color:var(--green-ink, #085041); border-color:var(--green-ink, #085041);}
.beat-hero .hero-cta:hover .arr{transform:translateX(5px);}
.beat-hero .hero-cta:focus-visible{outline:2px solid var(--green-light); outline-offset:2px;}

/* qa-fix C (2026-09-28, QA: on desktop the wave's leftmost strands crossed
   the end of the subline and the right half of "lewis"). A soft paper
   clean-zone sits behind the hero text block only — the canvas keeps
   drawing, the strands simply fade out before they reach the type. The
   approved wave itself is untouched. Phones use the bottom text-zone scrim
   in the max-width:700px block below instead (this rule is overridden
   there). */
.beat-hero::before{
  content:""; position:absolute; z-index:-1; pointer-events:none; inset:0;
  background:radial-gradient(ellipse 34% 30% at 27% 50%, var(--paper) 0, var(--paper) 58%, rgba(247,247,247,0) 100%);
}
html[data-room="dark"] .beat-hero::before{
  background:radial-gradient(ellipse 34% 30% at 27% 50%, var(--paper) 0, var(--paper) 58%, rgba(0,0,0,0) 100%);
}

/* numbered beats */
.beat-num{
  top:50%; transform:translateY(-50%);
}
.beat-num .lbl{
  display:block;
  font-family:var(--face);
  font-size:clamp(12px,.95vw,13px);
  letter-spacing:.12em;
  text-transform:uppercase;
  color:var(--green-ink, #085041);
  padding:.35em 0;
  margin:-.35em 0 .25em;
  overflow:hidden;
}
.beat-num .lbl span{
  display:inline-block; transform:translateY(110%);
  transition:transform .7s cubic-bezier(.16,1,.3,1);
}
.beat-num h2{
  font-family:var(--face-display);
  font-weight:700;
  letter-spacing:-.04em;
  font-size:clamp(40px,7vw,104px);
  line-height:.95;
  margin:0 0 .22em;
  color:var(--black);
  /* line-height .95 is tighter than the glyphs' own ascender/descender ink —
     without this, "production"/"mastering" get their tops sheared off by the
     overflow:hidden mask. Pad the box, pull it back with negative margin. */
  padding:.12em 0;
  margin-top:-.12em;
  margin-bottom:calc(.22em - .12em);
  overflow:hidden;
}
.beat-num h2 span{
  display:block; transform:translateY(115%);
  transition:transform .7s cubic-bezier(.16,1,.3,1) .04s;
}
.beat-num p{
  font-size:clamp(16px,1.3vw,18px);
  /* qa-fix C: service copy is a real paragraph — ink, not muted grey
     (BRAND.md: --grey-1 is for meta/helper lines) — and capped so it keeps
     a >=48px gutter to the scope/desk graphic on the right (the graphic's
     left edge sits at ~38-40% of the frame on landscape viewports). */
  color:var(--black);
  max-width:min(28em, calc(38vw - var(--page-x) - 48px));
  text-wrap:pretty;
  margin:0;
  padding:.15em 0;
  margin-top:-.15em;
  margin-bottom:-.15em;
  overflow:hidden;
}
.beat-num p span{
  display:block; transform:translateY(120%);
  transition:transform .7s cubic-bezier(.16,1,.3,1) .08s;
}
/* second sentence block (was a bare <br>) gets real paragraph spacing */
.beat-num p span + span{ margin-top:.65em; transition-delay:.12s; }

/* per-beat "learn more" link — smaller sibling of .beat-hero .hero-cta,
   same hairline-box CTA, dialed down since it sits under body copy rather
   than the hero name. All three .beat-num blocks share the exact same
   absolute position (top:50%/translateY(-50%)) — only one is opaque at a
   time, but opacity:0 does not stop hit-testing, so an unconditional
   pointer-events:auto here would let a not-yet-revealed beat's button
   (painted later in DOM order, so on top) silently steal clicks/hover
   from the visible one. Gate pointer-events on .is-in instead, same
   condition the reveal itself uses, so only the on-screen button is ever
   interactive. */
.beat-num .learn-cta{
  pointer-events:none;
  display:inline-flex; align-items:center; gap:.5em;
  margin-top:clamp(14px,1.8vw,20px);
  padding:clamp(9px,1vw,12px) clamp(18px,2vw,24px);
  font-family:var(--face); font-weight:600; font-size:clamp(12px,.9vw,13px);
  letter-spacing:0; text-transform:lowercase; text-decoration:none;
  color:var(--black); background:transparent;
  border:1.5px solid var(--green-ink, #085041);
  border-radius:2px;
  transition:background .2s ease-out, color .2s ease-out, border-color .2s ease-out;
}
.beat-num.is-in .learn-cta{ pointer-events:auto; }
.beat-num .learn-cta .arr{transition:transform .35s cubic-bezier(.22,1,.36,1);}
.beat-num .learn-cta:hover, .beat-num .learn-cta:focus-visible{background:var(--black); border-color:var(--black); color:var(--paper);}
.beat-num .learn-cta:hover .arr{transform:translateX(5px);}
/* mobile tap-target fix (QA 2026-09-23): measured 39px tall at 390px width —
   below the 44px minimum. Padding-only bump, font-size untouched. */
@media (max-width:760px){
  .beat-num .learn-cta{ padding-top:16px; padding-bottom:16px; }
}

/* reveal state — entry-triggered class toggle (see scrub.js revealBeat), not a
   scroll-scrubbed tween: the spans just transition to 0 over a fixed 0.7s. */
.beat.is-in{ opacity:1; }
.beat-num.is-in .lbl span,
.beat-num.is-in h2 span,
.beat-num.is-in p span{
  transform:translateY(0);
}
/* active beat number: while its beat is on screen, the small uppercase
   label (01/02/03) goes green-ink — every other section label on the
   site stays grey by default (2026-09-23 owner accent system). */
.beat-num.is-in .lbl{color:var(--accent);}

/* beat 04 — "get in touch." — the closing beat: the VU meter + system
   diagram, pulled back and drifting calmly (no synth panel any more —
   removed 2026-09-23), left column so the diagram (right ~60% of the frame,
   see viz.js CAM.settle + SYS_*_X) reads clearly beside it. Email sits
   underneath, underlined, normal weight.
   2026-09-23 stage9 fix (owner: the wavetable/cable/VU diagram ran straight
   through this heading — a mini scope box sat on "touch.", a stray vertical
   .beat-04-rule hairline stood at the frame's right edge for no reason once
   the synth panel it used to frame was removed). Fixes: the rule div/rule
   below is deleted outright, and the heading gets its own smaller display
   size (was var(--fs-display), same as the hero name, right edge ~52% of a
   1760px frame) — clamp(40px,5.6vw,92px) keeps its right edge comfortably
   inside the left ~35% of the frame so it can never reach the diagram's own
   >=40% zone, at any viewport width. */
.beat-04{
  top:0; bottom:0;
  display:flex; flex-direction:column; justify-content:center; align-items:flex-start;
}
.beat-04 h2{
  font-family:var(--face-display);
  font-weight:700;
  letter-spacing:-.04em;
  font-size:clamp(40px, 5.6vw, 92px);
  line-height:.92;
  color:var(--black);
}
/* h2 IS the .mask element here (<h2 class="mask">) — .beat .mask (two
   classes) outranks a plain `.beat-04 h2` (one class + one type), so its
   own padding/margin:.12em/-.12em silently won every fight for this box,
   leaving ~zero real gap below the display-size heading and clipping the
   "g"/"t" descender flush against the email link (owner-flagged overlap).
   `.beat-04 h2.mask` (two classes + one type) beats `.beat .mask` outright,
   so this rule is the one that actually lands: bigger padding-bottom keeps
   the descender ink inside the overflow:hidden clip box, and the explicit
   margin-bottom is the real visible gap above the link — same pattern as
   .hero-one-name .split-line's padding-bottom/negative-margin fix. */
.beat-04 h2.mask{
  padding-bottom:.3em;
  margin-bottom:.22em;
}
.beat-04 p{ margin:0; }
.beat-04 p a{
  pointer-events:auto; /* parent .beat is pointer-events:none */
  display:inline-block;
  font-family:var(--face); font-weight:600;
  font-size:clamp(17px,1.6vw,22px);
  color:var(--black);
  text-decoration:underline;
  text-decoration-color:var(--green-ink, #085041);
  text-decoration-thickness:1.5px;
  text-underline-offset:.28em;
}
/* ---------- hand-off hairline: the stage settles into this line ---------- */
.sheet-hairline{
  height:1px;
  background:var(--grey-2);
  opacity:1;
  margin:0;
}

/* no-js / reduced-motion static fallback */
.static-fallback{display:none;}
.reduced-motion #world-stage, .no-js #world-stage{height:auto;}
.reduced-motion #world-viewport, .no-js #world-viewport{position:relative; height:auto; background:var(--paper);}
.reduced-motion .static-fallback, .no-js .static-fallback{
  display:block; position:relative; z-index:5; background:var(--paper);
}
.reduced-motion #world-canvas, .no-js #world-canvas,
.reduced-motion .beat, .no-js .beat{display:none;}

.static-fallback{padding:120px clamp(24px,6vw,96px) 80px;}
.static-fallback h1, .static-fallback .static-h1{font-family:var(--face-display); font-weight:700; letter-spacing:-.04em; font-size:clamp(48px,9vw,120px); line-height:.92; margin:0 0 .2em;}
.static-fallback p.tagline{font-size:clamp(16px,1.3vw,18px); color:var(--grey-1); max-width:32em; margin:0 0 1.6em;}
.static-fallback .static-cta{
  display:inline-flex; align-items:center; gap:.5em;
  margin:0 0 4em;
  padding:13px 26px;
  font-family:var(--face); font-weight:600; font-size:15px; text-decoration:none;
  color:var(--black); border:1px solid var(--black); border-radius:2px;
}
.static-block{margin:0 0 4em; max-width:36em;}
.static-block .lbl{font-family:var(--face); font-size:13px; letter-spacing:.12em; text-transform:uppercase; color:var(--green-ink, #085041);}
.static-block h2{font-family:var(--face-display); font-weight:700; letter-spacing:-.04em; font-size:clamp(32px,5vw,56px); margin:.2em 0;}
.static-block p{font-size:16px; color:var(--grey-1);}

/* qa-fix C (2026-09-28): the phone layout also applies to portrait
   tablets / tall windows (aspect < 0.9) — viz.js already switches those to
   its narrow-aspect camera framings (scope/desk fit above a bottom text
   band), so the desktop left-column copy ran straight through the
   graphics at 768x1024. Same aspect threshold as viz.js's own. */
@media (max-width:700px), (max-aspect-ratio:9/10){
  /* svh/dvh pass (2026-09-28, herofit): every vh-based vertical anchor below
     is tuned against the true visible viewport, not the "large viewport"
     100vh can silently expand to on mobile Safari/Chrome when the toolbar
     is showing. Pairing each with the matching svh/dvh value keeps these
     bottom/top offsets proportional to #world-viewport's own (now
     svh-sized, see above) box instead of drifting past it — same
     fallback-then-override order as #world-viewport. */
  /* qa-fix C (2026-09-28): every numbered beat now sits in ONE bottom
     "text zone" on phones (the old 30svh/9svh split existed only to stop
     crossfading beats overlapping — viz.js now spaces the beats in time so
     no two are ever on screen together). A soft paper scrim (::before)
     sits between the canvas and the copy, so whatever the line art is
     doing behind it the text always reads on a clean ground. */
  .beat-num{top:auto; bottom:calc(7vh + env(safe-area-inset-bottom)); bottom:calc(7svh + env(safe-area-inset-bottom)); transform:none;}
  .beat-num::before,
  .beat-hero::before{
    content:""; position:absolute; z-index:-1; pointer-events:none;
    left:0; right:0; top:-56px; bottom:calc(-7svh - env(safe-area-inset-bottom) - 2px);
    background:linear-gradient(to bottom, rgba(247,247,247,0) 0, var(--paper) 56px);
  }
  html[data-room="dark"] .beat-num::before,
  html[data-room="dark"] .beat-hero::before{
    background:linear-gradient(to bottom, rgba(0,0,0,0) 0, var(--paper) 56px);
  }
  /* qa-fix C (2026-09-28, QA: the wave ran straight through "jacob lewis"
     and the subline on phones). The hero block now anchors to the bottom
     of the frame (same text zone as the numbered beats) and viz.js lifts
     the wavetable into the upper part of the frame (MOBILE_HERO_YFRAC), so
     the two no longer share a band. */
  .beat-hero{align-items:flex-start; justify-content:flex-end; padding-top:0; padding-bottom:calc(9vh + env(safe-area-inset-bottom)); padding-bottom:calc(9svh + env(safe-area-inset-bottom));}
  .beat-hero::before{inset:auto 0 0 0; height:calc(100% - 56svh);}
  .beat-num p{max-width:28em;}
  /* mobile audit 2026-09-27: --fs-display's clamp(56px,9.4vw,148px) floor
     doesn't clear 56px until ~595px wide, so across the whole 360-430px
     mobile-audit range the hero name renders at a flat, non-scaling 56px
     — the opposite of "a type scale that feels intentional." Since the
     vertical anchor is locked (see above), use the safe lever instead:
     let the name itself carry a little more visual weight so the block
     reads as a considered hero rather than an undersized line floating
     in a lot of grey, and let it respond (mildly) to viewport width
     again. Height grows ~11px at the low end, ~5.5px each side of the
     centred block — well inside the clearance margin verified above. */
  .beat-hero h1{font-size:clamp(60px, 15vw, 84px);}
  /* fix-story (2026-10-21, QA at 600-1024 portrait: the 84px cap left the
     name, line and CTA a small block in the corner of a tablet-wide frame).
     Past phone width the hero type keeps scaling with the frame. */
  @media (min-width:600px){
    .beat-hero h1{font-size:clamp(84px, 12.5vw, 136px);}
    .beat-hero p{font-size:clamp(17px, 2.3vw, 22px); max-width:30em;}
    .beat-hero .hero-cta{font-size:clamp(15px, 1.9vw, 18px); padding:clamp(15px,2vw,19px) clamp(28px,3.6vw,38px);}
    .beat-hero::before{height:calc(100% - 56svh + 64px); background:linear-gradient(to bottom, rgba(247,247,247,0) 0, var(--paper) 120px);}
    html[data-room="dark"] .beat-hero::before{background:linear-gradient(to bottom, rgba(0,0,0,0) 0, var(--paper) 120px);}
  }
  /* 2026-09-23 stage9 fix: was padding-top:18vh (same as hero), which put
     the heading right under the system diagram's mini wavetable/scope —
     bumped clear so the diagram lives entirely in its own band above,
     never touching the heading (see viz.js CAM.settle vuMobileAdjust for
     the matching camera-side headroom pass). */
  .beat-04{align-items:flex-start; padding-top:46vh; padding-top:46dvh; padding-top:46svh;}
  /* fix-story (2026-10-21, QA: the closing wave strands drew straight
     through "get in touch." and the email link on phones). viz.js now lifts
     the closing stack clear into the upper half (MOBILE_END_YFRAC); this
     scrim is the guarantee — a paper ground from just above the heading to
     the frame's bottom, same treatment as the numbered beats' text zone. */
  .beat-04::before{
    content:""; position:absolute; z-index:-1; pointer-events:none;
    /* the copy is centred in the 46svh..100svh band, so its top sits near
       73svh minus half its own height; start the fade just above that, so
       the scope/wave above (rev1) is never veiled. */
    left:0; right:0; bottom:0; top:calc(73svh - 150px);
    background:linear-gradient(to bottom, rgba(247,247,247,0) 0, var(--paper) 64px);
  }
  html[data-room="dark"] .beat-04::before{
    background:linear-gradient(to bottom, rgba(0,0,0,0) 0, var(--paper) 64px);
  }
  #world-stage{height:1351vh;} /* fix-story 2026-10-21: STAGE_VH 1175vh * 1.15 (was 1495vh) — phone flicks travel further per gesture; qa-fix C cut the old 1.3 stretch that left dead screens */
}

/* ---------- home page is one white surface (owner direction, 2026-09-23) ----------
   theme-v2.css's .footer is a black surface site-wide (by design, everywhere
   else); this file loads only on index.html, so scoping the flip to .footer
   here keeps every other page's black footer untouched. Text/icon colors that
   theme-v2.css keys off var(--on-dark-1/2) (built for a black background)
   need their own flip too, since --paper alone doesn't cover them. */
.footer{background:var(--paper); color:var(--black);}
.footer-meta span, .footer-meta a{color:var(--grey-1);}
.footer-meta a:hover, .footer-meta a:focus-visible{color:var(--accent);}
.footer-social a{color:var(--black);}

/* dark room (room.js): theme-v2.css re-pins .footer back to its normal
   fixed-black-surface tokens everywhere else on the site, but THIS
   page's footer is deliberately not that surface (it's the same "one
   page surface" as the rest of index.html, see above) — cancel that
   re-pin here so it keeps following the swapped page tokens instead.
   Loaded after theme-v2.css and same selector specificity, so this
   wins on index.html only. */
html[data-room="dark"] .footer{
  --paper: #121211; /* soft black (2026-10-28), matches the rest of this page's dark-room ground */
  --black: #F7F7F7;
  --grey-1: rgba(247,247,247,.62);
  --grey-2: rgba(247,247,247,.32);
  --hairline: rgba(247,247,247,.14);
}
.footer-word{color:var(--grey-2);}

/* ---------- qa-fix C (2026-09-28): sheet below the stage ----------
   Work cards: a title that wraps to two lines ("say my name (swaggaz
   edit)") pushed that card's player ~11px lower than its neighbours.
   Reserve two title lines on every card so the players share one line,
   and sit the time on the tag line instead of floating mid-block. Same
   rules as /work/'s page-scoped block. */
#work .work-tile-card{align-items:flex-start;}
#work .work-tile-card .work-meta .v{min-height:2.6em;} /* 2 lines @ line-height 1.3 */
#work .work-tile-card .work-tail{align-self:end;}
@media (max-width:599px){
  #work .work-tile-card .work-meta .v{min-height:0;} /* single column: nothing to align against */
}
/* Testimonials: a lone card no longer sits in half of a two-column grid
   with the other half empty — it spans the row as a feature quote. */
@media (min-width:900px){
  #words .c-cards .c-card:only-child{
    grid-column:1 / -1;
    display:grid; grid-template-columns:minmax(0,1fr) minmax(0,2.2fr);
    grid-template-areas:"stars quote" "attr quote"; grid-template-rows:auto 1fr;
    column-gap:var(--r6); row-gap:var(--r2); align-content:start;
  }
  #words .c-cards .c-card:only-child .c-card-stars{grid-area:stars;}
  #words .c-cards .c-card:only-child .c-card-attr{grid-area:attr; align-self:start;}
  #words .c-cards .c-card:only-child .c-card-quote{
    grid-area:quote; font-size:clamp(22px, 2.2vw, 30px); line-height:1.4; max-width:34ch;
  }
}

/* ---------- short-wide windows (robust pass, 2026-09-28) ----------
   The hero block is centred in the full-height sticky viewport, which is
   right on a normal screen — but on a short window (1440x400, 800x300: a
   laptop with the dev tools open, a landscape phone) the centred name rose
   under the fixed nav bar. Here only: centre it in the space BELOW the nav,
   and let the display type follow the viewport height as well as width so
   name + line + CTA always fit. Beat headings get the same height cap.
   Normal heights (>620px) are untouched. */
@media (max-height:620px) and (min-width:701px){
  .beat-hero{padding-top:calc(var(--nav-h) + 8px); padding-bottom:8px;}
  .beat-hero h1{font-size:min(var(--fs-display), 24vh); font-size:min(var(--fs-display), 24svh);}
  /* numbered beats fill the band under the nav and centre in it — but
     'safe' centring: if the copy is taller than the band it starts at the
     top (label always visible) instead of spilling up under the nav. */
  .beat-num{
    top:var(--nav-h); bottom:0; transform:none;
    display:flex; flex-direction:column; align-items:flex-start;
    justify-content:safe center;
    padding-top:8px; padding-bottom:8px;
  }
  .beat-num > *{flex-shrink:0;}
  .beat-num p{font-size:15px; line-height:1.45;}
  .beat-num h2{font-size:min(clamp(40px,7vw,104px), 17vh); font-size:min(clamp(40px,7vw,104px), 17svh);}
}
@media (max-height:620px) and (max-width:700px){
  .beat-hero{padding-top:calc(var(--nav-h-mobile) + 8px);}
  .beat-hero h1{font-size:min(clamp(60px, 15vw, 84px), 20vh); font-size:min(clamp(60px, 15vw, 84px), 20svh);}
}
/* very short desktop windows (<=380px tall): tighten the beat copy so the
   numbered label clears the nav too. */
@media (max-height:380px) and (min-width:701px){
  .beat-num h2{font-size:min(clamp(36px,7vw,104px), 12vh); font-size:min(clamp(36px,7vw,104px), 12svh);}
  .beat-num{padding-top:4px; padding-bottom:4px;}
  .beat-num p{font-size:13px; line-height:1.35;}
  .beat-num p span + span{margin-top:.4em;}
  .beat-num .learn-cta{margin-top:6px; padding:7px 16px;}
}
