/* Shared design tokens for the case-study pages (apollo, bearings, cluster, strata,
   ucdavis). Extracted 2026-08-03 from five byte-identical copies of this exact block --
   see docs/superpowers/specs/2026-08-02-design-tokens.md for what each token is and why.

   This file is tokens only: colour ramp, the interactive --accent family, --sans, the
   --fs-* scale, the --sp-* ladder, the --lh-* values, the radius/motion/material scale,
   tracking/heading-gap tokens. Component CSS (the rail, buttons, cards, everything that
   *consumes* these values) stays in each page's own <style> block -- this is not a
   stylesheet merge.

   --accent means the interactive signal only (link/back-link hover, the primary
   button's ground, focus rings). SINCE 2026-08-04 IT IS INK, NOT A HUE -- see the
   block at the definition below. It is unrelated to the tournament's live-match
   green (--live-deep/--live-bright in index.html/play.css), which this file does
   not define and never has a consumer for.

   Wired as of 2026-08-03: the five case studies, plus play.html, headmaker.html and
   gradientlab.html. index.html is the last page still carrying its own copy -- that
   pass lands after the concurrent hero-engine extraction finishes there.

   THERE IS NO SANCTIONED RE-BINDING OF --accent ANY MORE. play.css used to rebind it
   to the live-match green and gradientlab.html to the same green; both are gone, and
   both are why the name meant two different colours on one site. One token, one
   meaning. If you are about to write `--accent:` outside this file, don't. */

/* ===========================================================================
   THE TYPEFACE ITSELF, and it lives here for one reason: PER-PAGE COPIES ARE
   HOW THIS KEPT BREAKING.  2026-08-04.

   --sans has always been declared in this file, but the @font-face rules that
   make "Instrument Sans" resolvable were copied into each page's own <style>.
   Two pages never got the copy, and on both of them --sans silently fell
   through to the next name in the stack, -apple-system -- SF Pro on a Mac.
   Measured with document.fonts.check(), not by reading declarations:

     page          @font-face  preload   ACTUALLY RENDERED
     about.html    0           0         SF Pro       <- bug
     play.html     0           2         SF Pro       <- bug (preloaded a font
                                                          it then never declared,
                                                          which is why the
                                                          Network tab looked fine)
     the other 8   2           2         Instrument Sans

   play.html had already been reported fixed once and was not, so the fix is a
   SHARED MECHANISM rather than a ninth and tenth copy: every page in the site
   links tokens.css, and links it BEFORE its own <style>, so declaring the faces
   here reaches all ten and cannot be forgotten by page eleven. The six per-page
   copies are deleted with this pass, not left as duplicates.

   Relative to THIS file, not to the document -- @font-face src URLs resolve
   against the stylesheet, and tokens.css and fonts/ are both at the root, so
   the path is unchanged from the copies it replaces.

   TWO WEIGHTS, 400 AND 600, and no third. That is the type system's rule, and
   the 500/700 woff2 files in fonts/ are deliberately not declared -- an
   undeclared weight synthesises rather than silently loading a face nobody
   approved. "Instrument Sans Var" was declared on six pages and consumed on
   NONE of them (only headmaker.html and specimen.html use that family, and both
   declare it themselves); those six dead declarations go with this pass.
   Archivo is NOT here: it is the tournament's numeral face, wanted by exactly
   two pages, and it stays declared where it is used. */
@font-face{font-family:"Instrument Sans";font-style:normal;font-weight:400;font-display:swap;
  src:url(fonts/instrument-sans-latin-400-normal.woff2) format("woff2")}
@font-face{font-family:"Instrument Sans";font-style:normal;font-weight:600;font-display:swap;
  src:url(fonts/instrument-sans-latin-600-normal.woff2) format("woff2")}

:root{--fs-statnum:clamp(34px,3.6vw,48px);--fs-pagehead-sm:clamp(30px,8.2vw,40px);--sp-8-16:16px;--sp-16-24:24px;--sp-16-32:32px;--sp-16-40:40px;--sp-16-56:56px;--sp-24-32:32px;--sp-24-40:40px;--sp-24-48:48px;--sp-32-48:48px;--sp-32-56:56px;--sp-32-64:64px;--sp-40-64:64px;--sp-40-72:72px;--sp-48-64:64px;--sp-48-72:72px;--sp-48-80:80px;--sp-56-96:96px;--sp-56-112:112px;--sp-64-168:168px;--sp-72-144:144px;--sp-80-152:152px;--c50:#FDFDFD;--c100:#E6E6E6;--c75:#F1F1F1;--c500:#757575;--c600:#6B6B6B;--c700:#525252;--c900:#212121;--c950:#121212;--yellow:#FED259;--accent:#121212;--accent-press:#212121;--accent-wash:#F1F1F1;--accent-dark:#FDFDFD;
 --sans:"Instrument Sans",-apple-system,system-ui,sans-serif;--fs-display:clamp(68px,12vw,168px);--fs-h1:clamp(28px,2.6vw,38px);--fs-pagehead:clamp(33px,3.9vw,52px);--fs-h2:clamp(26px,2.5vw,36px);--fs-h3:clamp(21px,2.4vw,32px);--fs-h4:clamp(19px,2.1vw,28px);--fs-h5:clamp(20px,1.7vw,24px);--fs-lead:clamp(15px,1.4vw,18px);--fs-prose:clamp(17px,1.39vw,20px);--fs-body:16px;--fs-small:15px;--fs-tab:clamp(17px,1.5vw,20px);--fs-label:13px;--fs-caption:12px;--fs-micro:11px;--lh-display:1.0;--lh-tight:1.08;--lh-body:1.5;--lh-prose:1.5;--lh-flat:1;--rail-w:clamp(85px,7vw,110px);--rail-gutter:var(--sp-24-40);--rail-gap:var(--sp-40-72);--r-hair:2px;--r-xs:6px;--r-sm:10px;--r-md:14px;--r-lg:20px;--r-xl:28px;--r-pill:999px;--r-full:50%;--corner:squircle;--ease-out:cubic-bezier(.2,.8,.2,1);--ease-out-dur:160ms;--sp-quick:linear(0, 0.0757, 0.2282, 0.3918, 0.5383, 0.6586, 0.7524, 0.8231, 0.8751, 0.9126, 0.9394, 0.9582, 0.9714, 0.9805, 0.9867, 0.991, 0.994, 0.9959, 0.9973, 1);--sp-quick-dur:300ms;--sp-settle:linear(0, 0.024, 0.0833, 0.1633, 0.2531, 0.3454, 0.4352, 0.5193, 0.5959, 0.6642, 0.724, 0.7755, 0.8194, 0.8562, 0.8867, 0.9118, 0.9322, 0.9486, 0.9616, 0.9719, 0.9799, 0.986, 0.9907, 0.9941, 0.9967, 1);--sp-settle-dur:360ms;--sp-pop:linear(0, 0.0435, 0.1463, 0.2761, 0.4118, 0.5401, 0.6536, 0.7493, 0.8265, 0.8866, 0.9316, 0.964, 0.9863, 1.0007, 1.0092, 1.0136, 1.0151, 1.0148, 1.0134, 1.0115, 1.0095, 1.0075, 1.0057, 1.0042, 1.0029, 1);--sp-pop-dur:640ms;--sp-bounce:linear(0, 0.0659, 0.222, 0.4163, 0.6109, 0.7819, 0.917, 1.0128, 1.072, 1.1007, 1.1065, 1.0972, 1.0794, 1.0584, 1.0379, 1.0203, 1.0066, 0.9972, 0.9915, 0.989, 0.9887, 0.9899, 0.9919, 0.9942, 0.9963, 1);--sp-bounce-dur:860ms;--sp-bounce-approx:cubic-bezier(.34,1.56,.64,1);--rim-1:inset 0 0 0 1px rgba(18,18,18,.08);--rim-2:inset 0 0 0 1px rgba(18,18,18,.14);--rim-top:inset 0 1px 0 rgba(255,255,255,.55);--mat-0:transparent;--mat-1:rgba(253,253,253,.72);--mat-2:rgba(253,253,253,.86);--mat-3:rgba(253,253,253,.96);--mat-scrim:rgba(18,18,18,.34);--blur-1:8px;--blur-2:14px;--blur-3:20px;--sh-1:0 1px 2px rgba(18,18,18,.05);--sh-2:0 2px 8px rgba(18,18,18,.06),0 1px 2px rgba(18,18,18,.04);--sh-3:0 8px 28px rgba(18,18,18,.10),0 2px 6px rgba(18,18,18,.05);--sh-hero:-12px 12px 48px rgba(18,18,18,.22);--lh-auto:clamp(0.95em, 0.91em + 10.25px, 1.6em);--tr-display:-.042em;--tr-hero:-.028em;--tr-head:-.020em;--tr-sub:-.015em;--tr-title:-.010em;--tr-body:-.004em;--tr-flat:0;--tr-caps:.045em;--tr-mark:-.008em;--gap-head-top:1.6em;--gap-head-bot:.42em;--gap-eyebrow:.55em;--gap-para:.75em;--pad-bar:5px;--gap-bar:2px;--tap-min:44px;}
@media(max-width:880px){:root{--sp-8-16:8px;--sp-16-24:16px;--sp-16-32:16px;--sp-16-40:16px;--sp-16-56:16px;--sp-24-32:24px;--sp-24-40:24px;--sp-24-48:24px;--sp-32-48:32px;--sp-32-56:32px;--sp-32-64:32px;--sp-40-64:40px;--sp-40-72:40px;--sp-48-64:48px;--sp-48-72:48px;--sp-48-80:48px;--sp-56-96:56px;--sp-56-112:56px;--sp-64-168:64px;--sp-72-144:72px;--sp-80-152:80px}}
@media(max-width:760px){:root{--blur-1:0px;--blur-2:0px}}
@media(prefers-reduced-motion:reduce){:root{--sp-quick-dur:1ms;--sp-settle-dur:1ms;--sp-pop-dur:1ms;--sp-bounce-dur:1ms}}
/* ===========================================================================
   THE ACCENT IS INK.  2026-08-04, Jayden: "I think we are removing the blue
   colour all together -- we already kinda had on hover and on everything and
   everywhere it's shown."

   THE INTENT, and it is the reason this is four literals rather than a deletion:
   emphasis is carried by WEIGHT AND CONTRAST, not by hue. That is the Apple /
   Stripe move, and on this site it has a second job -- THE ONLY COLOUR LEFT ON
   THE PAGE COMES FROM JAYDEN'S OWN WORK: the heads, the photographs, the
   gradients, the team kits. A brand hue competing with those is the thing being
   removed. The token KEEPS ITS NAME so the ~25 rules that already resolve
   `var(--accent)` change value without changing text; renaming it would ripple
   across eight pages and five concurrent agents for no gain.

   WHY --accent-press IS LIGHTER THAN --accent, which looks backwards and is not.
   --accent is #121212, the page's own ink; there is nothing darker to move to.
   Every consumer of the pair is either a DARK-FILLED control (.workCta,
   .abLinkPrimary, .pBtnGo) whose hover/press has to be visible against a
   near-black ground, or a text link going momentarily quiet on :active. Both
   want a lift, not a deepening -- the same direction Apple's filled buttons dim
   on touch and Stripe's dark buttons lighten on hover. #212121 (--c900) is a
   1.35:1 step off #121212: visible on a 200px button, invisible as a mistake.
   White ink on it is 15.3:1, so nothing that was legible stops being legible.

   THE OTHER TWO:
   --accent-wash is the site's existing light grey pill, --c75 #F1F1F1 -- the same
   value header.css uses for --nav-hover-bg, so an interaction ground is one grey
   everywhere instead of two. It is a GROUND, never a boundary (1.13:1 against
   paper) and never text; that warning survives the colour change unchanged.
   --accent-dark is the interactive ink ON A DARK GROUND and it is now simply the
   paper, --c50 #FDFDFD (18.4:1 on --c950). The old #7BA4F0 existed to keep a blue
   legible over the Play sky; with no blue there is nothing to rescue.

   FOCUS RINGS. This is the one place removing colour can genuinely hurt, so it
   was checked rather than assumed: every focus ring on the site already ships
   `2px solid var(--c950)` at a POSITIVE offset, and --accent's new value is the
   same #121212. Measured against the paper it goes 5.59:1 -> 18.42:1 -- the four
   rings that were written against var(--accent) (gradientlab x2, orbs, play's
   .teamChip) get MORE findable, not less. WCAG 1.4.11 wants 3:1; this clears it
   six times over. On a dark region a --c950 ring is invisible and must be
   var(--accent-dark), which is exactly what that token is now for.

   No @supports fallback is needed any more -- these are four sRGB hex literals.
   The oklch()/hex pair that stood here is deleted rather than commented out.
   Superseded: docs/superpowers/specs/2026-08-03-interactive-accent.md, whose
   §1 blue is no longer the shipped value. Its §2 (live-green vs interactive) and
   §3.2 (where the signal is NOT allowed) still hold and still govern.
   =========================================================================== */

/* ===========================================================================
   THE TWELVE GAPS, closed. Landed 2026-08-03 with the site header (header.css).
   Each one is a rung the scale above does not have -- see
   .superpowers/sdd/2026-08-02-play-page/header-report.md §10 for the measured
   case behind every number. Nothing here overrides anything above it.
   =========================================================================== */
:root{
 /* 1 - FIXED micro-spacing. The --sp-*-* ladder above is RESPONSIVE and floors
    at 16px, so there is no deterministic sub-16px inset for a bar. This sits
    below that ladder on the same 2px grid --gap-bar (2px) and --pad-bar (5px)
    already imply. Largest single source of raw pixels in the header. */
 --sp-2:2px;--sp-4:4px;--sp-6:6px;--sp-8:8px;--sp-10:10px;--sp-12:12px;
 --sp-14:14px;--sp-28:28px;--sp-30:30px;--sp-32:32px;

 /* 2 - line weights. --r-hair is a RADIUS; neither a hairline's width nor a
    focus ring's had a name, and both ship site-wide today as bare 1px / 2px. */
 --hair-w:1px;--focus-w:2px;

 /* 3 - icon sizes, plus 4 - the one stroke weight (was 1.7 / 1.8 / 2 across
    three icons for no reason; CSS owns the stroke, per the icons spec). */
 --ico-xs:11px;--ico-sm:15px;--ico-md:18px;--ico-stroke:1.8;

 /* 5 - type. 14px was missing between --fs-small (15) and --fs-label (13);
    the wordmark's size is a brand constant and deserves a name. */
 --fs-nav:14px;--fs-mark:17px;

 /* 6 - motion. --ease-out-dur (160ms) was the fastest rung and
    --sp-settle-dur (360ms) the slowest short one. A press needs faster; the
    hero availability capsule's shipped entrance needs slower. */
 --press-dur:100ms;--enter-dur:500ms;
 /* 7 - a press reads as the same nudge at every size only if the bigger
    object scales less. Two rungs cover the whole header. */
 --press-scale:.97;--press-scale-lg:.985;

 /* 8 - interaction washes. The materials ladder covers surfaces, not the
    hover/press tint that sits on top of one. */
 --wash-1:rgba(18,18,18,.055);--wash-i1:rgba(253,253,253,.10);

 /* 9 - the ink ramp, mirroring --c700 / --c100 on a dark ground.
    (--c50 #FDFDFD already serves as the strong ink, so it is reused.) */
 --i700:rgba(253,253,253,.78);--i100:rgba(253,253,253,.16);

 /* 10 - the ink materials ladder. There was none: --mat-1/2/3 are all
    rgba(253,253,253,.) and --mat-scrim (.34) is a scrim, not a material --
    white 14px text on it fails contrast. Tuned so white ink clears 4.5:1 over
    ANY ground including white, because gradientlab's stage colour is user-set;
    NOT mirrored arithmetically from the light rungs. */
 --mat-i1:rgba(18,18,18,.58);--mat-i2:rgba(18,18,18,.70);--mat-i3:rgba(18,18,18,.82);
 --mat-i3-solid:rgb(18,18,18);
 --rim-i1:inset 0 0 0 1px rgba(253,253,253,.14);
 --rim-i2:inset 0 0 0 1px rgba(253,253,253,.22);
 --sh-i2:0 8px 28px rgba(0,0,0,.28);--sh-i3:0 12px 36px rgba(0,0,0,.36);

 /* 11 - the top rung of the materials ladder assumes a blur behind it, but
    --blur-1/2 are zeroed below 760px (line 23). --mat-3 with no blur is a flat
    96% tint and a headline scrolling under it ghosts through visibly. Either
    the ladder carries the fallback or every consumer re-solves it. */
 --mat-3-solid:var(--c50);

 /* 12 - the bar's own height. The chapter rail's sticky offset, every
    scroll-margin-top and the bar itself all need the same number. */
 --nav-h:52px;
}

/* ===========================================================================
   FOUR MORE GAPS, closed by the play.html tokenisation pass (2026-08-03).
   Same rule as the twelve above: each is a rung the scale did not have, found
   by counting what play.css was actually writing as raw pixels, and each sits
   on an existing grid rather than inventing a new one. Measured case for every
   number: .superpowers/sdd/2026-08-02-play-page/play-tokens-report.md §2.
   Nothing here overrides anything above it.
   =========================================================================== */
:root{
 /* 13 - the FIXED spacing ladder stopped at 14px and resumed at 28px. That
    hole contains the single most-written inset on the play surface: 16px, in
    24 declarations (the Play menu's item padding, the badge insets, the
    tournament card gutters), plus 20px x5 and 24px x3. Below 16 the ladder
    runs on a 2px grid (2,4,6,8,10,12,14); at and above 16 it runs on a 4px
    grid, which --sp-28/--sp-32 already assume. These three complete it:
    2,4,6,8,10,12,14,16,20,24,28,(30),32. */
 --sp-16:16px;--sp-20:20px;--sp-24:24px;

 /* 14 - a radius BELOW --r-xs. The scale is 6/10/14/20/28 plus --r-hair (2),
    --r-pill and --r-full, and play.css writes border-radius:4px 28 times --
    more than every named radius on the page combined. 4px is what the small
    controls (menu items, the head thumbnails, the goal posts, every chip)
    have always used; it is not --r-hair's 2px and rounding it up to --r-xs's
    6px would be a redesign, not a refactor. Named where it sits: one step
    under --r-xs, on the same even grid. */
 --r-2xs:4px;

 /* 15 - HEADER V2 MOTION. Two durations the ladder did not have, named rather
    than written as literals because the site is already carrying 14 distinct
    transition durations and 12 distinct easing curves against two defined
    tokens -- `ease-out` and `cubic-bezier(.2,.8,.2,1)` sit side by side 272
    times and are NOT the same curve (the keyword has a lazier start and a
    softer landing), which is exactly why two controls that look identical at
    rest feel different on hover. The header adds no thirteenth curve: every
    transition in it is --ease-out.
    --hover-out-dur is deliberately SLOWER than --ease-out-dur. Hover-in should
    answer immediately and hover-out should let go gently; equal timings read as
    mechanical. One rule, applied to every interactive element in the bar.
    --nav-slide-dur is the active indicator's travel between tabs -- longer than
    a hover because it crosses real distance, and erring soft after "the
    animation opening social media is too abrupt". */
 --hover-out-dur:240ms;--nav-slide-dur:280ms;

 /* 16 - HEADER V2 ROUND 10. Three names for geometry the round needed and the
    ladder did not have. All three are 4px-grid values that were already being
    written as literals somewhere.
    --sp-36 is the ink box of a secondary control -- one rung under the bar's
    own 38px nav item, which is the header's declared one-off. The mood dock's
    buttons use it; nothing in the round writes a raw height.
    --menu-w is the width a disclosure panel opens to. Contact's socials and
    Play's head roster are the same panel and must be the same width or the two
    ends of the bar disagree; 216px is the widest of the two measured contents
    (the roster grid at two thumbnails per row) rounded to the grid.
    --menu-gap is the distance from the bar's underside to the panel. It was
    --sp-10 written inline in two files. */
 /* 17 - THE TWO RAMP RUNGS THAT WERE BEING USED WITHOUT EXISTING.
    --c400 (headmaker.html:169, play.css x3) and --c800 (index.html x2, the
    speech bubble's text) resolve to nothing with no fallback, so those elements
    silently inherit whatever colour their parent happens to have. Both predate
    this branch. Defined rather than deleted because five live declarations want
    them, and defining is strictly better than a silent inherit.
    --c400 is #8F8F8F, one step lighter than --c500 (#757575), as proposed in
    the audit.
    --c800 is #383838, and it is a PERCEPTUAL midpoint, not an sRGB one: the
    ramp jumps --c700 (#525252, L* 35) straight to --c900 (#212121, L* 12.6),
    and the L* midpoint 23.8 is #383838. The naive sRGB average (#3A3A3A) sits
    visibly closer to --c700 because the transfer curve is not linear.
    FLAGGED FOR JAYDEN: these are the first two new rungs in his colour ramp
    and they are my values, not his. Nothing outside those five declarations
    consumes them, so changing them is one line each. */
 --c400:#8F8F8F;--c800:#383838;

 --sp-36:36px;--menu-w:216px;--menu-gap:10px;
    /* --ico-mark is the brand mark's box, and it is a STROKE-MATCH value, not a
       size preference: images/logo.svg is stroke-width 2 in a 30-unit viewBox,
       so it renders 2*(size/30) -- 1.20px at 18, 1.47px at 22, 1.60px at 24.
       The icon system renders 1.5px. 22 is where the mark matches its
       neighbours without overriding anything. */
 --ico-mark:22px;
    /* --menu-max-h is where a panel stops growing and starts scrolling inside
       itself. Play's carries a saved-heads roster that has no fixed length, and
       a panel that runs past the fold cannot be closed by scrolling to it. */
 /* 440px was the binding constraint at every realistic desktop height -- 70svh only
    bites below a 629px viewport -- so the Play panel scrolled with ONE head in it:
    measured content 466px against a 440px cap, before the roster grew at all. That is
    a scrollport firing when 630px of room was free, which is the opposite of what this
    token is for. Re-derived from the space that actually exists: the viewport, less the
    bar it hangs from, less one gutter top and bottom. At 900 that is 780 (the fullest
    panel is 525, no scroll); at 700 it is 580 (still no scroll); below ~620 it scrolls,
    which is the safety valve doing its real job rather than a fixed number doing it
    everywhere. Raising this REMOVES a scrollport, it does not add one. */
 --menu-max-h:calc(100svh - var(--nav-h) - var(--sp-16-24) - var(--sp-16-24));

    /* ── A KNOWN GAP IN THIS LADDER, recorded 2026-08-03 so the next person does not
       rediscover it by shipping an invalid declaration.
       The FIXED rungs stop at --sp-40. The RESPONSIVE rungs run to --sp-80-152.
       So anything that needs a FIXED 48, 56, 64, 72 or 80 has no rung to use, and the
       only thing that resolves to those values is a responsive rung -- which SILENTLY
       CHANGES BELOW 880px. var(--sp-32-48) is 48 on desktop and 32 on a phone; if you
       wanted a constant 48 you have quietly shipped two different layouts.
       This bit twice in one pass: gap:var(--sp-48) was written, --sp-48 does not
       exist, the declaration was invalid, and the gap fell back to its initial value
       with nothing reporting it -- token-audit.py does not flag an undefined token
       used without a fallback inside a shorthand. The workaround in use today is
       --sp-32-48 for a nominal 48, documented at each call site.
       THE FIX, when someone has room: extend the fixed ladder to --sp-48/56/64/72/80.
       They are all on the 4px grid and none of them conflict with an existing name.

       CLOSED 2026-08-04 -- the five rungs are declared below. Read the note above as
       the reason they exist, not as an open item. */
}

/* ===========================================================================
   THE FIXED LADDER'S LAST FIVE RUNGS.  2026-08-04, closing the gap recorded
   immediately above.

   The FIXED rungs ran 2,4,6,8,10,12,14,16,20,24,28,(30),32,36,40 and stopped at
   40. The RESPONSIVE rungs run to --sp-80-152. So anything that wanted a
   CONSTANT 48, 56, 64, 72 or 80 had exactly two options, and both were wrong:
   write the literal, or borrow a responsive rung that SILENTLY HALVES BELOW
   880px. var(--sp-32-48) is 48 on a desktop and 32 on a phone; code that wanted
   a constant 48 and reached for it has quietly shipped two different layouts.
   That trap has already cost this project one invalid declaration (`--sp-48`
   written before it existed, so the shorthand fell back to its initial value
   with nothing reporting it -- token-audit.py does not catch an undefined token
   inside a shorthand).

   All five are on the 4px grid the ladder already runs on at and above 16, none
   collides with an existing name, and NONE OF THEM OVERRIDES ANYTHING: they are
   new names, so no shipped value moves by declaring them. They are additive on
   purpose -- five agents are editing this site concurrently and a rename or a
   deletion here ripples across eight pages.

   WHEN TO REACH FOR WHICH. A fixed rung is correct when the value must not
   change with the viewport -- a control's own geometry, a fixed gutter, a
   reserved height. A responsive --sp-*-* rung is correct when the value IS the
   page breathing -- the space between two sections. If you cannot say which of
   those two a number is, it is the responsive one.
   =========================================================================== */
:root{--sp-48:48px;--sp-56:56px;--sp-64:64px;--sp-72:72px;--sp-80:80px}

/* ===========================================================================
   THE TWO MISSING LEADING RUNGS.  2026-08-04.

   The ladder was --lh-flat 1 · --lh-display 1.0 · --lh-tight 1.08 ·
   --lh-body/--lh-prose 1.5. The typography audit's verdict was that "leading
   misses at BOTH ends", and the reason is visible the moment you count what the
   pages actually write: there is a rung for a HEADING and a rung for a
   PARAGRAPH and nothing on either side of them, so anything that is not one of
   those two things has to invent a number. Counted across index.html, about.html
   and the five case studies, 60 raw line-height literals in the 1.09-1.55 band:

     1.12 x5  .head (case-study page title)      1.30 x5  .hq
     1.14 x5  .head, <=760 fork                  1.35 x10 .fv .fvsub
     1.20 x7  .hmwQ .moodGo .mhTxt               1.40 x2  .csInfoVal .pullBody
     1.22 x5  .stepHead                          1.45 x16 .shotNote .baNote
     1.25 x5  .hlabel                                     .playerNote figcaption
                                                 1.55 x1  .csNoteText

   TWO CLUSTERS, NOT ONE, and this is the answer to "is one rung enough":
   everything from 1.12 to 1.25 is type LARGER than body that still wraps -- page
   titles, section sub-heads, a 36px pull-question. Everything from 1.35 to 1.55
   is type SMALLER than body -- captions, photo notes, figcaptions. They are
   pulling in opposite directions from 1.5 and no single value serves both. The
   band's median is 1.2 on one side and 1.45 on the other, and those are the two
   values already written most often (7 and 16 declarations respectively), so the
   rungs are the site's own measured behaviour rather than a preference.

   --lh-head 1.2   the rung the brief asked for, between --lh-tight and --lh-body.
                   Large type that wraps. Tighter than prose because the lines are
                   long in ems and the eye does not need the extra return distance;
                   looser than --lh-tight because 1.08 on two wrapped lines of 32px
                   sets the descenders of one line into the ascenders of the next.
   --lh-note 1.45  small type. Slightly tighter than --lh-prose 1.5 because at
                   12-13px the same RATIO reads looser -- leading is proportional
                   and legibility is not -- which is precisely why sixteen
                   declarations independently landed on 1.45 rather than reusing
                   --lh-prose.

   Additive: nothing existing resolves differently for their being declared.
   Conversion of the call sites is a separate, measured pass -- see the report for
   which literals moved to a rung and which are recorded as genuinely wanting
   their own value.
   =========================================================================== */
:root{--lh-head:1.2;--lh-note:1.45}

/* ===========================================================================
   THE TWO HERO TYPE RUNGS THAT WERE BEING USED WITHOUT EXISTING.  2026-08-09.

   Same test, and the same verdict, as --panel-w / --bar-h / --lbl-w above:
   found by diffing custom-property DEFINITIONS across every shipping file
   rather than by reading any one of them. --fs-hero is declared in THREE files
   and --fs-heroline in TWO, and this file -- the one that owns the type scale --
   declares neither:

     --fs-hero      index.html:89   play.css:63   specimen.html:59
     --fs-heroline  index.html:89   play.css:63

   ALL FIVE DECLARATIONS AGREE TODAY, which is exactly why this is worth doing
   now: two names for one geometry is the state immediately before they
   disagree, and nothing would report it when they did. play.css:51 already says
   so in as many words -- "not in tokens.css at all; kept verbatim so the ..." --
   which is a file flagging its own copy and having nowhere to move it to.

   WHAT THEY ARE. --fs-hero is the bare h1 rung: bigger than --fs-pagehead (52)
   and smaller than --fs-display (168), the size an h1 takes when it is the only
   thing on the screen. --fs-heroline is the STEPPED-DOWN size the home and Play
   heroes actually set, because both of those h1s wrap to two or three lines and
   the bare rung is sized for one; it aliases --fs-pagehead-sm rather than
   carrying its own literal, which is what keeps the step a RELATIONSHIP instead
   of a second number to maintain.

   ADDITIVE, AND NOTHING SHIPPED MOVES BY DECLARING THEM. index.html and
   play.css keep their local copies and those copies still win -- a page's own
   :root is later in the cascade. This is the home the two names did not have;
   deleting the local copies belongs to those files' owners, and until they do,
   the values here are identical so the site renders byte-for-byte as before.

   NOT DONE HERE, deliberately: --accent-live (#17A45A, three copies, same
   shape of fork) stays out. The header of this file states the decision that
   the tournament's live-match green is NOT a token this file defines, and
   reversing a recorded decision is a design call for Jayden, not a tidy-up.
   It is reported in docs/superpowers/specs/2026-08-09-system-conformance-audit.md
   instead.
   =========================================================================== */
:root{--fs-hero:clamp(38px,4.7vw,60px);--fs-heroline:var(--fs-pagehead-sm)}

/* ===========================================================================
   THE RADIUS LADDER, AND THE RULE FOR CHOOSING A RUNG.  2026-08-04.

   Jayden: "I thought we were doing 28 squircle for everything -- when did we
   decide on 20?" The honest answer was that 28 had only ever been assigned to
   three things (the nav drawer, the work cards, the cup panel) while the
   design-tokens spec wrote `.card{border-radius:var(--r-lg)}`. Both were
   defensible and neither was written down, which is the whole complaint: it
   looked like pick-or-choose because the choosing rule did not exist.

   HIS DECISION, and it is now the rule: RADIUS IS A FUNCTION OF THE BOX'S SIZE,
   and there are three size classes.

     --r-xl  28   THE BIGGEST SURFACES.  >= 900px wide.
                  Full-width media frames, the work cards, the nav drawer, the
                  cup panel. These are objects the width of the page's measure.
     --r-lg  20   CARDS AND IMAGES.  ~200px to <900px.
                  Stats, phone shots, column-width photo figures, menus,
                  tooltips, the About stack.
     --r-md  14   CONTROLS.  Buttons, tabs, fields, free-standing chips.

   BELOW THOSE: --r-sm 10 for a row inside a menu (the concentric rule -- an
   --r-lg panel with an --sp-8 inset gives its rows --r-md, and an --r-md control
   inside that gives --r-sm); --r-xs 6 and --r-2xs 4 for small inline marks (a
   numeral chip, a toast, a 130px logo tile); --r-pill for a capsule; --r-full
   for a circle. --r-hair 2 is a 2px underline's cap, not a box.

   THE 900px BOUNDARY IS A MEASUREMENT, NOT A ROUND NUMBER. Every element on the
   site carrying a radius was measured at 1440. The widths cluster hard, with one
   large gap: 1200 · 1120 · 1110 · 1103 · 980 | 552 · 540 · 520 · 320 · 286 · 270
   · 216 · 206. The gap between 980 and 552 is the biggest in the distribution by
   a factor of four, and it is exactly the line between "as wide as the column"
   and "sits inside the column". 900 falls in it with room either side, so no
   element is near enough the boundary for a viewport change to flip its class.

   ONE DELIBERATE CARVE-OUT, so nobody "fixes" it: `.shot.phone img` on the case
   studies is `clamp(20px, 2.6vw, 32px)`, which is off the ladder on purpose. It
   is a PICTURE OF A PHONE, and that corner is the device's, drawn to scale with
   the render -- it is content, not chrome. The ladder governs surfaces the site
   draws, not the geometry of objects inside a photograph.
   =========================================================================== */

/* ===========================================================================
   THE CONTROL BLOCK.  Specified in docs/superpowers/specs/2026-08-03-control-
   system.md §10 and never landed until now. Both builders (headmaker.html,
   gradientlab.html) have been consuming control values with no token home,
   which is the state right before two components invent two geometries.

   ONE CHANGE FROM THE SPEC AS WRITTEN, and it matters. §10 gives --ctl-accent
   its own literal, with this reason: "--accent is a page-level signal and TWO
   pages deliberately re-declare it ... A control rule written against
   var(--accent) therefore renders GREEN on play.html and in the Lab." That is no
   longer true -- both rebindings were removed on 2026-08-04 and --accent is now
   defined in exactly one place. So the control accent ALIASES the page accent
   instead of duplicating it. Keeping two literals in sync by hand is the drift
   this file exists to prevent, and the hazard the separate token was insuring
   against has been deleted at the root.

   Everything else is the spec's table verbatim. Only --dur-state-out and
   --dur-intent are new numbers; the rest alias rungs that already exist, which
   is the point -- a control's height IS the tap minimum, a control's radius IS
   the control rung of the radius ladder, and saying so in a token is what stops
   the next component picking a different one.
   =========================================================================== */
:root{
 /* the control accent -- an alias, not a copy. See above. */
 --ctl-accent:var(--accent);
 --ctl-accent-press:var(--accent-press);
 --ctl-accent-wash:var(--accent-wash);

 /* geometry -- two height rungs and nothing else */
 --ctl-h:var(--tap-min);      /* 44px, the default */
 --ctl-h-sm:var(--sp-36);     /* 36px, the compact rung. Legal only under §4.2's
                                 three conditions: inside a >=44px container, with
                                 the ::after hit expander, and >=8px from its
                                 neighbours. Free-standing in content is never
                                 compact. */
 --ctl-pad:var(--sp-16);      /* 16px inline at the 44 rung. 44 x 0.36 */
 --ctl-pad-sm:var(--sp-12);   /* 12px inline at the 36 rung. 36 x 0.36 */
 --ctl-gap:var(--sp-8);       /* icon <-> label at 44 */
 --ctl-gap-sm:var(--sp-6);    /* icon <-> label at 36; also label <-> caret at any size */
 --ctl-r:var(--r-md);         /* 14px, the control rung of the radius ladder above.
                                 Inside an --r-pill bar an item takes --r-pill --
                                 the concentric rule, and 999 minus 6 is still 999. */

 /* ink and type */
 --ctl-fs:var(--fs-small);        /* 15px at the 44 rung */
 --ctl-fs-sm:var(--fs-nav);       /* 14px at the 36 rung */
 --ctl-ink:var(--theme-muted,var(--c700));        /* semantic resting ink */
 --ctl-ink-strong:var(--theme-ink,var(--c950));   /* hover, selected, and focus ink */
 --ctl-ink-mute:var(--theme-muted,var(--c500));   /* caret and placeholder ink */
 --ctl-disabled-o:.38;

 /* ===================== THE MOTION LADDER =====================
    SIX RUNGS, NAMED FOR THE JOB, NOT THE LENGTH. This block started as the
    control system's three tiers; the 2026-08-08 motion audit found the site
    carrying 34 distinct literal transition durations and 11 distinct literal
    easings against two motion tokens, so it is now the site-wide index rather
    than the control system's private set. Pick by the JOB column. If no row
    describes what you are doing, you are probably choreographing, not
    transitioning -- see THE EXCEPTIONS below.

      RUNG              ms   JOB
      --dur-press      100   the acknowledgement under the finger. Transform
                             only, and it must finish before the release.
      --dur-state      160   hover-IN / focus / aria-selected. A control
                             changing its own colour, border or ink IN PLACE.
      --dur-state-out  240   the same change letting go. 160 x 1.5 -- a control
                             answers immediately and releases gently; equal
                             timings read mechanical, and "the animation was too
                             abrupt" is a note this site has already had twice.
      --dur-move       280   travel across real distance -- the nav ink sliding
                             between tabs, a carousel, a caret crossing a row.
                             Longer than a state change because it covers ground.
      --dur-reveal     360   appear or dismiss IN PLACE -- menus, sheets,
                             scrims, panels, a scoreboard growing a part.
      --dur-enter      500   first arrival. A section, panel or page region
                             showing up on load or on scroll-in. Once per view.

    NOT RUNGS, and deliberately so:
      --dur-intent     120   a DELAY, not a duration (§6.1) -- why a cursor
                             sweeping the bar does not fire three menus.
      --theme-duration 400   site-theme.css. A whole-page repaint, not one
                             element, and it forks to 280ms under 760px and 0ms
                             under prefers-reduced-motion. Owned there.

    EVERY RUNG EXCEPT --dur-move AND --dur-reveal WAS ALREADY IN THIS FILE under
    another name (--press-dur, --ease-out-dur, --hover-out-dur, --nav-slide-dur,
    --enter-dur, --sp-settle-dur). That is the evidence the ladder is DERIVED
    and not invented: it is the set the site had already converged on, given one
    home and one naming convention. The older names stay valid and are what
    these alias, so nothing that already references them breaks.

    THE EXCEPTIONS -- values that are NOT on the ladder and must not be pulled
    onto it. A measurement can tell you a value is unusual; it cannot tell you
    it is wrong.
      1. --hero-time-duration (hero-time.css). The sky's own cross-fade,
         deliberately slower than chrome. The sky is not a control.
      2. The spring durations --sp-quick-dur / --sp-pop-dur / --sp-bounce-dur.
         These are BOUND TO THEIR CURVES: a linear() spring is sampled for a
         specific length and re-timing it re-shapes the overshoot. The head has
         weight and --sp-bounce is where that weight lives. Never swap one for a
         generic ease.
      3. about.html's dissolve -- 680ms in / 1000ms out / 1200ms drift. Three
         different numbers ON PURPOSE, so the crossfade is never a 50/50 blend.
         Collapsing them to one rung is the bug, not the fix.
      4. The pitch props in play.css -- .hmBall / .hmGoal / .hmScore and their
         shadows all fade at .3s, and play-engine.js writes the SAME .3s inline
         when it builds crates, rings and crowns at runtime. It is one authored
         convention spanning CSS and JS; moving the CSS half alone would create
         the inconsistency this ladder exists to remove.
      5. prefers-reduced-motion forks (0ms / 1ms / `none`). Those are the
         accessibility contract, not stray literals.

    ONE CURVE for chrome: --ease-out. The `ease-out` KEYWORD is not the same
    curve (lazier start, softer landing) and is why two controls that look
    identical at rest feel different on hover -- see --ease-exit below for the
    only sanctioned second curve. */
 --dur-press:var(--press-dur);          /* 100ms */
 --dur-state:var(--ease-out-dur);       /* 160ms */
 --dur-state-out:var(--hover-out-dur);  /* 240ms -- was a second literal 240 next
                                           to --hover-out-dur's. One number, one
                                           definition; this is now the alias. */
 --dur-move:var(--nav-slide-dur);       /* 280ms */
 --dur-reveal:var(--sp-settle-dur);     /* 360ms -- the spring's own settle time,
                                           reused rather than a seventh number.
                                           Pair it with --ease-out for a plain
                                           fade, or with --sp-settle when the
                                           thing that appears should have weight. */
 --dur-enter:var(--enter-dur);          /* 500ms */
 --dur-intent:120ms;                    /* a delay, not a rung. See above. */

 /* The ONE sanctioned second curve. --ease-out decelerates, which is right for
    anything arriving and wrong for anything leaving: a dismissal that eases OUT
    hangs on the screen after the user has moved on. This is the accelerating
    mirror, and it was already shipping as a literal in six places (the party
    scrim and the disco ball, both on their way off). Named, not added. */
 --ease-exit:cubic-bezier(.3,0,.8,.15);

 /* between-control spacing. One ladder, 2:1, and no third value: the gap between
    GROUPS is exactly twice the gap between ITEMS, which is what lets a bar read
    as groups without drawing a divider. --gap-item is also the minimum
    separation between two expanded 44px hit areas, so it is a FLOOR. */
 --gap-item:var(--sp-8);
 --gap-group:var(--sp-16);
 --gap-band:var(--sp-24);

 /* Semantic control materials. These aliases deliberately resolve through the
    local theme instead of through a page-specific Hero colour. All resting
    secondary/icon chrome and all popup sheets are therefore opaque in both
    light and Night, while the raw material and grey ramps stay private. */
 --ctl-ground:var(--theme-surface,var(--c0,#fff));
 --ctl-ground-hover:var(--theme-elevated,var(--c75));
 --ctl-menu-ground:var(--theme-surface,var(--c0,#fff));
 --ctl-container-ground:var(--theme-page,var(--c50));
 --ctl-primary-ground:var(--theme-ink,var(--c950));
 --ctl-primary-ground-hover:var(--theme-muted,var(--c900));
 --ctl-primary-ink:var(--theme-page,var(--c50));
 --ctl-focus:var(--theme-focus,var(--ctl-ink-strong));
 --ctl-rim:0 0 0 var(--hair-w) var(--theme-rim,rgba(17,18,20,.12)) inset;
 --ctl-rim-strong:0 0 0 var(--hair-w) var(--theme-rim-strong,rgba(17,18,20,.20)) inset;
 --ctl-container-rim:0 0 0 var(--hair-w) var(--theme-rim,rgba(17,18,20,.12)) inset;
 /* --ctl-menu-shadow is gone. It was var(--ctl-rim),var(--sh-2) -- a rim plus a
    real drop shadow -- and it was the site's own no-shadows-on-chrome rule being
    broken by the library meant to enforce it. Menus take --ctl-container-rim. */

 /* Shared surface/container system. Components consume these semantic aliases;
    authored media continues to own its pixels while the shared frame owns the
    ground, rim, radius, padding, and responsive page gutter around it. */
 --surface-ground:var(--theme-surface,var(--c0,#fff));
 --surface-ground-muted:var(--theme-elevated,var(--c75));
 --surface-rim:var(--ctl-container-rim);
 --radius-container:var(--r-xl);
 --radius-media:var(--r-lg);
 --radius-control:var(--r-md);
 --radius-menu:var(--r-lg);
 --surface-radius:var(--radius-container);
 --surface-radius-compact:var(--radius-media);
 /* The radius ladder governs OBJECTS -- cards, media, controls, things that sit
    ON the page. The Hero is not an object, it is the environment the page sits
    in, so it is the one surface with no corners at all. Everything else stays
    a rounded card precisely because this one stopped being one; a partial
    radius here would read as a mistake rather than a decision. */
 /* THREE SQUARE EDGES AND ONE CURVED ONE, and that is not a compromise radius.
    The Hero runs to the viewport on the left, right and top, and an edge that
    meets the viewport has nothing to round against -- a corner there would be
    a notch of page colour. The BOTTOM edge is the only one that terminates
    inside the page, so it is the only one that gets a corner. Jayden picked
    this directly: sharp bottom edge, curved bottom corners, no fade. */
 --surface-hero-radius:0 0 var(--r-xl) var(--r-xl);
 --surface-hero-pad:var(--sp-16-40);
 --surface-pad:var(--sp-32);
 --surface-inset:var(--sp-8);
 --surface-gutter:var(--sp-40);
 --surface-gap:var(--sp-16-24);
 --section-join-gap:var(--sp-16);
 --work-item-gap:var(--sp-64);
 --work-meta-gap:var(--sp-16);
 --work-title-size:var(--fs-lead);
 --work-year-size:var(--fs-small);
 --media-mockup-inset:var(--sp-32);
 --hero-peek-width:520px;
 --hero-peek-depth:calc(190px + var(--sp-16));
 --hero-peek-offset:var(--sp-8);
 --hero-peek-lift:var(--sp-64);
 /* NO BLEED AND NO BOTTOM FADE. A bleed token and a downward mask existed so
    the light could fall into the work section; Jayden rejected that outright
    ("I liked when it was sharp"). The gradient now stops dead on the Hero's
    own lower edge, and the rounded bottom corners are what keep that hard
    stop from reading as a slab. Both tokens are deleted rather than zeroed so
    neither reads as a knob someone forgot to turn back.
    THE VERTICAL PERCENTAGES ARE THEREFORE THE AUTHORED ONES AGAIN -- with no
    bled box to re-express them against, 100% is the Hero's floor once more.
    The six radials were authored against a 1200px box. Full width makes the
    same HORIZONTAL percentage spread out and the glow flatten, so that radius
    is capped as a LENGTH: the light source keeps its size instead of growing
    with the monitor. That is the whole retune -- one axis, not both. */
 --hero-glow-rx:clamp(1060px,84vw,1720px);
 --hero-glow-rx-near:clamp(560px,44vw,900px);
 --hero-glow-rx-far:clamp(740px,58vw,1180px);
 --selection-ink:#0A84FF;
 --selection-line:1px;
 /* A UNIFORM RING OF AIR, IN SCREEN PIXELS. A frame drawn tight against an
    irregular photographic cutout reads as cramped -- it traces a silhouette
    instead of describing a region, which is the other half of "too conformed
    to the head". This is added to the frame's half-extents and is never
    scaled, so the gap is identical at 0.24x and at 2.2x. */
 --selection-air:8px;
 --selection-handle-size:8px;
 --selection-hit-size:var(--tap-min);
 --selection-rotate-size:10px;
 /* The portrait is a peeking head, not a contained object: at rest its logical
    rect already hangs ~150px below the Hero's lower edge. A containment clamp
    therefore makes the RESTING position illegal, so once the head is dragged
    away it can never be put back. The clamp is stated as visibility instead --
    a share of the head's box must stay inside the Hero on every axis -- which
    the resting composition satisfies and which is what "still grabbable"
    actually means. --hero-head-safe-gap is the pixel floor under that share so
    a head at minimum scale cannot shrink its own handle out of reach. */
 --hero-head-min-visible:.42;
 --hero-head-safe-gap:var(--sp-16);
 --hero-head-min-scale:.24;
 --hero-head-max-scale:2.2;
 --hero-head-x:0px;
 --hero-head-y:0px;
 --hero-movie-guard-y:0px;
 --hero-head-scale:1;
 /* Rotation is clamped to a half turn either way, which still reaches every
    orientation, so nothing is unreachable -- but the angle cannot accumulate
    into a number that makes level feel far away. Snap and step are degrees. */
 --hero-head-rotate:0deg;
 --hero-head-min-rotate:-180;
 --hero-head-max-rotate:180;
 --hero-head-rotate-snap:6;
 --hero-head-rotate-step:2;
 --hero-head-rotate-step-large:15;
 /* Written by hero-head-transform.js: the logical head's centre inside the
    wrapper, so rotation turns the head about itself rather than about the
    stage's corner. The authored values are the data-head-bounds midpoints. */
 --hero-head-origin-x:51%;
 --hero-head-origin-y:51.5%;
 /* THE FLOAT. Amplitudes are deliberately below the threshold at which a
    visitor can consciously track the motion -- it should read as "suspended"
    in peripheral vision while they read the headline, not as a bob.
    THE PERIODS ARE MUTUALLY PRIME-ISH ON PURPOSE. One sine wave on Y is
    instantly recognisable as canned; three slow motions at 5.9s / 8.3s /
    11.7s plus a small 3.7s harmonic on Y only re-align after about 38
    minutes, so the path never visibly repeats. Seconds, written as plain
    numbers because JS does the arithmetic. */
 --hero-head-float-y-amp:9;
 --hero-head-float-y-period:5.9;
 --hero-head-float-y2-amp:3;
 --hero-head-float-y2-period:3.7;
 --hero-head-float-x-amp:5;
 --hero-head-float-x-period:8.3;
 --hero-head-float-rot-amp:.7;
 --hero-head-float-rot-period:11.7;
 /* How far the contact shadow answers the float. Spread grows the shadow as
    the head rises, fade lightens it -- the coupling that makes a suspended
    object read as an object rather than a sticker with a smudge under it. */
 --hero-head-shadow-spread:.14;
 --hero-head-shadow-fade:.30;
 /* The greeting: how far below its resting place the head starts, and how
    long it waits so it arrives just after the headline has been read. */
 --hero-head-enter-rise:64px;
 --hero-head-enter-delay:420ms;
 --scene-cut-duration:250ms;
 --scene-cut-ease:steps(2,end);
 --star-twinkle-duration:7s;
 --star-twinkle-ease:var(--ease-out);
 --star-bright-size:1.5px;
 --star-glow-size:var(--sp-2);
 --menu-viewport-gutter:var(--sp-16);
 --skip-inset:var(--sp-10);
 --skip-hidden-offset:calc((var(--ctl-h) + var(--sp-16)) * -1);
 --floating-control-inset:var(--surface-gutter);
 --layer-floating-control:90;
 --layer-skip-link:200;

 /* THE BUILDERS' PANEL. headmaker.html and gradientlab.html are the same
    archetype -- an artifact beside an inspector -- and each declared its own name
    for the inspector's width: --mk-panel:360px and --panel-w:360px. They AGREE
    today, which is the good case and also the fragile one: two names for one
    geometry is the state immediately before they disagree, and nothing would
    report it when they did. Named once here, at the value both already use, so
    the two tools stay the same tool. --panel-w is the surviving name because
    gradientlab already writes it; headmaker's --mk-panel should be repointed to
    it by that file's owner. */
 --panel-w:360px;

 /* THE OTHER TWO THE ARCHETYPE OWNS. Same test as --panel-w and the same
    verdict: both are declared in headmaker.html AND gradientlab.html, and
    both are declared IDENTICALLY. Extracted by diffing the two files' custom
    properties rather than by reading them -- if two independent files have
    arrived at the same number, that number is describing the archetype, not
    either page, and the second copy is always the one that goes stale.

    --bar-h is the height the tool's own bar reserves above the workspace.
    It is deliberately an EXPRESSION rather than a literal: it is the site
    header's height plus the gutter above and below it, so a change to --nav-h
    moves both builders' layouts together instead of leaving them 8px apart.

    --lbl-w is the label column every control in a builder panel aligns to.
    A fixed label column is the whole reason a stack of sliders reads as one
    object rather than as six unrelated rows -- every track starts on one x.
    THE 420px FORK COMES WITH IT and is not optional: below ~420 the label
    column has to give way before the track does, because a track that has
    stopped being draggable is a broken control while a truncated label is
    only a tighter one. This was reported as a divergence -- the Gradient
    Maker had the fork and the head maker did not -- and by the time it was
    measured here both files carried it, identically, at the same breakpoint.
    Landing it once is what stops that being luck. */
 --bar-h:calc(var(--sp-14) + var(--nav-h) + var(--sp-6));
 --lbl-w:84px;
}
@media(max-width:760px){:root{
 --surface-pad:var(--sp-16);--surface-gutter:var(--sp-16);
 --radius-container:var(--r-lg);--radius-media:var(--r-md);
 --surface-hero-radius:0 0 var(--r-lg) var(--r-lg);
 --surface-hero-pad:var(--sp-16);
 /* The phone Hero is already narrower than the authored box, so the cap that
    protects the desktop focus would only shrink the glow further here. */
 --hero-glow-rx:104%;
 --hero-glow-rx-near:88%;
 --hero-glow-rx-far:110%;
 /* A head twice the phone's width is cropped, not big. 1.7 is the largest
    scale whose logical box still fits inside 390 with the copy gutter. */
 --hero-head-max-scale:1.7;
 /* Was clamp(600px,100svh - 160px,680px): a phone Hero deliberately shorter
    than the screen. Full-bleed retires that -- the Hero IS the screen now. */
 --hero-mobile-height:100dvh;
 --work-item-gap:var(--sp-40);
 --work-meta-gap:var(--sp-12);
 --media-mockup-inset:var(--sp-16);
 --hero-peek-width:clamp(312px,calc(100vw - var(--sp-8)),520px);
 --hero-peek-depth:clamp(110px,calc(42vw - var(--sp-24)),140px);--hero-peek-offset:var(--sp-4);
 --hero-peek-lift:var(--sp-48)
}}
@media(max-width:340px){:root{
 --hero-peek-offset:var(--sp-2)
}}
@media(max-width:420px){:root{--lbl-w:72px}}

/* ===========================================================================
   FOUR MORE GAPS, closed by the home page's CASE-STUDY CARD pass (2026-08-03).
   Same rule as every block above: each is a rung the scale did not have, found
   by counting what the card grid on index.html was actually writing as raw
   pixels, and each sits on a grid that already exists. Measured case for every
   number: .superpowers/sdd/2026-08-02-play-page/cards-report.md.
   Nothing here overrides anything above it.
   =========================================================================== */
:root{
 /* 17 - the FIXED ladder's next 4px rung. It ran 16,20,24,28,(30),32,36 and
    stopped. 40px is the page's own side gutter and it is written as a literal
    in four places on index.html alone (.wrap, .work, .caseInset, .csStageInner)
    plus twice more inside `calc(100vw - 80px)`, which is this value doubled.
    It is NOT --sp-16-40: that rung is RESPONSIVE and collapses to 16px below
    881px, so it cannot stand in for a gutter that stays 40 until the 760px
    fork replaces it wholesale. */
 --sp-40:40px;

 /* 21 - HEADER V2, ROUND 11. The bar had been subtracted past quiet into
    absent: measured at 1280 on index.html the lit nav item was rgb(117,117,117)
    and so was every unlit one (1.00:1), and the bar's own ground was --c50, the
    same #FDFDFD as the page under it (1.00:1). Four names close that.

    --c0 IS THE RAMP'S MISSING TOP RUNG, and the ramp genuinely did not have it:
    it starts at --c50 #FDFDFD, which is the PAGE. Jayden's call is that the bar
    should read lighter than the page it floats over -- separation upward, which
    is what a floating element does and is the one direction a no-shadow rule
    leaves open. Nothing lighter than the paper existed to say that with.
    It is opaque, and that is a measurement rather than a preference: --mat-1/2/3
    are all rgba(253,253,253,a), i.e. --c50 at reduced alpha, so over a --c50
    page every one of them composites back to exactly #FDFDFD -- 1.00:1, the
    problem restated. A translucent white cannot be lighter than a white ground.
    #FFFFFF vs #FDFDFD is 1.02:1 on its own, so the rim below is doing most of
    the drawing; what the white buys is that the bar never reads as a grey plate,
    which was the previous round's error.

    --rim-3 is the third rung of the rim ladder (.08 / .14 / .20). Composited on
    white it is #D1D1D1 = 1.56:1 against the #FDFDFD page, against --rim-2's
    1.31:1. With no cast shadow permitted on chrome and the ground only 1.02:1
    off the paper, the rim IS the component's edge, so it takes the rung the
    ladder stopped one short of.

    --ico-stroke-strong IS THE ICON RAMP'S SECOND RUNG, and the ramp had one.
    An icon beside a label has to change weight when the label does -- the lit
    nav item goes 400 -> 600 and its glyph was staying at 1.5px, so half the
    control answered and half did not. Two facts fix the value rather than an eye:
    Tabler, the pack every icon on this site comes from, ships its own stroke
    ladder as 1.25 / 1.5 / 2, so 2 is the pack's next rung and not a new weight;
    and measured on this site's own face, Instrument Sans's vertical stem at 14px
    goes 400 -> 600 as a x1.32 step, which puts 1.5 at 1.98. The two agree.
    --ico-mark-stroke / --ico-mark-stroke-strong are the same pair for the brand
    mark, which needs its own numbers because it is NOT non-scaling-stroke: the
    drawing is a 30-unit viewBox rendered at --ico-mark, so its effective weight
    is stroke*(22/30). 2 gives 1.47px, which is what makes the mark agree with a
    1.5px nav icon at rest; 2.7 gives 1.98px, matching --ico-stroke-strong. Both
    are the SVG's own unitless stroke-width, so they are not px values missing a
    unit. */
 --c0:#FFFFFF;
 --rim-3:inset 0 0 0 1px rgba(18,18,18,.20);
 --ico-stroke-strong:2;
 --ico-mark-stroke:2;--ico-mark-stroke-strong:2.7;

 /* 18 - the page measure. 1280px is the width every full-bleed band on the
    site caps at, and it is not a spacing value -- it is the column the whole
    layout is drawn in, which is why it has never fitted on the --sp-* ladder
    and has therefore been retyped as a literal everywhere instead. One name,
    so that changing the site's measure is one edit rather than a search. */
 --page-max:1280px;

 /* 19 - the scroll-entrance travel. A case-study cover rises 26px into place
    on reveal; the caption under it rises --sp-14. 26 is the only one of the
    two with no rung (the ladder runs 2px below 16 and 4px above it, and 26 is
    on neither), so it has been retyped as a literal in both the base rule and
    the <=760 fork. This is motion distance, not spacing: it never affects
    layout, only how far the thing comes from. Paired with --enter-dur, which
    already exists and is the duration it takes. */
 --enter-rise:26px;

 /* 20 - the stagger between a cover's entrance and its caption's. The caption
    starts 80ms after the cover so the two read as one gesture with a follow,
    not as two fades. It shipped as .08s on desktop and .07s below 760px -- a
    10ms fork with no stated reason and no perceptible difference -- so this
    names the desktop value and both forks now use it. */
 --enter-stagger:80ms;
}

/* ===========================================================================
   HEADER V2 (2026-08-03). Two site-wide corrections that belong to the system
   rather than to the header, landed here because the header pass is what found
   them. Spec: docs/superpowers/specs/2026-08-03-header-v2-research.md §4.1, §7.
   index.html does not link this file yet, so it carries a verbatim copy of both
   rules in its own token block -- flagged there, deleted with the block when
   that page is finally wired to tokens.css.
   =========================================================================== */

/* WCAG 2.4.11 Focus Not Obscured (Minimum), AA. Shift+Tab scrolls the focused
   element to the very top of the viewport and the sticky bar covers it. The
   sufficient technique is scroll-padding on the SCROLL CONTAINER, not
   scroll-margin on every target: scroll-margin has known Safari bugs and the
   site had it on case-study sections only. One declaration, every anchor jump
   and every focus scroll on every page. Uses the TALLER of the bar's two
   padding states -- which is why the bar's own height must not change. */
html{scroll-padding-top:calc(var(--nav-h) + var(--sp-16-24))}   /* 76px >=881, 68px below */

/* THE ICON STROKE, for the pages that do not declare one.
   THE SPEC'S BUG DID NOT REPRODUCE, and this rule is what is left after checking.
   §4.1 reports .gIco computing to stroke-width:1 with butt caps and mitre joins
   -- 0.75px on an 18px box over a 24 viewBox, half the weight of the label. It
   does not: index.html and all five case studies each carry an "ICONS -- one
   pack" block that already normalises every icon inside a control to
   stroke-width:1.5, round cap, round join, fill:none AND
   vector-effect:non-scaling-stroke, so they render 1.5 DEVICE px regardless of
   viewBox scaling. Measured live: 1.5px / round. index.html holds three .gIco
   uses, not the 40+ the spec counted -- the game rows left it in an earlier pass.
   Declaring --ico-stroke 1.8 on top of that would have made the icons heavier
   (1.8 device px), not lighter, because non-scaling-stroke stays in force.
   So this is the SAME normalisation, copied for gradientlab.html and any future
   page that links tokens.css without carrying the block. It is deliberately a
   no-op everywhere the block already exists (those rules come later in document
   order and win the tie). Sizing is not set here: every consumer hard-codes 18px
   and would out-specify it; the header scopes its own responsive box, and its
   own authoritative stroke, in header.css. */
.gIco{stroke:currentColor;fill:none;stroke-width:1.5;
      vector-effect:non-scaling-stroke;
      stroke-linecap:round;stroke-linejoin:round}
