/* ============================================================================
   site.css :: szczleon.com
   ----------------------------------------------------------------------------
   ASSEMBLED FILE. Do not hand-edit. It is the concatenation, in cascade order,
   of the fragments under _src/. Order is the only cascade tool on this page, so
   a rule that needs to beat a default relies on coming later:

     1  core/tokens.css      palette, scales, timing, zone overrides, reset
     2  lib/motion.css       motion tokens, gating rules, reduced-motion kill
     3  core/nav.css
     4  section CSS in DOM order
     5  core/footer.css
     6  the integrator patch at the bottom of this file

   Edit the fragment, then re-assemble.
   ========================================================================= */


/* ==========================================================================
   core/tokens.css
   ========================================================================== */
/* ============================================================================
   tokens.css: the design token layer for szczleon.com
   ----------------------------------------------------------------------------
   OWNER: shell agent. Section builders READ these and never redeclare them at
   :root. If you need a value that is not here, declare it locally inside your
   own section id scope with your own prefix, or ask for it to be added here.

   Loaded FIRST, before nav.css, before every section stylesheet, before
   footer.css. Everything else in the page can therefore override it, which is
   the intended direction of the cascade.

   Five things live in this file and nothing else does:
     §1  the dark base palette, fonts, scales, timing        (:root)
     §2  the zone system: section-scoped light and dark      ([data-zone])
     §3  the reduced-motion token floor
     §4  reset
     §5  shared primitives every section is allowed to assume

   WHAT IS GONE, deliberately: html[data-lane="…"]. The three role lanes died
   with the lane directory, and with them the runtime accent re-theming, the
   .bgfx crossfade dependency on it, and the localStorage hiringLane key. Do not
   reintroduce a global theme attribute; the only theme switch on this page is
   §2's section-scoped one.
   ========================================================================== */


/* ============================================================================
   §1  ROOT: the dark base. This is the page's resting state.
   ========================================================================== */
:root{

  /* ---- surface ----------------------------------------------------------- */
  --bg:       #07080a;   /* page floor; body background                        */
  --bg2:      #0b0d11;   /* a half-step up from the floor, for inset bands     */
  --panel:    #0e1116;   /* card / panel base                                  */
  --panel2:   #13171d;   /* recessed stage inside a panel (lab demo wells)     */
  --panel-fill: linear-gradient(180deg, var(--panel), rgba(14,17,22,.35));
                         /* the standard card background. One token so the
                            light zone can flatten it to a solid without every
                            section knowing.                                   */

  /* ---- ink --------------------------------------------------------------- */
  --text:     #e8eaef;   /* body and headings. 16.65:1 on --bg                 */
  --muted:    #9aa1ad;   /* secondary prose.    7.70:1 on --bg                 */
  --faint:    #7a828e;   /* mono captions, meta. 5.16:1 on --bg                */

  /* ---- hairlines --------------------------------------------------------- */
  --hair:     1px;                        /* the only border width on the page */
  --line:     rgba(255,255,255,.08);      /* default hairline                  */
  --line2:    rgba(255,255,255,.14);      /* hover / emphasis hairline         */
  --line3:    rgba(255,255,255,.22);      /* structural divider, used sparingly */
  --seam:     rgba(255,255,255,.10);      /* the LAB grid's own background,
                                             showing through 1px gaps as seams */

  /* ---- accent scale ------------------------------------------------------ */
  --accent:      #74e0c4;                 /* the accent. 12.60:1 on --bg.
                                             glass.css consumes this one token
                                             and nothing else, so it must stay
                                             declared on :root.                */
  --accent2:     #6aa3ff;                 /* the gradient partner. 7.91:1      */
  --accent-ink:  var(--accent);           /* the accent value that is LEGAL FOR
                                             TEXT in the current zone. Use this
                                             for coloured type, always. In the
                                             light zone it is a different hue;
                                             --accent alone would fail there.  */
  --accent-soft: rgba(116,224,196,.10);   /* accent wash: fills, active rows   */
  --accent-line: rgba(116,224,196,.35);   /* accent hairline: active borders   */
  --on-accent:   #05130f;                 /* type that sits ON an accent fill  */

  /* ---- status ------------------------------------------------------------ */
  --live:  #46d27a;   /* status dot: running right now      10.27:1 on --bg    */
  --build: #f0b35e;   /* status dot: building / warning     10.79:1 on --bg    */
  --shipped: var(--accent2);              /* status dot: shipped, not running  */

  /* ---- ambient field (the fixed .bgfx + .grid-field behind everything) ---- */
  --glow-a: rgba(106,163,255,.10);
  --glow-b: rgba(116,224,196,.08);

  /* ---- depth ------------------------------------------------------------- */
  --shadow-1: 0 1px 2px rgba(0,0,0,.40), 0 12px 34px rgba(0,0,0,.35);
  --shadow-2: 0 2px 6px rgba(0,0,0,.45), 0 28px 70px rgba(0,0,0,.45);

  /* ---- type -------------------------------------------------------------- */
  /* System stacks only. Zero webfont requests, and that is a hard rule: the
     media budget is 1.5MB first load on mobile and a webfont is a request the
     page has not earned. */
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --mono: "SF Mono", ui-monospace, "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;

  /* ---- layout ------------------------------------------------------------ */
  --maxw:      1080px;   /* reading column. .wrap uses this.                   */
  --maxw-wide: 1280px;   /* wide lane: the flagship stack, the lab grid.       */
  --gutter:    24px;     /* horizontal page padding                            */
  --nav-h:     82px;     /* the vertical space to leave clear for the nav.
                            Desktop: 12px top offset + a 58px capsule + 12px of
                            breathing room. Mobile: the flush two-row ladder,
                            48 + 46, with no offset and no breathing room.
                            Anything that must clear the nav (hero top padding,
                            :target scroll offset) uses this, never a literal.  */

  /* ---- spacing scale ----------------------------------------------------- */
  /* Use these. A raw px value in a section stylesheet is a smell unless it is
     a measured geometry (a card width, a video aspect box, a seam). */
  --sp-1:  4px;
  --sp-2:  8px;
  --sp-3:  12px;
  --sp-4:  16px;
  --sp-5:  24px;
  --sp-6:  32px;
  --sp-7:  48px;
  --sp-8:  64px;
  --sp-9:  88px;
  --sp-10: 120px;
  --sp-band: clamp(64px, 9vw, 104px);   /* vertical padding of a normal section */

  /* ---- radius scale ------------------------------------------------------ */
  --r-1: 8px;      /* chips, small controls                                    */
  --r-2: 11px;     /* buttons, inputs                                          */
  --r-3: 14px;     /* small cards                                              */
  --r-4: 16px;     /* the standard card radius                                 */
  --r-5: 22px;     /* large cards, media frames                                */
  --r-6: 30px;     /* full-bleed stages                                        */
  --r-pill: 999px; /* capsules, dots, date pills                               */

  /* ---- interaction timing ------------------------------------------------ */
  /* The measured budget: interaction transitions live between 0.12s and 0.22s,
     with 0.4s as an absolute ceiling for the largest state change on the page.
     If you are reaching for a number outside that range for a hover, a focus,
     a toggle, or a press, the answer is no. */
  --t-fast: .12s;   /* colour, opacity, small nudges                           */
  --t-base: .16s;   /* the default. Most hovers and focus rings.               */
  --t-slow: .22s;   /* transforms with distance; accordion chevrons            */
  --t-max:  .40s;   /* CEILING. Section-scale state changes only.              */

  /* Entrance reveals are NOT interactions and are budgeted separately. This is
     the only duration allowed to exceed --t-max, and only on first-in-view
     reveals (line masks, materialize cascade, .rv). */
  --t-reveal: .7s;

  --ease:       cubic-bezier(.2,.7,.2,1);   /* the house out-ease             */
  --ease-inout: cubic-bezier(.4,0,.2,1);

  /* ---- focus ------------------------------------------------------------- */
  --focus: 2px solid var(--accent-ink);
  --focus-offset: 2px;
}

@media (max-width: 760px){
  :root{
    --gutter: 16px;
    /* The mobile nav is a two-row ladder flush to the top edge: 48px brand row
       plus a 46px link row. Nothing floats, so there is no top offset. */
    --nav-h: 94px;
  }
}


/* ============================================================================
   §2  ZONES: the section-scoped theme flip
   ----------------------------------------------------------------------------
   The page has exactly ONE dark-to-light flip and it lands on the decision beat:
   #cv, #faq, #contact. It is NOT a global theme swap and there is no <html>
   attribute involved. A section declares its own zone and every custom property
   below is redeclared for that section's subtree only. Custom properties
   inherit, so a card five levels deep inside a light section resolves --panel
   to white without knowing anything about zones.

   TWO SELECTORS, ONE RULE BLOCK, ON PURPOSE:
     [data-zone="light"]   authored in the HTML. This is the authoritative hook
                           and it is why the light band renders correctly with
                           JavaScript disabled.
     .zone-light           the same thing as a class, so themeflip.js can flip a
                           section with one classList call if it ever needs to.

   Author the ATTRIBUTE in markup. Never rely on the class alone: a JS-off
   browser would render the light band's dark tokens under a light background,
   which is the one failure mode this whole arrangement exists to prevent.

   A light section paints its own background because the ambient field (.bgfx,
   .grid-field) is fixed at z-index -2 / -1 and would otherwise show through.
   position:relative + z-index:0 puts the section above both.
   ========================================================================== */
[data-zone="light"],
.zone-light{
  --bg:       #f4f4f2;
  --bg2:      #ffffff;
  --panel:    #ffffff;
  --panel2:   #eaeae6;
  --panel-fill: #ffffff;

  --text:     #14161a;   /* 16.45:1 on --bg                                    */
  --muted:    #4e545e;   /*  6.92:1 on --bg,  6.32:1 on --panel2               */
  --faint:    #626874;   /*  5.08:1 on --bg,  4.64:1 on --panel2               */

  --line:     rgba(0,0,0,.10);
  --line2:    rgba(0,0,0,.20);
  --line3:    rgba(0,0,0,.30);
  --seam:     rgba(0,0,0,.12);

  /* The mint reads at 1.6:1 on this background, so it cannot survive the flip.
     Both --accent and --accent-ink move to a dark teal of the same family:
     5.54:1 on --bg, 5.06:1 on --panel2. A section that used var(--accent) for
     type in the dark zone therefore stays legible here with no extra work. */
  --accent:      #0d6f5b;
  --accent2:     #1e4fd8;   /* 6.02:1 on --bg                                  */
  --accent-ink:  #0d6f5b;
  --accent-soft: rgba(13,111,91,.09);
  --accent-line: rgba(13,111,91,.32);
  --on-accent:   #ffffff;   /* white on the dark teal: 6.10:1                  */

  --live:    #157f4a;
  --build:   #8a5a12;       /* 5.37:1 on --bg                                  */
  --shipped: var(--accent2);

  --shadow-1: 0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.07);
  --shadow-2: 0 2px 6px rgba(0,0,0,.08), 0 24px 60px rgba(0,0,0,.10);

  /* On light, the primary button is a solid near-black block, not the accent
     gradient. The gradient's --on-accent ink would sit dark-on-dark here. */
  --btn-fill: #14161a;
  --btn-fg:   #f4f4f2;      /* 16.45:1                                         */

  background: var(--bg);
  color: var(--text);
  position: relative;
  z-index: 0;
}

/* The dark zone as an explicit, addressable state. Used by the footer to return
   the page to dark after the light band. Tokens only: it deliberately does NOT
   paint a background, so ordinary dark sections keep showing the ambient field
   through them. Anything that wants an opaque dark floor sets its own
   background: var(--bg). */
[data-zone="dark"],
.zone-dark{
  --bg:       #07080a;
  --bg2:      #0b0d11;
  --panel:    #0e1116;
  --panel2:   #13171d;
  --panel-fill: linear-gradient(180deg, var(--panel), rgba(14,17,22,.35));
  --text:     #e8eaef;
  --muted:    #9aa1ad;
  --faint:    #7a828e;
  --line:     rgba(255,255,255,.08);
  --line2:    rgba(255,255,255,.14);
  --line3:    rgba(255,255,255,.22);
  --seam:     rgba(255,255,255,.10);
  --accent:      #74e0c4;
  --accent2:     #6aa3ff;
  --accent-ink:  #74e0c4;
  --accent-soft: rgba(116,224,196,.10);
  --accent-line: rgba(116,224,196,.35);
  --on-accent:   #05130f;
  --live:  #46d27a;
  --build: #f0b35e;
  --shipped: var(--accent2);
  --shadow-1: 0 1px 2px rgba(0,0,0,.40), 0 12px 34px rgba(0,0,0,.35);
  --shadow-2: 0 2px 6px rgba(0,0,0,.45), 0 28px 70px rgba(0,0,0,.45);
  --btn-fill: linear-gradient(100deg, var(--accent), var(--accent2));
  --btn-fg:   var(--on-accent);
}

/* The dark zone's button tokens also belong on :root, since :root IS the dark
   zone and most of the page never carries a [data-zone] attribute at all. */
:root{
  --btn-fill: linear-gradient(100deg, var(--accent), var(--accent2));
  --btn-fg:   var(--on-accent);
}


/* ============================================================================
   §3  REDUCED MOTION: the token floor
   ----------------------------------------------------------------------------
   This block is a SAFETY NET, not the implementation. It collapses every
   duration token so anything driven by them stops moving. It does not and
   cannot cover keyframe animations, rAF loops, IntersectionObserver reveals,
   scripted demos, video playback, or canvas. Each section is responsible for
   its own kill switch (see _src/CONTRACT.md §8). Do not treat this as done.
   ========================================================================== */
@media (prefers-reduced-motion: reduce){
  :root{
    --t-fast: .001s;
    --t-base: .001s;
    --t-slow: .001s;
    --t-max:  .001s;
    --t-reveal: .001s;
  }
  html{ scroll-behavior: auto; }
}


/* ============================================================================
   §4  RESET
   ========================================================================== */
*{ box-sizing: border-box; margin: 0; padding: 0; }

html{ scroll-behavior: smooth; scroll-padding-top: calc(var(--nav-h) + var(--sp-4)); }

body{
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;   /* the wall overruns both viewport edges on mobile      */
}

img, video, canvas, svg{ max-width: 100%; }
a{ color: inherit; text-decoration: none; }
button{ font-family: inherit; }

:where(a, button, summary, input, select, textarea, [tabindex]):focus-visible{
  outline: var(--focus);
  outline-offset: var(--focus-offset);
}


/* ============================================================================
   §5  SHARED PRIMITIVES
   ----------------------------------------------------------------------------
   Carried forward from the shipping index.html, unchanged in behaviour, with
   raw values swapped for tokens so they follow the zone flip.

   These are declared HERE AND NOWHERE ELSE. Section builders use them and do
   not redeclare them; the integrator does not add a second base layer. If a
   section needs a variant, it scopes the variant under its own section id.
   ========================================================================== */

.wrap{ max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
.wrap-wide{ max-width: var(--maxw-wide); }

.mono{ font-family: var(--mono); }

.sr-only{
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* the ambient field, fixed behind everything. Lane re-tinting is gone; the six
   radials are now static, which also removes the 600ms background transition
   that only existed to crossfade between lanes. */
.bgfx{
  position: fixed; inset: 0; z-index: -2; pointer-events: none;
  background:
    radial-gradient(900px 520px at 78% -6%, var(--glow-a), transparent 62%),
    radial-gradient(820px 540px at 6% 10%, var(--glow-b), transparent 62%),
    radial-gradient(760px 620px at 88% 62%, rgba(214,120,255,.20), transparent 64%),
    radial-gradient(900px 640px at 14% 88%, rgba(106,163,255,.20), transparent 64%),
    radial-gradient(680px 520px at 50% 42%, rgba(116,224,196,.13), transparent 66%),
    radial-gradient(560px 460px at 96% 100%, rgba(255,168,92,.15), transparent 64%),
    var(--bg);
}
.grid-field{
  position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: .85;
  background-image:
    linear-gradient(rgba(255,255,255,.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.055) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: radial-gradient(circle at 50% 34%, #000 0%, transparent 88%);
          mask-image: radial-gradient(circle at 50% 34%, #000 0%, transparent 88%);
}

/* section shell. See CONTRACT §3 for the markup convention. */
section{ padding: var(--sp-band) 0; position: relative; }
.sec-line{ border-top: var(--hair) solid var(--line); }
.sec-flush{ padding: 0; }        /* pinned / full-bleed sections opt out       */

/* section eyebrow */
.label{
  color: var(--accent-ink); font-family: var(--mono); font-size: 12.5px;
  letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-3);
}
.label::before{ content: ""; width: 22px; height: 1px; background: currentColor; opacity: .6; }

h2{ font-size: clamp(26px, 4.4vw, 40px); font-weight: 780; letter-spacing: -.8px; margin-bottom: var(--sp-4); }
h3{ font-size: 21px; letter-spacing: -.4px; }

.lead{ color: var(--muted); max-width: 660px; font-size: 18px; }
.lead b{ color: var(--text); font-weight: 600; }

/* buttons. --btn-fill / --btn-fg are zone tokens, so .btn.primary is a gradient
   on dark and a solid block on light with no per-section knowledge. */
.btn{
  padding: 13px 22px; border-radius: var(--r-2); font-size: 14.5px; font-weight: 600;
  border: var(--hair) solid var(--line2); transition: var(--t-base);
  display: inline-flex; align-items: center; gap: 9px; cursor: pointer;
  font-family: var(--sans); min-height: 44px;
}
.btn.primary{ background: var(--btn-fill); border: none; color: var(--btn-fg); }
.btn.primary:hover{ filter: brightness(1.07); transform: translateY(-1px); }
.btn.ghost:hover{ border-color: var(--accent-line); color: var(--accent-ink); }
.arrow{ transition: transform var(--t-base); }
.btn:hover .arrow{ transform: translateX(3px); }
.ctarow{ margin-top: var(--sp-7); display: flex; gap: var(--sp-3); flex-wrap: wrap; }

/* card + status grammar, kept from the audited page */
.card{
  background: var(--panel-fill); border: var(--hair) solid var(--line);
  border-radius: var(--r-4); padding: 26px; position: relative; overflow: hidden;
  transition: border-color var(--t-base), transform var(--t-base);
}
.card:hover{ border-color: var(--line2); }
.tag{
  font-family: var(--mono); font-size: 11px; letter-spacing: 1px; text-transform: uppercase;
  color: var(--accent-ink); border: var(--hair) solid var(--line2);
  border-radius: var(--r-pill); padding: 4px 11px;
}
.status{ font-family: var(--mono); font-size: 11px; display: inline-flex; align-items: center; gap: 7px; color: var(--muted); }
.status .d{ width: 7px; height: 7px; border-radius: 50%; }
.status.live .d{ background: var(--live);    box-shadow: 0 0 7px var(--live); }
.status.shipped .d{ background: var(--shipped); box-shadow: 0 0 7px var(--shipped); }
.status.building .d{ background: var(--build); box-shadow: 0 0 7px var(--build); }
.plink{
  margin-top: var(--sp-4); display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: 13px; color: var(--accent-ink); transition: gap var(--t-base);
}
.plink:hover{ gap: 11px; }

/* ---- the reveal primitive -------------------------------------------------
   THE HIDDEN STATE ONLY EXISTS UNDER html.motion-ready. That class is added by
   JS. A browser with JavaScript disabled never sees .rv's opacity:0, so nothing
   on this page can be held invisible by a script that failed to run. This is
   the single most load-bearing accessibility rule in the file; every section's
   own pre-reveal state must follow the same shape. */
html.motion-ready .rv{
  opacity: 0; transform: translateY(22px);
  transition: opacity var(--t-reveal) var(--ease), transform var(--t-reveal) var(--ease);
}
html.motion-ready .rv.in{ opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce){
  html.motion-ready .rv{ opacity: 1; transform: none; transition: none; }
}


/* ==========================================================================
   lib/motion.css
   ========================================================================== */
/* =============================================================================
   lib/motion.css: the shared motion layer for szczleon.com

   Four things live here and nothing else:
     1. Motion tokens and the shared keyframes.
     2. The .motion-ready gating rules. Every hidden pre-reveal state in the
        build is written under .motion-ready, so a JS-off browser never hides
        anything, and .motion-failsafe force-reveals if something stalls.
     3. The data-inview pause rules (one attribute flip pauses a subtree).
     4. ONE global prefers-reduced-motion block at the bottom that kills every
        animation and transition on the page and restores final values.

   Plus the mechanical CSS each lib module needs to function at all: reel
   clipping, line masks, sticky offsets, snap geometry, wire strokes. Mechanics
   only. Every colour, size, and radius is a custom property with a neutral
   default, so section CSS layers on top without fighting this file.

   Load order: glass.css, motion.css, then the page's own styles.
   ============================================================================= */

/* ---------------------------------------------------------------- 1. tokens */

:root {
  /* Interaction transitions cluster 0.12s to 0.22s with a 0.4s ceiling. */
  --sz-t-fast: 120ms;
  --sz-t-base: 180ms;
  --sz-t-slow: 220ms;
  --sz-t-max:  400ms;

  --sz-dur-rise: 460ms;
  --sz-dur-line: 620ms;
  --sz-dur-fill: 500ms;

  --sz-ease-out:  cubic-bezier(.22, .61, .36, 1);
  --sz-ease-soft: cubic-bezier(.16, 1, .3, 1);
  --sz-ease-osmo: cubic-bezier(.625, .05, 0, 1);

  --sz-stagger: 110ms;

  /* Hairlines. Wires must read darker than card borders: that contrast is what
     makes a static diagram look powered (Runlayer's whole mechanic). */
  --sz-line: rgba(255, 255, 255, .10);
  --sz-line-strong: rgba(255, 255, 255, .18);
  --sz-wire: rgba(255, 255, 255, .26);

  --sz-fill-ink: #f4f4f5;
  --sz-fill-on: #0a0a0a;

  --sz-bloom-c1: rgba(120, 170, 255, .30);
  --sz-bloom-c2: rgba(90, 220, 200, .20);
  --sz-bloom-c3: rgba(255, 190, 120, .14);
}

/* Section-scoped theme flip. One attribute on a <section> (or any inner div)
   inverts the band. The accent lives outside the pair so it survives both.
   Values are the measured Composio-style pair; section CSS may override any of
   them inside its own scope. */
[data-mode="dark"] {
  color-scheme: dark;
  --sz-bg: #0a0a0a;
  --sz-panel: #131313;
  --sz-panel-2: #1a1a1a;
  --sz-line: rgba(255, 255, 255, .10);
  --sz-line-strong: rgba(255, 255, 255, .18);
  --sz-wire: rgba(255, 255, 255, .26);
  --sz-txt: #f4f4f5;
  --sz-dim: #a1a1aa;
  --sz-btn-bg: #ffffff;
  --sz-btn-ink: #0a0a0a;
  --sz-fill-ink: #f4f4f5;
  --sz-fill-on: #0a0a0a;
}

[data-mode="light"] {
  color-scheme: light;
  --sz-bg: #f6f6f6;
  --sz-panel: #ffffff;
  --sz-panel-2: #efeff0;
  --sz-line: rgba(0, 0, 0, .09);
  --sz-line-strong: rgba(0, 0, 0, .18);
  --sz-wire: rgba(0, 0, 0, .28);
  --sz-txt: #0f0f0f;
  --sz-dim: #5c5c60;
  --sz-btn-bg: #0f0f0f;
  --sz-btn-ink: #ffffff;
  --sz-fill-ink: #0f0f0f;
  --sz-fill-on: #ffffff;
}

/* Visually hidden, still announced. Used by odometer.js so a screen reader
   hears "100/100" instead of twenty digits per column. */
.sz-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------ 2. keyframes */

@keyframes sz-rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

@keyframes sz-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0; }
}

@keyframes sz-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .18; }
}

/* ------------------------------------------- 3. the motion-ready gating layer */
/*
   Read every rule below as: this hidden state only exists once JS has proved it
   can un-hide it. Without .motion-ready none of these selectors match, so the
   page renders fully composed. With .motion-failsafe (8000ms, see core.js) the
   hidden states are forced open again even if a module threw.
*/

/* --- generic reveal ------------------------------------------------------- */
.motion-ready [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity var(--sz-dur-rise) var(--sz-ease-out) var(--sz-d, 0ms),
    transform var(--sz-dur-rise) var(--sz-ease-out) var(--sz-d, 0ms);
}
.motion-ready [data-reveal].is-revealed { opacity: 1; transform: none; }

/* --- cascade.js ----------------------------------------------------------- */
.motion-ready [data-cascade] > *,
.motion-ready [data-cascade] [data-cascade-item] {
  opacity: 0;
  transform: translateY(var(--sz-cascade-y, 25px));
  will-change: transform, opacity;
  transition:
    opacity var(--sz-dur-rise) var(--sz-ease-out) var(--sz-d, 0ms),
    transform var(--sz-dur-rise) var(--sz-ease-out) var(--sz-d, 0ms);
}
.motion-ready [data-cascade] > .is-in,
.motion-ready [data-cascade] [data-cascade-item].is-in {
  opacity: 1;
  transform: none;
}

/* --- linemask.js ---------------------------------------------------------- */
/* The wrapper has no fixed height, so a line that re-wraps on resize grows its
   own clip box instead of losing text. */
.sz-line {
  display: block;
  overflow: hidden;
  padding-bottom: .14em;
  margin-bottom: -.14em;
}
.sz-line-i { display: block; }
.motion-ready [data-linemask] .sz-line-i {
  transform: translateY(110%);
  transition: transform var(--sz-dur-line) var(--sz-ease-soft) var(--sz-d, 0ms);
}
.motion-ready [data-linemask].is-in .sz-line-i { transform: none; }

/* --- zoomwall.js ---------------------------------------------------------- */
[data-zoomwall] { position: relative; }
.sz-wall-track { position: relative; height: var(--sz-wall-track, 350vh); }
.sz-wall-pin {
  position: sticky; top: 0;
  height: 100vh; height: 100dvh;
  overflow: hidden;
  display: grid; place-items: center;
}
.sz-wall {
  width: var(--sz-wall-w, 1180px);
  max-width: none;
  transform-origin: 50% 46%;
  transform: scale(var(--sz-wall-s, 1));
  opacity: var(--sz-wall-o, 1);
  will-change: transform, opacity;
}
.sz-wall-payoff {
  position: absolute; inset: 0;
  display: grid; place-content: center; justify-items: center;
  text-align: center;
  opacity: var(--sz-wall-po, 1);
  pointer-events: var(--sz-wall-pe, auto);
}
/* Mobile: the wall overruns both edges so you begin inside the dock. */
@media (max-width: 720px) {
  .sz-wall { width: var(--sz-wall-w-sm, 140vw); }
}
/* Reduced motion and the no-JS path: no pin, static composed wall, payoff
   readable immediately. data-static is written by zoomwall.js; the :not()
   below covers JS-off, where the attribute never appears. */
[data-zoomwall][data-static="true"] .sz-wall-track { height: auto; }
[data-zoomwall][data-static="true"] .sz-wall-pin { position: static; height: auto; }
[data-zoomwall][data-static="true"] .sz-wall-payoff { position: static; opacity: 1; pointer-events: auto; }

/* --- cardstack.js --------------------------------------------------------- */
/* A sticky element is clamped by its containing block, so each card must be a
   direct child of the tall lane. Wrapping one in a per-card <section> kills it. */
.sz-cardwrap {
  position: sticky;
  top: calc(var(--sz-stack-top, 88px) + var(--sz-i, 0) * var(--sz-stack-step, 26px));
  z-index: var(--sz-i, 0);
}
.sz-sent { display: block; height: 1px; width: 100%; pointer-events: none; }
.sz-rail { position: sticky; top: var(--sz-rail-top, 96px); }
.sz-rail-row { transition: color var(--sz-t-slow) ease, border-color var(--sz-t-slow) ease; }
@media (max-width: 900px) {
  .sz-cardwrap { position: static; }
  .sz-rail { position: static; }
}

/* --- hairlinegrid.js ------------------------------------------------------ */
/* gap:1px over a rule-coloured wrapper draws every seam exactly once:
   N cells produce N+1 rules, never a doubled border, no card fill anywhere. */
.sz-grid {
  display: grid;
  gap: 1px;
  background: var(--sz-line);
  border: 1px solid var(--sz-line);
  border-radius: var(--sz-grid-radius, 14px);
  overflow: hidden;
  grid-template-columns: repeat(var(--sz-grid-cols, 1), minmax(0, 1fr));
}
.sz-cell { background: var(--sz-bg, #0a0a0a); min-width: 0; position: relative; }
.sz-demo { position: relative; overflow: hidden; }

/* --- videoindex.js -------------------------------------------------------- */
/* Frame 01 is in flow and visible: that is the no-JS rendering (poster shows,
   stage has a height). Once the module takes over it writes data-live="1" on
   the root and the data-on attribute drives visibility from there.

   The hidden state lives ONLY under [data-live="1"], which the module writes.
   It used to sit on the bare .sz-vi-frame rule as well, which meant a JS-off
   page depended on a consumer section out-specifying it to stay readable
   (#work .cm-frame happened to do that). Cascade luck is not a JS-off
   guarantee, so the ungated pair is gone: with no JS the frames stack, every
   poster and caption is present, and nothing is invisible. */
.sz-vi-stage { position: relative; }
.sz-vi-frame {
  position: absolute; inset: 0;
  transition: opacity var(--sz-t-slow) ease;
}
.sz-vi-frame:first-of-type { position: relative; }
[data-videoindex][data-live="1"] .sz-vi-frame { opacity: 0; visibility: hidden; }
[data-videoindex][data-live="1"] .sz-vi-frame[data-on="1"] { opacity: 1; visibility: visible; }
.sz-vi-poster { display: block; width: 100%; height: 100%; object-fit: cover; }
.sz-vi-frame video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}

/* --- snappeek.js ---------------------------------------------------------- */
.sz-rail-track {
  display: flex;
  gap: var(--sz-rail-gap, 14px);
  padding-inline: var(--sz-rail-edge, 20px);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--sz-rail-edge, 20px);
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.sz-rail-track::-webkit-scrollbar { display: none; }
.sz-rail-card { flex: 0 0 var(--sz-card-w, 78vw); scroll-snap-align: start; }

/* --- dome.js -------------------------------------------------------------- */
/* The flat gradient plate is the base layer and always ships. The canvas is the
   enhancement painted over it, so mobile, reduced motion, and a failed 2D
   context all land on something composed. */
.sz-dome-plate {
  position: absolute; inset: 0;
  background: var(--sz-dome-plate,
    radial-gradient(120% 70% at 50% 108%, rgba(62, 198, 255, .20) 0%, rgba(10, 30, 60, 0) 62%));
  pointer-events: none;
}
canvas[data-dome] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: var(--sz-dome-opacity, .92);
  pointer-events: none;
}

/* --- odometer.js ---------------------------------------------------------- */
.sz-od { display: inline-flex; align-items: baseline; font-variant-numeric: tabular-nums; }
.sz-od-reel {
  display: inline-block;
  overflow: hidden;
  height: 1em;
  line-height: 1;
  vertical-align: baseline;
}
.sz-od-strip {
  display: block;
  transform: translateY(calc(var(--sz-od-n, 0) * -5%));
  transition: transform var(--sz-od-dur, 1500ms) var(--sz-ease-soft) var(--sz-d, 0ms);
}
.sz-od-d { display: block; height: 1em; line-height: 1; }
.sz-od-static { display: inline-block; }

/* --- clock.js ------------------------------------------------------------- */
.sz-clock { font-variant-numeric: tabular-nums; }
.sz-clock-sep { animation: sz-blink 1s step-end infinite; }

/* --- wordswap.js ---------------------------------------------------------- */
/* Default state, and the mobile / reduced-motion / JS-off state: the NEW words
   are the ones that render. The old words only exist once the effect is live. */
.sz-swap { position: relative; display: inline-block; }
.sz-swap-old { display: none; }
.sz-swap-new { display: inline; }
.motion-ready [data-wordswap][data-swap-live="true"] {
  overflow: hidden;
  vertical-align: bottom;
}
.motion-ready [data-wordswap][data-swap-live="true"] .sz-swap-old {
  display: inline-block;
  position: absolute;
  left: 0; top: 0;
  white-space: nowrap;
  transform: translateY(calc(var(--sz-swap, 0) * -100%));
  opacity: calc(1 - var(--sz-swap, 0));
}
.motion-ready [data-wordswap][data-swap-live="true"] .sz-swap-new {
  display: inline-block;
  transform: translateY(calc((1 - var(--sz-swap, 0)) * 100%));
  opacity: var(--sz-swap, 0);
}

/* --- wires.js ------------------------------------------------------------- */
.sz-wire-layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  overflow: visible;
}
.sz-wire { fill: none; stroke: var(--sz-wire); stroke-width: 1; vector-effect: non-scaling-stroke; }

/* --- demostage.js --------------------------------------------------------- */
/* Without .motion-ready every scenario and every step is in the DOM and on
   screen: the completed transcripts, fully indexable. That IS the reduced-motion
   and no-JS rendering, not a separate fallback. */
.motion-ready [data-demostage] [data-scenario] { display: none; }
.motion-ready [data-demostage] [data-scenario][data-on="1"] { display: block; }
.motion-ready [data-demostage] [data-step] {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--sz-t-slow) var(--sz-ease-out), transform var(--sz-t-slow) var(--sz-ease-out);
}
.motion-ready [data-demostage] [data-step][data-on="1"] { opacity: 1; transform: none; }

/* --- hoverfill.js --------------------------------------------------------- */
[data-hoverfill] { position: relative; isolation: isolate; }
/* top/height overshoot swallows the row hairlines so the fill has no seam. */
.sz-fill {
  position: absolute;
  left: 0; top: -1px;
  width: 100%; height: calc(100% + 2px);
  background: var(--sz-fill-ink);
  z-index: -1;
  transform: translate(0, 110%);
  transition: transform var(--sz-dur-fill) var(--sz-ease-soft);
  pointer-events: none;
}
/* isolation:isolate makes the row a stacking context, so the z-index:-1 tile
   paints over the row's own background and under its text with no extra
   positioning on the content. */
[data-hoverfill] { transition: color var(--sz-t-slow) ease; }
[data-hoverfill][data-filled="1"] { color: var(--sz-fill-on); }

/* --- bloom.js ------------------------------------------------------------- */
[data-bloom] { position: relative; }
.sz-bloom {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: var(--sz-bloom-o, 0);
  transition: opacity var(--sz-t-max) ease;
  background:
    radial-gradient(var(--sz-bloom-r1, 220px) var(--sz-bloom-r1, 220px)
      at calc(50% + var(--sz-bloom-x1, 0px)) calc(50% + var(--sz-bloom-y1, 0px)), var(--sz-bloom-c1), transparent 70%),
    radial-gradient(var(--sz-bloom-r2, 300px) var(--sz-bloom-r2, 300px)
      at calc(50% + var(--sz-bloom-x2, 0px)) calc(50% + var(--sz-bloom-y2, 0px)), var(--sz-bloom-c2), transparent 70%),
    radial-gradient(var(--sz-bloom-r3, 380px) var(--sz-bloom-r3, 380px)
      at calc(50% + var(--sz-bloom-x3, 0px)) calc(50% + var(--sz-bloom-y3, 0px)), var(--sz-bloom-c3), transparent 70%);
  filter: blur(18px);
}
[data-bloom] > :not(.sz-bloom) { position: relative; z-index: 1; }

/* --- fitbox.js ------------------------------------------------------------ */
.sz-fitbox {
  display: flex;
  align-items: baseline;
  justify-content: center;
  column-gap: var(--sz-fitbox-gap, .08em);
  overflow: hidden;
  line-height: .82;
  font-size: var(--sz-fitbox-size, 16px);
}
.sz-fitbox-i { display: block; white-space: nowrap; }

/* ------------------------------------------------ 4. the data-inview pause gate */
/*
   One attribute flip pauses a whole subtree, with no rAF bookkeeping anywhere.
   core.js writes data-inview; this is the only place the pause is expressed.
*/
[data-inview="false"] :is(.sz-demo, .sz-loop, [data-loop]),
[data-inview="false"]:is(.sz-demo, .sz-loop, [data-loop]) {
  animation-play-state: paused;
}

/* The explicit stop, and the only one that reaches a keyboard. hairlinegrid.js
   writes data-frozen in two places: on a CELL when a no-hover device taps it,
   and on the whole GRID when the section's [data-loop-pause] button is pressed.
   The button is the WCAG 2.2.2 mechanism, because the hover freeze below is
   mouse-only and the :focus-within half of it cannot match: the cells are not
   focusable and hold nothing focusable. */
[data-frozen="1"] :is(.sz-demo, .sz-loop, [data-loop]) {
  animation-play-state: paused !important;
}

/* Pointing at a loop freezes it on the frame being read. The :focus-within
   selectors are kept for any consumer whose cells DO hold a focusable child;
   on this page nothing matches them, which is why the pause button exists. */
@media (hover: hover) and (pointer: fine) {
  .sz-cell:hover .sz-demo,
  .sz-cell:hover .sz-loop,
  .sz-cell:hover [data-loop],
  .sz-cell:focus-within .sz-demo,
  .sz-cell:focus-within .sz-loop,
  .sz-cell:focus-within [data-loop] {
    animation-play-state: paused !important;
  }
}

/* -------------------------------------------------------- 5. the 8000ms failsafe */
/*
   This is a FAILURE path, not a timeout on the choreography. A page that boots
   calls SZ.bootOk() and disarms the clock, so these rules never land on a
   healthy page and can be as blunt as they need to be. When a module throws, a
   script 404s, or a slow network holds the bundle, .motion-failsafe arrives on
   <html> (at 8000ms, or immediately on an uncaught error) and every gated
   element is forced back to its final value.

   Nothing on this page can end up permanently invisible.
*/
.motion-failsafe [data-reveal],
.motion-failsafe [data-cascade] > *,
.motion-failsafe [data-cascade] [data-cascade-item],
.motion-failsafe [data-linemask] .sz-line-i,
.motion-failsafe [data-demostage] [data-step] {
  opacity: 1 !important;
  transform: none !important;
}
.motion-failsafe [data-demostage] [data-scenario] { display: block; }
.motion-failsafe .sz-wall { opacity: 1 !important; }
.motion-failsafe .sz-wall-payoff { opacity: 1 !important; }

/* The force-reveal above only covers elements the choreography holds at
   opacity 0 with a transition. A looping keyframe whose 0% frame is opacity 0
   (the #lab surface-in labels are the measured case) is held at frame zero by
   the data-inview pause gate above, so on the failure path it would stay
   invisible for good. Nothing on this page can end up permanently invisible,
   so the failsafe un-pauses the gate as well: every loop on the page holds a
   readable final frame. */
.motion-failsafe :is(.sz-demo, .sz-loop, [data-loop]) {
  animation-play-state: running !important;
  /* One pass, then hold. Every cell keyframe is authored so that 100% IS the
     readable final frame (that contract is stated in lib/hairlinegrid.js and in
     the #lab stylesheet), so running once with fill-mode forwards lands the
     whole field on its final frame permanently instead of cycling back through
     opacity 0 every few seconds. */
  animation-iteration-count: 1 !important;
  animation-fill-mode: forwards !important;
}

/* ------------------------------------------ 6. ONE prefers-reduced-motion block */
/*
   Duration alone is not enough: a staggered transition-delay would still hold an
   element at opacity 0 for a third of a second. The motion is cut outright, and
   every final value is restored explicitly. Verified target: zero running
   animations, nothing at opacity 0, layout unchanged.
*/
@media (prefers-reduced-motion: reduce) {

  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  /* Pre-reveal states never arm (core.js declines to add .motion-ready), but
     these are declared anyway so a manually-set class cannot hide content. */
  .motion-ready [data-reveal],
  .motion-ready [data-cascade] > *,
  .motion-ready [data-cascade] [data-cascade-item],
  .motion-ready [data-demostage] [data-step] {
    opacity: 1 !important;
    transform: none !important;
  }
  .motion-ready [data-linemask] .sz-line-i { transform: none !important; }
  .sz-line { overflow: visible; }

  /* The demo stage renders every scenario's completed transcript. */
  .motion-ready [data-demostage] [data-scenario] { display: block !important; }

  /* The wall renders as a static composed grid with the payoff visible. */
  .sz-wall { transform: none !important; opacity: 1 !important; }
  .sz-wall-track { height: auto !important; }
  .sz-wall-pin { position: static !important; height: auto !important; }
  .sz-wall-payoff { position: static !important; opacity: 1 !important; pointer-events: auto !important; }

  /* The word swap shows its second line only. */
  .sz-swap-old { display: none !important; }
  .sz-swap-new { display: inline !important; transform: none !important; opacity: 1 !important; }

  /* Odometers and count-ups keep their authored text: the JS never runs. */
  .sz-od-strip { transform: none !important; }

  /* Video is display:none with the poster underneath keeping the layout. Add
     data-motion-video to any other decorative video to get the same treatment. */
  .sz-vi-frame video,
  video[data-motion-video] { display: none !important; }

  /* Cursor-adjacent and hover effects render at rest. */
  .sz-bloom { display: none !important; }
  .sz-fill { display: none !important; }

  /* The clock keeps ticking (a text update is not an animation); only the
     colon's blink is cut, by the global rule above. */
}


/* ==========================================================================
   core/nav.css
   ========================================================================== */
/* ============================================================================
   nav.css: the persistent glass capsule, relabelled
   ----------------------------------------------------------------------------
   OWNER: shell agent. Loaded after tokens.css, before every section stylesheet.

   Everything in here is scoped to .nv, .nv-*, or .skip. Nothing leaks.

   The surface (blur, tint, hairline, rim) comes from glass.css via
   .dk-header.dk-glass.dk-glass-bar. Only layout, stacking, and the four MEASURED
   overrides below are declared here. A background or backdrop-filter written
   casually on this element loads after glass.css and silently clobbers the
   material, so do not add one.
   ========================================================================== */


/* ---- skip link -------------------------------------------------------------
   Ships with the nav because it must be the first focusable thing in the
   document. Target is #main on the page's <main> element. */
.skip{
  position: absolute; left: -999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--on-accent);
  padding: 10px 16px; border-radius: var(--r-1); font-weight: 600;
}
.skip:focus{ left: 16px; top: 16px; }


/* ---- the capsule -----------------------------------------------------------
   position:fixed rather than sticky: a sticky element still occupies its row in
   flow, which would leave a nav-height gap above the hero. */
.nv{
  position: fixed; top: 12px; left: 50%; transform: translateX(-50%);
  width: min(1180px, calc(100% - 24px)); z-index: 50;
}
.nv-wrap{
  display: flex; align-items: center; gap: var(--sp-5);
  height: 58px; padding: 0 10px 0 20px;
}

/* THE BAR IS GONE, and that is the point: Liquid Glass belongs on the floating
   navigation layer, and a full-bleed bar welded to the top edge is the one shape
   that gives the material nothing to float above. Three consequences of
   detaching it, all handled here:
     · glass.css's --glass-r:0 and bottom hairline are undone
     · the specular rim, which the full-bleed recipe explicitly removes, is put
       back. On a detached shape the rim IS the edge.
     · the cast shadow returns, because it is now genuinely floating.
   Specificity note: (0,2,1) beats glass.css's (0,2,0) recipe and this file loads
   after it, so both tiebreakers point the same way. */
nav.nv.dk-header.dk-glass{
  --glass-r: 999px;
  --glass-cast: .46;
  --glass-rim-hi: .72;
  border-radius: 999px;
  border-bottom: 0;
}
nav.nv.dk-header.dk-glass::before{ content: ""; }   /* the rim comes back */

nav.nv.dk-glass{
  /* ---- MEASURED override of the .dk-glass-bar tier, for this page only ----
     glass.css ships the bar tier at a heavier fill. Two measurements moved it:
       (a) refraction: a heavy fill crushes the one backdrop this page actually
           has (the hero morph video) to a near-monotone smear;
       (b) the no-rasterise degenerate case: if the filter is dropped without
           @supports firing, the fill alone decides legibility.
     Retuned 2026-08-08 against glass.css §13, which moves the legibility work to
     the clamp so the fill can drop hard: backdrop energy (1-a)·b goes .198 to
     .264, i.e. a third more of the hero survives the bar. That is the whole
     point, since this is the one surface on the page with something behind it.
     Case (b) is met by pinning the link colour rather than by the fill.
     Literals, not var()s: see the §3 note in glass.css (Safari 18.3 / IACVT). */
  background-color: color-mix(in srgb, var(--glass-base) 26%, transparent);
  -webkit-backdrop-filter: blur(24px) saturate(150%) brightness(.36);
          backdrop-filter: blur(24px) saturate(150%) brightness(.36);

  /* ⚠ VERIFIED BUG, do not add background-color back to this transition list.
     glass.css §2 transitions background-color for hover fills. On this element
     the fill is a color-mix() over --glass-base AND the element is composited
     (it owns a backdrop-filter). In Chromium 148 that combination freezes: the
     computed background-color sticks at a stale oklab() literal forever even
     though --glass-base re-resolves correctly. Nothing is lost; the nav has no
     hover fill. */
  transition: box-shadow var(--t-slow) ease;
}


/* ---- brand ---------------------------------------------------------------- */
.nv-brand{
  display: flex; align-items: center; gap: 9px;
  font-weight: 800; letter-spacing: .4px; font-size: 18px;
  color: var(--text); flex: 0 0 auto; min-height: 44px;
}
.nv-dot{
  width: 9px; height: 9px; border-radius: 50%; background: var(--live);
  box-shadow: 0 0 0 0 rgba(70,210,122,.6); animation: nv-pulse 2.6s infinite;
  flex: 0 0 auto;
}
@keyframes nv-pulse{
  0%   { box-shadow: 0 0 0 0 rgba(70,210,122,.55); }
  70%  { box-shadow: 0 0 0 7px rgba(70,210,122,0); }
  100% { box-shadow: 0 0 0 0 rgba(70,210,122,0); }
}
.nv-mark{ display: none; }          /* mobile only */
.nv-name{ display: inline; }


/* ---- links -----------------------------------------------------------------
   ⚠ CONTRAST FIX, carried forward. The bar tier is NOT --muted-safe on this
   page. glass.css §3 quotes #9aa1ad at 4.69:1 from a worst-case surface, but
   that figure is --glass-ink alone; the 10% accent tint lifts G/B and costs
   about 0.9 of the ratio. Measured composite over white: 3.8:1, and the real
   page reaches 4.41:1 under the bright end of the hero photo. Both are below AA
   for 14px/400. Hierarchy therefore moves to size and weight, and the links take
   --text. Hover cannot be colour-only either, because --accent measures 3.68:1
   on this surface, so hover is an underline. */
.nv-links{
  display: flex; align-items: center; gap: 28px;
  font-size: 14px; color: var(--text); margin-left: auto;
}
.nv-links a{
  transition: color var(--t-fast);
  display: inline-flex; align-items: center; min-height: 44px;
}
.nv-links a:hover{
  text-decoration: underline; text-underline-offset: 5px; text-decoration-thickness: 1px;
}
.nv-l-short{ display: none; }


/* ---- the CV chip -----------------------------------------------------------
   Lives INSIDE the nav's glass. glass.css §5 forbids glass-on-glass and it is
   physically pointless anyway (a nested blur samples only its parent's paint),
   so this stays what Apple prescribes for a nested control: a fill plus a
   hairline. */
.nv-cta{
  flex: 0 0 auto;
  border: var(--hair) solid var(--line2);
  background: rgba(255,255,255,.05);
  padding: 8px 14px; border-radius: 9px; color: var(--text);
  font-size: 14px; font-weight: 600;
  display: inline-flex; align-items: center; min-height: 40px;
  transition: border-color var(--t-base), color var(--t-base);
  white-space: nowrap;
}
.nv-cta:hover{ border-color: var(--accent); color: var(--accent); }


/* ============================================================================
   MOBILE LADDER: hamburger-free, by design
   ----------------------------------------------------------------------------
   The documented anti-pattern is a mobile header that drops the primary CTA into
   a drawer. Ours never does. The ladder, in order:
     1. sit flush at the top edge (the capsule becomes a bar, radius 0, no cast)
     2. drop the wordmark, keep the mark
     3. swap to the pre-authored short labels
     4. every target is at least 44px
     5. the CV chip stays visible at every width
   Trigger is viewport width only. No JS, no state, no drawer to get stuck open.
   ========================================================================== */
@media (max-width: 760px){

  .nv{ top: 0; left: 0; right: 0; transform: none; width: 100%; }

  nav.nv.dk-header.dk-glass{
    --glass-r: 0;
    --glass-cast: 0;
    border-radius: 0;
  }

  .nv-wrap{
    height: auto; flex-wrap: wrap; gap: 0;
    padding: 0 var(--gutter);
    max-width: none;
  }

  .nv-brand{ height: 48px; font-size: 16px; }
  .nv-name{ display: none; }
  .nv-mark{ display: inline; letter-spacing: 1px; }

  /* 44px tall, inside the 48px brand row. The chip is a touch target on this
     breakpoint, so it takes the same minimum as the link ladder. */
  .nv-cta{
    margin-left: auto; align-self: center;
    padding: 7px 12px; font-size: 13px; min-height: 44px;
  }

  /* row two: the destination ladder. flex:1 on each link makes six equal
     44px-tall targets. At 360px that is 57px per target, which clears the
     minimum with the short labels. Nothing scrolls, nothing collapses. */
  .nv-links{
    order: 3; flex: 0 0 100%; width: 100%;
    margin-left: 0; gap: 0;
    height: 46px;
    border-top: var(--hair) solid var(--line);
    justify-content: space-between;
  }
  .nv-links a{
    flex: 1 1 0; justify-content: center;
    min-width: 44px; min-height: 44px;
    font-size: 12.5px; letter-spacing: .2px;
  }
  .nv-l-full{ display: none; }
  .nv-l-short{ display: inline; }
}


/* ---- reduced motion --------------------------------------------------------
   The dot stops pulsing. It stays green and it stays visible: the claim is
   still made, it just stops moving. */
@media (prefers-reduced-motion: reduce){
  .nv-dot{ animation: none; }
}


/* ==========================================================================
   sections/hero.css
   ========================================================================== */
/* ============================================================================
   #hero: the morph, re-anchored
   ----------------------------------------------------------------------------
   Every rule is scoped under #hero. Keyframe names would be prefixed hr- if this
   section had any: it has none. The only motion here is the cascade (lib) and the
   scrub (hero.js, which seeks a video and animates nothing).

   THE PIN IS GATED ON html.motion-ready. That class only lands when JS ran and
   motion is allowed, so a JS-off browser and a reduced-motion reader both get a
   normal-height hero instead of 100vh of scroll that holds still and does
   nothing. Reduced motion never gets the class (core.js declines), so the block
   at the bottom of this file is belt and braces, not the mechanism.
   ========================================================================== */

/* ---------------------------------------------------------------- 1. the pin */

#hero .hr-pin{ position: relative; }

#hero .hr-stage{
  position: relative;          /* containing block for the video and the scrim */
  isolation: isolate;
  display: flex;
  align-items: center;
  min-height: min(760px, 90vh);
  padding: calc(var(--nav-h) + var(--sp-7)) 0 var(--sp-9);
  overflow: hidden;
}

/* Desktop, motion allowed: one screen of scroll drives the whole morph.
   200vh of track, 100vh of it pinned. lib core's 'pin' progress mode reads
   exactly this geometry (0 when the track's top hits the viewport top, 1 when
   its bottom hits the bottom), which is why hero.js does no maths of its own. */
@media (min-width: 641px){
  html.motion-ready #hero .hr-pin{ height: 200vh; }
  html.motion-ready #hero .hr-stage{
    position: sticky; top: 0; height: 100vh; min-height: 100vh;
  }
}

/* ------------------------------------------------------- 2. the portrait layer */

/* One element carries both states. With no src (phone, reduced motion, JS off)
   it renders its poster and downloads nothing; with a src it is the scrubbed
   morph. Right-anchored and un-mirrored so the face looks toward the copy. */
#hero .hr-media{
  position: absolute; inset: 0 0 0 auto; z-index: 0;
  width: 50%; height: 100%;
  object-fit: cover; object-position: 35% center;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 14%);
          mask-image: linear-gradient(90deg, transparent 0%, #000 14%);
}

/* The scrim. Flat page floor under the copy column, falling off to nothing over
   the portrait, plus a top wash so the nav capsule never sits on a bright face. */
#hero .hr-stage::after{
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(90deg, var(--bg) 0%, var(--bg) 34%, rgba(7,8,10,.4) 46%, rgba(7,8,10,0) 60%),
    linear-gradient(0deg,  var(--bg) 0%, rgba(7,8,10,0) 24%),
    linear-gradient(180deg, rgba(7,8,10,.5) 0%, rgba(7,8,10,0) 15%);
}

#hero .hr-wrap{ position: relative; z-index: 2; width: 100%; }

/* --------------------------------------------------------------- 3. the copy */

/* Fill and rim come from .dk-glass-faux.dk-glass-capsule: the no-blur tier, so
   this costs nothing to composite and the page's one real glass surface stays
   the nav. Ink is --text, not --muted: measured, --muted fails on glass. */
#hero .hr-status{
  display: inline-flex; align-items: center; gap: var(--sp-2) 10px;
  flex-wrap: wrap;
  font-family: var(--mono); font-size: 12px; color: var(--text);
  padding: 7px 13px; margin-bottom: var(--sp-5);
}
#hero .hr-dot{
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--live); box-shadow: 0 0 8px var(--live);
}
#hero .sz-clock{ letter-spacing: .2px; }
/* clock.js blinks the separator, which is the right heartbeat for a clock that
   shows hours and minutes only. This one shows seconds, so there are TWO
   separators and they blink in step: for half of every second the time reads
   "03 53 06" and looks broken. The ticking seconds digit is the liveness proof
   here, so the blink is switched off, which also leaves the hero band with zero
   running animations at rest. Higher specificity, so no !important. */
#hero .sz-clock-sep{ animation: none; }

#hero .hr-h1{
  font-size: clamp(32px, 5.2vw, 56px);
  line-height: 1.06;
  font-weight: 800;
  letter-spacing: -1.4px;
  max-width: 18ch;
}
/* The second sentence takes its own line. Two beats, two blocks, and the
   gradient runs the length of the payoff instead of being cut in half by a
   line break landing mid-phrase. */
#hero .hr-h1 em{ display: block; }

/* Fallback first, then the gradient. Without the guard an engine that lacks
   background-clip:text renders transparent type, which is invisible copy. */
#hero .hr-h1 em{ font-style: normal; color: var(--accent-ink); }
@supports ((-webkit-background-clip: text) or (background-clip: text)){
  #hero .hr-h1 em{
    background: linear-gradient(100deg, var(--accent), var(--accent2));
    -webkit-background-clip: text; background-clip: text;
    color: transparent;
  }
}

#hero .hr-count{
  margin-top: var(--sp-4);
  font-family: var(--mono); font-size: 13px; letter-spacing: .2px;
  color: var(--muted);
  max-width: 44ch;
}
#hero .hr-count b{ color: var(--text); font-weight: 600; }
/* The break is authored, not left to the wrap: it lands on the sentence
   boundary at every viewport instead of mid-phrase. */
#hero .hr-count span{ display: block; }

#hero .hr-sub{
  margin-top: var(--sp-4);
  font-size: 18px;
  max-width: 46ch;             /* stays clear of the portrait's left edge */
}

#hero .hr-ctas{ margin-top: var(--sp-6); align-items: center; }
/* .arrow's shared hover nudge is horizontal; this one points down. */
#hero .hr-ctas .btn:hover .arrow{ transform: translateY(2px); }

/* Two by two, not a ragged wrap. Four labels laid out as a grid read as a
   deliberate answer to four questions; the same four reflowed by flex read as
   an accident, with one label orphaned onto its own line. */
#hero .hr-micro{
  list-style: none;
  margin-top: var(--sp-5);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, max-content));
  gap: var(--sp-2) var(--sp-6);
  font-family: var(--mono); font-size: 12px; letter-spacing: .3px;
  color: var(--muted);
}
#hero .hr-micro li{ display: inline-flex; align-items: center; gap: var(--sp-2); }
#hero .hr-micro li::before{
  content: ""; width: 4px; height: 4px; border-radius: 50%; flex: none;
  background: currentColor; opacity: .55;
}

/* Below 900px the portrait takes 58% of the width and its soft edge reaches
   further left, so a second column of 12px mono would end up sitting on the
   face. One column keeps every label on the flat part of the scrim. */
@media (max-width: 900px){
  #hero .hr-micro{ grid-template-columns: minmax(0, max-content); }
}

/* ------------------------------------------------------------- 4. narrow */

/* Not a squeezed desktop: the portrait goes full-bleed and drops behind the
   copy instead of sitting beside it, the scrim turns vertical, and the pin
   never applies (the media query above starts at 641px). */
@media (max-width: 640px){
  #hero .hr-stage{
    min-height: auto;
    padding: calc(var(--nav-h) + var(--sp-6)) 0 var(--sp-8);
  }
  #hero .hr-media{
    inset: 0; width: 100%; object-position: 45% center; opacity: .3;
    -webkit-mask-image: none; mask-image: none;
  }
  #hero .hr-stage::after{
    background:
      linear-gradient(180deg, rgba(7,8,10,.32) 0%, rgba(7,8,10,.7) 52%, var(--bg) 100%),
      linear-gradient(90deg, rgba(7,8,10,.6), rgba(7,8,10,.2));
  }
  #hero .hr-h1{ letter-spacing: -.9px; max-width: none; }
  #hero .hr-count, #hero .hr-sub{ max-width: none; }
  #hero .hr-ctas{ gap: var(--sp-3); }
  #hero .hr-ctas .btn{ flex: 1 1 auto; justify-content: center; }
  /* One column: at 390px a second column would either truncate a label or
     force a font size nobody can read. */
  #hero .hr-micro{ grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); font-size: 11.5px; }
}

@media (max-width: 1024px) and (min-width: 641px){
  #hero .hr-media{ width: 58%; object-position: 40% center; }
  #hero .hr-stage::after{
    background:
      linear-gradient(90deg, var(--bg) 0%, var(--bg) 28%, rgba(7,8,10,.4) 44%, rgba(7,8,10,0) 64%),
      linear-gradient(0deg,  var(--bg) 2%, rgba(7,8,10,0) 30%),
      linear-gradient(180deg, rgba(7,8,10,.5) 0%, rgba(7,8,10,0) 15%);
  }
}

/* ------------------------------------------------- 5. reduced motion kill block */

/* Zero running animation and final rendered values. The pin collapses so the
   section is a normal-height hero, the portrait stays as its poster (hero.js
   returns before it ever assigns a src), and the clock's colon stops blinking
   while the clock itself keeps ticking: a text update is not an animation, and
   a frozen clock would be a lie. */
@media (prefers-reduced-motion: reduce){
  #hero *, #hero *::before, #hero *::after{
    animation: none !important;
    transition: none !important;
  }
  #hero .hr-pin{ height: auto !important; }
  /* No min-height restated here on purpose: the narrow-viewport rule above sets
     min-height:auto and an !important here would force a 760px hero onto a phone. */
  #hero .hr-stage{ position: relative !important; height: auto !important; }
  html.motion-ready #hero .hr-copy > *{ opacity: 1 !important; transform: none !important; }
}


/* ==========================================================================
   sections/wall.css
   ========================================================================== */
/* ============================================================================
   #wall :: everything that's running
   ----------------------------------------------------------------------------
   Prefix wl-. Every rule below is scoped under #wall, keyframes included.

   GEOMETRY, because it is not obvious from the markup.

   The rows are self-justifying. Each <figure> carries one custom property,
   --wl-r, which is the crop's true width/height ratio out of wall-manifest.json.
   That single number drives two declarations:

       flex-grow: var(--wl-r);  flex-basis: 0;   ->  width  = r / SUM(r) * W
       aspect-ratio: var(--wl-r);                ->  height = width / r

   so every tile in a row lands at the SAME height, W / SUM(r), at its own true
   aspect. Three things fall out of that and all three are load-bearing:

     1. The rows never read as a grid. Row heights differ because the rows hold
        different numbers of tiles with different ratio sums (measured: 63px to
        113px at a 1180px wall), and no tile is ever distorted.
     2. The wall scales to any width, so --wl-w can be clamped against viewport
        HEIGHT. The whole composed wall then always fits inside the 100vh pin,
        which is what makes the phase-A rest beat land.
     3. Hiding a tile re-justifies its row for free. That is what makes the
        phone field a different composition rather than a squeezed desktop one.

   Measured at a 1180px wall: 8 rows, 63 slots, 717px tall (ratio 0.608).
   Measured at a 593px wall with only [data-sm] visible: 5 rows, 18 slots,
   602px tall (ratio 1.015). Those two ratios are where 126vh and 74vh below
   come from: 0.608 x 1.26 = 0.77 and 1.015 x 0.74 = 0.75 of the viewport.

   Motion is entirely lib/zoomwall.js. This file adds no scroll listener, no
   observer, and no animation. The section ships no JS of its own.
   ========================================================================== */

/* ---- header band ------------------------------------------------------- */
#wall .wl-head{ padding: var(--sp-band) 0 var(--sp-7); }
#wall .wl-meta{
  margin-top: var(--sp-5); color: var(--faint);
  font-size: 12.5px; letter-spacing: .06em;
}
#wall .wl-note{
  margin-top: var(--sp-2); color: var(--faint);
  font-size: 13px; max-width: 560px; line-height: 1.55;
}

/* ---- the wall ---------------------------------------------------------- */
/* --wl-w is the one size knob. --sz-wall-w / --sz-wall-w-sm are what
   motion.css reads; --wl-gap is derived from the width so the justified maths
   holds at every size (10px at 1180 is 0.85% of the wall). */
#wall .sz-wall{
  --wl-w: min(1180px, 126vh, 116vw);
  --sz-wall-w: var(--wl-w);
  --wl-gap: calc(var(--wl-w) * .0085);
}

#wall .wl-row{
  display: flex;
  align-items: flex-start;
  gap: var(--wl-gap);
  margin-bottom: var(--wl-gap);
}
#wall .wl-row:last-child{ margin-bottom: 0; }
/* two rows overrun the wall and two sit inside it, so the field has a ragged
   edge instead of a printed rectangle */
#wall .wl-row-wide{ width: 106%; margin-left: -3%; }
#wall .wl-row-narrow{ width: 92%; margin-left: 4%; }

#wall .wl-tile{
  flex-grow: var(--wl-r);
  flex-shrink: 1;
  flex-basis: 0;
  min-width: 0;                    /* without this, min-width:auto floors the
                                      tile at the image's intrinsic width and
                                      the row stops justifying */
  aspect-ratio: var(--wl-r);
  margin: 0;
  overflow: hidden;
  border: var(--hair) solid var(--line);
  border-radius: 4px;
  background: var(--panel2);
}
#wall .wl-tile img{
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 0;   /* screenshots read from the top */
}

/* ---- staged tile loading ------------------------------------------------ */
/* The 60 tiles all carry loading="lazy", width/height and fetchpriority="low",
   and none of that defers anything here. Measured cold at 390px: all 15 mobile
   tiles (723KB, 52% of the whole first load) were requested within 2ms of each
   other during HTML parse, at scrollY 0, with no scroll and no interaction. The
   cause is distance, not a missing attribute. The wall is transformed to
   scale 2.6 at rest, so its box starts 794px down a 26,336px document while the
   viewport is 844px tall, and that sits inside Chrome's lazy-load fetch margin.

   content-visibility: hidden skips the tile's CONTENTS, so the <img> has no
   layout box and the lazy-loader has nothing to fetch. The tile itself keeps
   its geometry exactly, because its size comes from aspect-ratio and flex-grow
   above and never from the image, so releasing the hold cannot reflow the wall.

   Three gates keep this from ever hiding anything:
     · html[data-sz-js] is written pre-paint by _src/core/inline-head-js.js, so
       the hold does not exist at all with JS off.
     · zoomwall.js removes data-hold when the track comes within 150px of the
       viewport, and removes it immediately on its static path (reduced motion,
       no IntersectionObserver, no rAF).
     · the 8000ms failsafe and the reduced-motion block below force it back,
       for the case where the bundle never arrives to do either. */
html[data-sz-js] #wall .wl-tile[data-hold]{ content-visibility: hidden; }
html.motion-failsafe #wall .wl-tile[data-hold]{ content-visibility: visible; }
@media (prefers-reduced-motion: reduce){
  html[data-sz-js] #wall .wl-tile[data-hold]{ content-visibility: visible; }
}

/* ---- reserved slots ---------------------------------------------------- */
/* A build with no capture yet: dashed frame, build name, mono status. The
   manifest alt string still ships, in .sr-only, so the wall stays a complete
   index of the inventory even where the picture is missing.

   Two build notes:

   1. The label sizes in cqmin, not cqh. A tall slot is narrow (measured 42px
      wide at a 1134px wall), and sizing off its height alone broke the longest
      name across two lines. The px value above each clamp is the fallback
      where container query units are unsupported.
   2. No padding on the tile itself. Percentage padding on a flex item with
      flex-basis 0 becomes a minimum main size, which handed the reserved tiles
      a fatter share of the row than their ratio and broke the justification
      (measured: 260px where 165px was owed). The inset lives on the labels. */
#wall .wl-reserved{
  container-type: size;
  background: none;
  border-style: dashed;
  border-color: var(--line2);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  justify-items: center;
  gap: 2px;
  text-align: center;
}
#wall .wl-res-name,
#wall .wl-res-note{ max-width: 90%; overflow-wrap: anywhere; }
#wall .wl-res-name{
  color: var(--text); font-weight: 640; line-height: 1.12;
  font-size: 9px;
  font-size: clamp(6px, 13cqmin, 13px);
}
#wall .wl-res-note{
  color: var(--faint); text-transform: uppercase; letter-spacing: .1em;
  font-size: 7px;
  font-size: clamp(4.5px, 8.5cqmin, 9px);
}

/* ---- payoff card ------------------------------------------------------- */
/* The wall is still at part opacity when this arrives, so the layer paints its
   own floor under the type. Opacity and pointer-events are zoomwall.js's. */
#wall .sz-wall-payoff{
  padding: 0 var(--gutter);
  background: radial-gradient(58% 54% at 50% 50%,
              var(--bg) 0%, var(--bg) 44%, transparent 100%);
}
#wall .wl-payoff{
  display: grid; justify-items: center;
  max-width: 620px; text-align: center;
}
#wall .wl-count{
  font-size: clamp(30px, 6vw, 58px);
  font-weight: 800; letter-spacing: -1.6px; line-height: 1.04;
}
#wall .wl-law{
  margin-top: var(--sp-4);
  font-size: clamp(15px, 2.2vw, 19px); color: var(--muted);
}
#wall .wl-pill{
  margin-top: var(--sp-6);
  display: inline-flex; align-items: center; gap: 9px;
  min-height: 44px; padding: 13px 26px;
  border-radius: var(--r-pill);
  background: var(--text); color: var(--bg);
  font-size: 14.5px; font-weight: 650;
  transition: transform var(--t-base) var(--ease), filter var(--t-base) var(--ease);
}
#wall .wl-pill:hover{ transform: translateY(-1px); filter: brightness(.93); }
#wall .wl-arrow{ transition: transform var(--t-base) var(--ease); }
#wall .wl-pill:hover .wl-arrow{ transform: translateX(3px); }
#wall .wl-fine{
  margin-top: var(--sp-7);
  max-width: 560px;
  font-size: 11.5px; line-height: 1.65; color: var(--faint);
}
#wall .wl-credit{
  margin-top: var(--sp-3);
  font-size: 11px; color: var(--faint);
}

/* ---- the phone field: its own composition, not a squeeze --------------- */
/* Under 720px the wall overruns both viewport edges, so the visitor opens
   inside the dock, and only [data-sm] survives: 5 rows of 3 to 4, 15 crops
   (719KB) plus the 3 reserved slots, against 8 rows of 7 to 9 on desktop.
   Measured at 390x844: wall 570x589, tiles 99px to 134px tall, which is a
   readable screen on a phone. The same 60 tiles squeezed into that width would
   be 41px tall.
   The other 45 tiles are display:none, which is also the byte story: a lazy
   image in a display:none subtree never intersects, so it is normally never
   requested. Worst case, if a browser fetches them anyway, the phone lands on
   the full 2.58MB wall, which is still inside the 3MB section ceiling. */
@media (max-width: 720px){
  #wall .sz-wall{
    --wl-w: max(112vw, min(152vw, 74vh));
    --sz-wall-w-sm: var(--wl-w);
    --wl-gap: calc(var(--wl-w) * .0084);
  }
  #wall .wl-row:not([data-sm]),
  #wall .wl-tile:not([data-sm]){ display: none; }
  #wall .wl-row-wide{ width: 106%; margin-left: -3%; }
  #wall .wl-row-narrow{ width: 94%; margin-left: 3%; }
  #wall .wl-count{ letter-spacing: -1px; }
  #wall .wl-pill{ width: 100%; justify-content: center; }
  #wall .wl-fine{ margin-top: var(--sp-6); }
}

/* ---- no JS, stalled JS, reduced motion: no pin, composed wall ---------- */
/* zoomwall.js writes data-static="false" when it takes the section over, so
   :not([data-static="false"]) is every path where nothing is driving the pin:
   JS off, IntersectionObserver missing, reduced motion (data-static="true"),
   and the moment before init runs. Without this the JS-off page would hold a
   350vh sticky pin with the payoff card sitting on top of the wall.
   motion.css carries the same relief for data-static="true"; this is the
   version that also covers a browser that never ran the script. */
#wall:not([data-static="false"]) .sz-wall-track,
html.motion-failsafe #wall .sz-wall-track{ height: auto; }

#wall:not([data-static="false"]) .sz-wall-pin,
html.motion-failsafe #wall .sz-wall-pin{
  position: static; height: auto;
  padding-bottom: var(--sp-band);
}
#wall:not([data-static="false"]) .sz-wall-payoff,
html.motion-failsafe #wall .sz-wall-payoff{
  position: static; opacity: 1; pointer-events: auto;
  background: none;
  padding-top: var(--sp-9);
}
/* unpinned there is no 100vh box to fit, so the wall is bounded by the column
   instead of by viewport height */
#wall:not([data-static="false"]) .sz-wall,
html.motion-failsafe #wall .sz-wall{ --wl-w: min(1180px, 100%); }

@media (max-width: 720px){
  #wall:not([data-static="false"]) .sz-wall,
  html.motion-failsafe #wall .sz-wall{ --wl-w: 100%; }
}

/* ---- reduced motion ---------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  #wall *, #wall *::before, #wall *::after{
    animation: none !important;
    transition: none !important;
  }
  /* header cascade and wall both render their final frame */
  html.motion-ready #wall [data-cascade] > *{ opacity: 1 !important; transform: none !important; }
  #wall .sz-wall{ transform: none !important; opacity: 1 !important; --wl-w: min(1180px, 100%); }
  #wall .sz-wall-track{ height: auto !important; }
  #wall .sz-wall-pin{ position: static !important; height: auto !important; }
  #wall .sz-wall-payoff{
    position: static !important; opacity: 1 !important; pointer-events: auto !important;
    background: none !important;
  }
  @media (max-width: 720px){
    #wall .sz-wall{ --wl-w: 100% !important; }
  }
}


/* ==========================================================================
   sections/live.css
   ========================================================================== */
/* =============================================================================
   live.css: #live, "Watch it work"

   Every rule is scoped under #live. Nothing here declares a reserved class and
   nothing leaks out of the section.

   Two accessibility rules govern this file, both from CONTRACT §8:
     1. No hidden state exists outside html.motion-ready, and the .rv pre-reveal
        additionally requires #live[data-lv-ready="1"], which only live.js sets.
        If this section's script never runs, the header is visible, full stop.
     2. The reduced-motion block at the bottom renders all three completed
        transcripts and kills every animation and transition in the section.
   ========================================================================== */

/* ---------------------------------------------------------------- header */

/* motion.css declares --sz-wire as a neutral default meant to be layered on.
   Lifting it here keeps the Runlayer relationship intact at this section's
   contrast: the wire must read stronger than a card border (--line, .08 alpha)
   so a completely still diagram looks powered. Measured at 1px over --bg, .26
   almost disappeared. */
#live{ --sz-wire: rgba(232, 234, 239, .30); }

#live .lv-head{ max-width: 720px; }


/* ----------------------------------------------------------------- stage */
/* Grid, not margins: a display:none scenario generates no box and therefore no
   gap, so the stage spaces itself correctly whether one scenario is on screen
   (the loop) or all three are (JS off, reduced motion, paused). */

#live .lv-stage{
  display: grid;
  gap: var(--sp-8);
  margin-top: var(--sp-7);
}

#live .lv-scene-head{
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-3) var(--sp-5);
  margin-bottom: var(--sp-5);
}

#live .lv-scene-head h3{ font-weight: 700; }

#live .lv-scene-sub{
  color: var(--muted);
  font-size: 15px;
  flex: 1 1 320px;
  min-width: 0;
}

#live .lv-scene-head .status{ flex: 0 0 auto; }


/* ---------------------------------------------------------------- panels */
/* The wire layer is injected by wires.js as the root's first child and is
   absolutely positioned, so it is out of flow and never becomes a grid item.
   The panels sit above it on z-index so a wire can never cross panel text. */

#live .lv-panels{
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-7);
}

#live .lv-panel{
  position: relative;
  z-index: 1;
  background: var(--panel-fill);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-4);
  padding: var(--sp-5);
}

#live .lv-panel-head{
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding-bottom: var(--sp-3);
  margin-bottom: var(--sp-4);
  border-bottom: var(--hair) solid var(--line);
}

#live .lv-idx{
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--faint);
}

#live .lv-panel-name{
  font-size: 14.5px;
  font-weight: 650;
  letter-spacing: -.2px;
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}

#live .lv-panel-meta{
  margin-left: auto;
  font-size: 10.5px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--faint);
}


/* ------------------------------------------------------------ transcripts */
/* min-height holds the panel box steady while steps fade in, and absorbs one
   wrapped line so the stage does not jump between scenarios. */

#live .lv-steps{
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-height: 136px;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
}

#live .lv-steps li{
  position: relative;
  padding-left: 17px;
}

#live .lv-steps li::before{
  content: "";
  position: absolute;
  left: 1px;
  top: .58em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--line3);
}

/* The last line of each panel is the conclusion, so it carries the ink. */
#live .lv-steps li:last-child{ color: var(--text); }
#live .lv-steps li:last-child::before{ background: var(--accent-line); }


/* ------------------------------------------------- the accent scenario (Joy) */
/* The punchline gets the accent, and only where it lands: the scenario name and
   the closing panel. A full accent wash would spend the colour on three panels
   and leave the actual offer looking the same as the plumbing. */

#live [data-accent="1"]{ --sz-wire: var(--accent-line); }

#live [data-accent="1"] .lv-scene-head h3{ color: var(--accent-ink); }

#live [data-accent="1"] [data-panel="hire"]{
  border-color: var(--accent-line);
  background: linear-gradient(180deg, var(--accent-soft), transparent 62%), var(--panel-fill);
}

#live [data-accent="1"] [data-panel="hire"] .lv-idx,
#live [data-accent="1"] [data-panel="hire"] .lv-steps li:last-child{ color: var(--accent-ink); }


/* ------------------------------------------------------------------- foot */

#live .lv-foot{
  margin-top: var(--sp-6);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4) var(--sp-5);
}

#live .lv-note{
  flex: 1 1 340px;
  min-width: 0;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--faint);
}

/* Injected by live.js only when the loop actually arms, so a JS-off page never
   shows a dead control. WCAG 2.2.2: auto-updating content needs a pause. */
#live .lv-pause{
  min-height: 44px;
  padding: 11px 18px;
  border-radius: var(--r-pill);
  border: var(--hair) solid var(--line2);
  background: transparent;
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
  transition: border-color var(--t-base), color var(--t-base);
}

#live .lv-pause:hover{
  border-color: var(--accent-line);
  color: var(--accent-ink);
}

/* .plink is a 21px inline row in tokens.css, which is under the 44px touch
   minimum. Extending it inside this scope, not redefining it. */
#live .lv-foot .plink{ min-height: 44px; margin-top: 0; }


/* -------------------------------------------------------- the paused state */
/* Pausing does not freeze a half-drawn frame, it opens everything: all three
   scenarios, all steps, the same rendering reduced motion gets. The id selector
   outranks motion.css's .motion-ready rules with no !important. */

#live[data-lv-paused="1"] [data-demostage] [data-scenario]{ display: block; }
#live[data-lv-paused="1"] [data-demostage] [data-step]{ opacity: 1; transform: none; }


/* ----------------------------------------------------- the .rv reveal gate */
/* tokens.css hides .rv under html.motion-ready. That is safe only if something
   adds .in. This section does not assume anyone else will: the hidden state is
   re-gated on an attribute live.js sets, so if this script never runs the
   header renders composed instead of waiting on a failsafe that bootOk()
   disarms. */

html.motion-ready #live .rv{ opacity: 1; transform: none; transition: none; }

/* The armed state carries NO transition. live.js sets the attribute after first
   paint, and a transition on the hidden rule would make the header visibly fade
   OUT before it ever faded in. It snaps instead, off screen, where nobody is
   looking, and the transition lives on the revealed state where it belongs. */
html.motion-ready #live[data-lv-ready="1"] .rv{
  opacity: 0;
  transform: translateY(22px);
  transition: none;
}
html.motion-ready #live[data-lv-ready="1"] .rv.in{
  opacity: 1;
  transform: none;
  transition: opacity var(--t-reveal) var(--ease), transform var(--t-reveal) var(--ease);
}


/* ----------------------------------------------------------------- mobile */
/* Single-column panel sequence, wires dropped (the stage is read top to bottom
   instead of left to right, so a horizontal wire has nothing left to say). */

@media (max-width: 860px){
  #live .lv-panels{
    grid-template-columns: 1fr;
    gap: var(--sp-4);
  }
  #live .sz-wire-layer{ display: none; }
  #live .lv-steps{ min-height: 0; }
  #live .lv-stage{ gap: var(--sp-7); }
  #live .lv-scene-head{ gap: var(--sp-2) var(--sp-4); }
  #live .lv-foot{ gap: var(--sp-4); }
}

@media (max-width: 420px){
  #live .lv-steps{ font-size: 12px; }
}


/* ------------------------------------------------------- reduced motion */
/* Zero running animation, final values, all three transcripts on screen. This
   is the acceptance case that matters most in this section, because the section
   is the animation. */

@media (prefers-reduced-motion: reduce){
  #live *,
  #live *::before,
  #live *::after{
    animation: none !important;
    transition: none !important;
  }

  html.motion-ready #live .rv{ opacity: 1 !important; transform: none !important; }

  #live [data-demostage] [data-scenario]{ display: block !important; }
  #live [data-demostage] [data-step]{ opacity: 1 !important; transform: none !important; }
}


/* ==========================================================================
   sections/systems.css
   ========================================================================== */
/* =============================================================================
   systems.css: Flagships (#systems), five deep chapters

   Every rule in this file is scoped under #systems. There are no @keyframes
   here at all: this section's only running pixel is the particle dome canvas,
   which owns its own rAF loop and its own four gates. That is deliberate. The
   page's animation ration is shared, and a chapter stack does not need to
   twitch to be read.

   Cascade position: after tokens.css and nav.css, before footer.css.
   ============================================================================= */

/* -------------------------------------------------------------- 1. the shell */

#systems .fs-stack{
  /* lib knobs, declared inside this scope only. cardstack.js and motion.css
     read them off the ancestor; --sz-i is per wrapper, inline in the markup. */
  --sz-stack-top:  calc(var(--nav-h) + var(--sp-3));
  --sz-stack-step: 14px;
  --sz-rail-top:   calc(var(--nav-h) + var(--sp-3));

  /* hoverfill's tile and its inverted ink, expressed in zone tokens so the
     rail follows the palette instead of the lib's neutral defaults. */
  --sz-fill-ink: var(--text);
  --sz-fill-on:  var(--bg);

  margin-top: var(--sp-8);
  display: grid;
  grid-template-columns: 244px minmax(0, 1fr);
  gap: var(--sp-7);
  /* align-items stays at its default stretch ON PURPOSE. Setting it to start
     collapses the rail column to its content height and the sticky rail would
     then have nowhere to travel. */
}

#systems .fs-railcol{ min-width: 0; }

#systems .fs-rail{
  /* .sz-rail carries position:sticky from motion.css. It is on this wrapper,
     not on the <ol>, so the note under the list pins with it. */
  padding-right: var(--sp-4);
}

#systems .fs-rail-list{ list-style: none; }

#systems .fs-row{
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-3);
  min-height: 44px;
  padding: var(--sp-3) var(--sp-3);
  border-top: var(--hair) solid var(--line);
  color: var(--muted);
  font-size: 14.5px;
  line-height: 1.35;
  /* hoverfill parks its tile OUTSIDE the row (translate 0, 110%) and the row
     has no opaque background of its own, so without this the parked tile is
     visible sitting on top of the next row. Clipping it here costs nothing: the
     tile's 1px overshoot only exists to swallow the row hairline, and that
     hairline is a low-alpha white that vanishes against the fill anyway. An
     element's own focus outline is not clipped by its own overflow. */
  overflow: hidden;
}
#systems .fs-rail-list li:last-child .fs-row{ border-bottom: var(--hair) solid var(--line); }

#systems .fs-row-n{
  font-size: 11.5px;
  letter-spacing: 1px;
  color: var(--faint);
  transition: color var(--t-base);
}

#systems .fs-row[aria-current="true"]{
  color: var(--text);
  background: var(--accent-soft);
  border-top-color: var(--accent-line);
  box-shadow: inset 2px 0 0 0 var(--accent-line);
}
#systems .fs-row[aria-current="true"] .fs-row-n{ color: var(--accent-ink); }
#systems .fs-rail-list li:last-child .fs-row[aria-current="true"]{ border-bottom-color: var(--accent-line); }

/* hoverfill inverts the row's ink through [data-filled="1"], but that rule in
   motion.css is two bare attribute selectors and loses to the id-scoped
   aria-current colour above. Without this, hovering the ACTIVE row paints
   near-white text on a near-white tile. The number rides along, otherwise it
   stays --faint on the fill. */
#systems .fs-row[data-filled="1"]{ color: var(--sz-fill-on); }
#systems .fs-row[data-filled="1"] .fs-row-n{ color: inherit; opacity: .72; }

#systems .fs-rail-note{
  margin-top: var(--sp-5);
  font-size: 11.5px;
  letter-spacing: .6px;
  color: var(--faint);
}
#systems .fs-rail-note a{ color: var(--accent-ink); border-bottom: var(--hair) solid var(--accent-line); }

/* ---------------------------------------------------------------- 2. the lane */

#systems .fs-lane{ min-width: 0; }

/* The dwell between pins: a card travels its own height plus this before the
   next one arrives and covers it. */
#systems .sz-cardwrap{ margin-bottom: var(--sp-8); }

/* Gives chapter 05 a beat at the top of the stack before the section ends. */
#systems .fs-tail{ height: 34vh; }

/* --------------------------------------------------------------- 3. the card */

#systems .fs-card{
  /* .card's default fill fades to 35% alpha at the bottom, which would let the
     card underneath show through a stacked card. The stack needs opacity. */
  background: var(--panel);
  border-radius: var(--r-5);
  padding: clamp(20px, 2.1vw, 30px);
  box-shadow: var(--shadow-2);
}
/* .card's hover border is meant for a small tile. On a 500px chapter card it
   reads as a glitch, so it is neutralised rather than inherited. */
#systems .fs-card:hover{ border-color: var(--line); }

#systems .fs-head{
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

#systems .fs-n{
  font-size: clamp(24px, 2.6vw, 32px);
  font-weight: 700;
  letter-spacing: -1px;
  line-height: 1;
  color: var(--accent-ink);
  margin-right: var(--sp-2);
}

#systems .fs-title{
  margin: var(--sp-4) 0 var(--sp-4);
  font-size: clamp(20px, 2.3vw, 27px);
  font-weight: 720;
  letter-spacing: -.5px;
  line-height: 1.2;
}

#systems .fs-body{
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 430px);
  gap: var(--sp-6);
  align-items: start;
}
/* The single-column variant (chapter 05). gap:0 because its three children
   carry their own top margins; leaving the row gap on would stack 32px twice
   on top of them and push the card's last row under the fold while pinned. */
#systems .fs-body-stack{ grid-template-columns: minmax(0, 1fr); gap: 0; }

#systems .fs-copy p,
#systems .fs-copy-lede{
  color: var(--muted);
  font-size: 15.5px;
  line-height: 1.62;
}
#systems .fs-copy-lede{ max-width: 76ch; }

#systems .fs-facts{
  list-style: none;
  margin-top: var(--sp-5);
  display: grid;
  gap: var(--sp-2);
}
#systems .fs-facts li{
  position: relative;
  padding-left: var(--sp-5);
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: .2px;
  color: var(--faint);
}
#systems .fs-facts li::before{
  content: "";
  position: absolute;
  left: 0; top: .62em;
  width: 12px; height: 1px;
  background: var(--line3);
}

#systems .fs-link{ margin-top: var(--sp-5); }

/* ------------------------------------------------- 4. card 01, the dome panel */

#systems .fs-art{ display: grid; gap: var(--sp-4); min-width: 0; }

#systems .fs-dome{
  /* wires.js and dome.js both need a positioned, measurable box. The height is
     tied to the viewport so a short window shortens the card instead of
     pushing its bottom under the fold while it is pinned. */
  position: relative;
  height: clamp(150px, 21vh, 226px);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-4);
  overflow: hidden;
  background: var(--panel2);
}
#systems .fs-dome .sz-dome-plate{
  /* The always-shipped base layer under the canvas: mobile, reduced motion, a
     refused 2D context and a narrow viewport all land here. */
  --sz-dome-plate: radial-gradient(126% 74% at 50% 112%,
                     rgba(116,224,196,.26) 0%,
                     rgba(106,163,255,.13) 38%,
                     rgba(10,30,60,0) 68%);
}

#systems .fs-dome-panel{
  position: absolute;
  left: var(--sp-4);
  bottom: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  /* .dk-glass-faux: the glass READ with zero compositing cost. The page's only
     persistent composited surface is the nav, so no backdrop-filter here. */
  --glass-r: var(--r-3);
}
#systems .fs-dome-k{
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -1.4px;
  color: var(--text);
}
#systems .fs-dome-l{
  margin-top: 3px;
  font-size: 12px;
  line-height: 1.35;
  color: var(--muted);
  max-width: 20ch;
}

#systems .fs-shot{
  border: var(--hair) solid var(--line);
  border-radius: var(--r-3);
  overflow: hidden;
  background: var(--panel2);
}
#systems .fs-shot img{
  display: block;
  width: 100%;
  height: clamp(96px, 13vh, 150px);
  object-fit: cover;
  object-position: 50% 0;
}
#systems .fs-cap{
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--faint);
}
#systems .fs-shot .fs-cap{ padding: var(--sp-2) var(--sp-3) var(--sp-3); }

/* ------------------------------------------------ 5. card 02, the wire field */

#systems .fs-diagram{ display: grid; gap: var(--sp-3); }

#systems .fs-wire-scroll{
  /* The scroller is the OUTER box. wires.js injects its SVG into .fs-wire-field
     and sizes it to that element's real rect, so keeping the overflow one level
     out means a phone side-scrolls a miniature of the identical topology
     instead of a diagram whose wires stop at the visible edge. */
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  border: var(--hair) solid var(--line);
  border-radius: var(--r-4);
  padding: var(--sp-4);

  /* The side-scroll affordance, and the reason the background is four layers
     instead of a flat fill. Measured at 390px: the box is 306px wide holding
     492px of topology, so a third of the diagram is off screen with nothing on
     screen saying so. The two `local` cover layers scroll with the content, the
     two `scroll` shadow layers do not, so a shadow is only visible on the side
     that still has content past the edge and both vanish at the ends. No JS, no
     scroll listener, and it degrades to a plain panel wherever multiple
     backgrounds are unsupported. */
  background:
    linear-gradient(to right, var(--panel2) 40%, transparent) left center / 30px 100% no-repeat local,
    linear-gradient(to left,  var(--panel2) 40%, transparent) right center / 30px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%,
      color-mix(in srgb, var(--text) 24%, transparent), transparent) left center / 16px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%,
      color-mix(in srgb, var(--text) 24%, transparent), transparent) right center / 16px 100% no-repeat scroll,
    var(--panel2);
}

/* Chapter 02 gives the diagram the larger half of the card. The topology needs
   a wider box than a stat block does, and the alternative is a phone-style
   side-scroll on a 1400px screen. Kept behind a min-width query on purpose:
   two ids beat the single-column mobile rule below on specificity, and left
   unguarded it would hold a 500px column open inside a 350px card. */
@media (min-width: 901px){
  #systems #sys-02 .fs-body{ grid-template-columns: minmax(0, 1fr) minmax(0, 500px); }
}

#systems .fs-wire-field{
  position: relative;          /* wires.js injects <svg class="sz-wire-layer"> here */
  /* Measured geometry, both numbers. 460px is the narrowest box that still fits
     three node columns without the labels wrapping to three lines. The 60px
     column gap is what the elbow is drawn in: wires.js spends `run` (14px, set
     in systems.js) on a straight segment at each end, so the cubic gets the
     remaining 32px to travel in. Drop the gap and the elbow collapses into a
     near-vertical jog. */
  min-width: 460px;
  height: clamp(178px, 24vh, 228px);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: 1fr 1fr;
  row-gap: var(--sp-4);
  column-gap: 60px;
  align-items: center;
}

#systems .fs-node{
  display: block;
  align-self: center;
  padding: var(--sp-3);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-2);
  /* The node fill sits brighter than the field and the wire stroke (--sz-wire,
     .26 alpha) sits stronger than these borders (.08). That contrast is the
     whole "powered" read; it costs zero animation. */
  background: var(--panel);
  color: var(--text);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
}
#systems .fs-node small{
  display: block;
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: .3px;
  color: var(--faint);
}
#systems .fs-node[data-node="mac"]{    grid-column: 1; grid-row: 1; }
#systems .fs-node[data-node="iphone"]{ grid-column: 1; grid-row: 2; }
#systems .fs-node[data-node="nas"]{    grid-column: 2; grid-row: 1 / span 2; }
#systems .fs-node[data-node="vault"]{  grid-column: 3; grid-row: 1; }
#systems .fs-node[data-node="tunnel"]{ grid-column: 3; grid-row: 2; }
#systems .fs-node-hub{
  border-color: var(--accent-line);
  background: var(--panel2);
  padding-block: var(--sp-4);
}

#systems .fs-reserved{
  border: var(--hair) dashed var(--line2);
  border-radius: var(--r-2);
  padding: var(--sp-2) var(--sp-3);
  font-size: 11px;
  letter-spacing: .4px;
  color: var(--faint);
}

/* -------------------------------------------------- 6. card 03, the two numbers */

#systems .fs-stats{ display: grid; gap: var(--sp-4); }
#systems .fs-stat{
  border: var(--hair) solid var(--line);
  border-radius: var(--r-3);
  background: var(--panel2);
  padding: var(--sp-4) var(--sp-5);
}
#systems .fs-stat-k{
  font-size: clamp(28px, 3.4vw, 40px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -1.6px;
  color: var(--accent-ink);
}
#systems .fs-stat-l{
  margin-top: var(--sp-2);
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--muted);
}

/* ---------------------------------------------------- 7. card 04, the call rail */

#systems .fs-call{
  list-style: none;
  border: var(--hair) solid var(--line);
  border-radius: var(--r-3);
  background: var(--panel2);
  padding: var(--sp-4) var(--sp-5);
  display: grid;
  gap: var(--sp-4);
}
#systems .fs-call-row{
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: baseline;
}
#systems .fs-call-t{
  font-size: 11px;
  letter-spacing: .4px;
  color: var(--faint);
  text-align: right;
}
#systems .fs-call-b{
  position: relative;
  padding-left: var(--sp-5);
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}
#systems .fs-call-b b{ color: var(--text); font-weight: 640; }
/* The spine, anchored to the text column so it never depends on the width of
   the timestamp gutter. Pure hairline: this rail carries no motion. */
#systems .fs-call-b::before{
  content: "";
  position: absolute;
  left: 0; top: .5em;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent-line);
}
#systems .fs-call-row:not(:last-child) .fs-call-b::after{
  content: "";
  position: absolute;
  left: 3px; top: 1.35em;
  bottom: calc(-1 * var(--sp-4));
  width: 1px;
  background: var(--line);
}

/* -------------------------------------------------- 8. card 05, the pipeline */

/* Six stages in two rows of three. The type sizes here are a size down from the
   rest of the card on purpose: this chapter is a pipeline index, and the whole
   thing has to stay shorter than the space under the stack offset or its bottom
   rows sit below the fold for the length of the pin. */
#systems .fs-pipe{
  list-style: none;
  margin-top: var(--sp-5);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4) var(--sp-5);
}
#systems .fs-pipe-item{
  border-top: var(--hair) solid var(--line);
  padding-top: var(--sp-3);
  min-width: 0;
}
#systems .fs-pipe-s{
  display: block;
  font-size: 10.5px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--accent-ink);
}
#systems .fs-pipe-h{
  margin-top: var(--sp-1);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  font-size: 14.5px;
  font-weight: 660;
  letter-spacing: -.2px;
  color: var(--text);
}
#systems .fs-pipe-item p{
  margin-top: 5px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}

/* ---------------------------------------------------------- 9. the CTA row */

#systems .ctarow{ margin-top: var(--sp-8); }

/* ================================================================ 10. breakpoints */

/* Short windows: a pinned card taller than the space below the stack offset
   would hide its own bottom for the whole pin, because a sticky element does
   not scroll inside its pin. Below this height the stack is a plain stack. */
/* Measured at 1400px wide: the tallest chapter is 542px and pins at 150px, so
   the deepest pinned edge on the page is 692px. Anything at or under 700px of
   viewport height gets the plain stack instead. */
@media (max-height: 700px){
  #systems .sz-cardwrap{ position: static; }
  #systems .fs-rail{ position: static; }
  #systems .fs-tail{ height: 0; }
}

@media (max-width: 1100px){
  #systems .fs-stack{ grid-template-columns: 208px minmax(0, 1fr); gap: var(--sp-6); }
  #systems .fs-body{ grid-template-columns: minmax(0, 1fr) minmax(0, 360px); }
  #systems .fs-pipe{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Mobile is its own component, not a squeezed desktop. motion.css already drops
   sticky from .sz-cardwrap and .sz-rail at this width; the rail itself goes
   away because the cards carry the same numbers in the same order, and a
   duplicate index above a five-card stack is noise on a phone. */
@media (max-width: 900px){
  #systems .fs-stack{
    display: block;
    margin-top: var(--sp-6);
  }
  #systems .fs-railcol{ display: none; }
  #systems .sz-cardwrap{ margin-bottom: var(--sp-5); }
  #systems .fs-tail{ height: 0; }

  #systems .fs-body{ grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); }
  #systems .fs-body-stack{ gap: 0; }
  #systems .fs-pipe{ grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); }

  #systems .fs-card{ padding: var(--sp-5); border-radius: var(--r-4); }
  #systems .fs-head{ gap: var(--sp-2); }
  #systems .fs-dome{ height: 180px; }
  #systems .fs-shot img{ height: 132px; }
  #systems .fs-wire-field{ height: 210px; }
  #systems .fs-call-row{ grid-template-columns: 42px minmax(0, 1fr); gap: var(--sp-3); }
  /* .plink is 21px tall in the house grammar, which is fine under a fine
     pointer and too small for a thumb. Raised here only, so the desktop card
     heights that the sticky stack depends on do not move. */
  #systems .fs-link{ min-height: 44px; align-items: center; }
  #systems .ctarow{ margin-top: var(--sp-6); }
}

@media (max-width: 480px){
  #systems .fs-title{ font-size: 20px; }
  #systems .fs-dome-k{ font-size: 26px; }
  #systems .fs-call{ padding: var(--sp-4); }
}

/* =============================================== 11. prefers-reduced-motion */
/*
   Zero running animation, final rendered values, layout unchanged. Sticky is
   layout rather than animation, so the stack still stacks. systems.js declines
   to initialise the dome at all under this query, which leaves the canvas empty
   and the gradient plate as the whole scene: the state the contract asks for
   and the one the plate was built to be.
*/
@media (prefers-reduced-motion: reduce){
  #systems *,
  #systems *::before,
  #systems *::after{
    animation: none !important;
    transition: none !important;
  }
  /* Nothing in this section authors a hidden pre-reveal state of its own, so
     there is no opacity to restore beyond the shared .rv rule in tokens.css.
     hoverfill's tile is already display:none from the lib's global block. */
}


/* ==========================================================================
   sections/lab.css
   ========================================================================== */
/* ============================================================================
   #lab: the Lab, eight small live demonstrations.
   ----------------------------------------------------------------------------
   Every rule in this file is scoped under #lab. Every keyframe name is
   prefixed lb-.

   THE ONE RULE THAT GOVERNS THIS FILE
   -----------------------------------
   The base (unanimated) value of every animated element IS its final frame.
   Animations are then attached only under html.motion-ready, which core.js adds
   and which it declines to add under prefers-reduced-motion. So:

     JS off            no .motion-ready  -> no animation, final frames rendered
     reduced motion    no .motion-ready  -> no animation, final frames rendered
     paused (off view) animation-play-state: paused (lib/motion.css gate)
     hover / focus     same gate, frozen on the frame being read

   Nothing in this section is ever held at opacity 0 without .motion-ready.

   Seams: lib/motion.css .sz-grid draws them from the grid's own background
   (gap:1px over a rule-coloured wrapper, N cells produce N+1 rules). This file
   only feeds that mechanic the page's own tokens (--seam, --bg).

   Inversion: two diagonal cells carry data-field="accent". That single
   attribute remaps this file's per-cell custom properties and every stage,
   index, stroke and fill downstream follows.
   ========================================================================== */

/* ---- the pause control -----------------------------------------------------
   The WCAG 2.2.2 stop for the eight looping cells. It is display:none until
   hairlinegrid.js sets data-ready, so a JS-off page never shows a control that
   cannot work, and it is hidden under reduced motion where nothing is running
   in the first place. State is carried by the visible label ("Pause the loops"
   / "Resume the loops") and mirrored onto data-paused for the CSS. Deliberately
   NOT aria-pressed: a toggle button either changes its name or carries a
   pressed state, and doing both announces "Resume the loops, pressed", which
   reads backwards. The name is the single source of truth. */
#lab .lb-pause{
  display: none;
  margin-top: var(--sp-4);
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 10px 18px;
  border: var(--hair) solid var(--line);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--muted);
  font-size: 12px;
  letter-spacing: .04em;
  cursor: pointer;
  transition: color var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease),
              background-color var(--t-base) var(--ease);
}
#lab .lb-pause[data-ready="1"]{ display: inline-flex; }
#lab .lb-pause:hover{ color: var(--text); border-color: var(--line2); }
#lab .lb-pause:focus-visible{ outline: 2px solid var(--accent-ink); outline-offset: 3px; }
#lab .lb-pause[data-paused="1"]{
  color: var(--text);
  border-color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent-ink) 8%, transparent);
}
/* Two bars while running, one triangle while paused: the glyph states what the
   press will do next, which is the same thing the label says. */
#lab .lb-pause-i{
  width: 9px; height: 11px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}
#lab .lb-pause[data-paused="1"] .lb-pause-i{
  width: 0; height: 0;
  border-left: 9px solid currentColor;
  border-right: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}
@media (prefers-reduced-motion: reduce){
  #lab .lb-pause{ display: none !important; }
}

/* ---- the grid --------------------------------------------------------------
   --sz-grid-cols lives here rather than inline, because an inline custom
   property cannot be beaten by a media query. */
#lab .lb-grid{
  margin-top: var(--sp-7);
  --sz-grid-cols: 4;
  --sz-line: var(--seam);      /* the seam colour the wrapper paints           */
  --sz-bg: var(--bg);          /* cell ground: the page floor                  */
  --sz-grid-radius: var(--r-4);
  box-shadow: var(--shadow-1);
}

/* ---- the cell -------------------------------------------------------------- */
#lab .lb-cell{
  --lb-stage-h: 192px;
  --lb-well: var(--panel2);    /* the recessed demo stage                      */
  --lb-line: var(--line);
  --lb-line2: var(--line2);
  --lb-fill: var(--line3);     /* solid block inside a demo (clips, frames)    */
  --lb-fill2: var(--line2);    /* the quieter half of a block                  */
  --lb-idx: var(--faint);

  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: 20px;
}

/* the one-attribute accent remap. Exactly two diagonal cells carry it. The
   cell ground lifts and takes an accent wash, and because the stage, the index,
   every stroke and every fill read from the properties below, one attribute is
   the whole inversion. */
#lab .lb-cell[data-field="accent"]{
  background:
    linear-gradient(180deg, var(--accent-soft), transparent 62%),
    var(--panel);
  --lb-well: var(--bg2);       /* darker than its own cell: genuinely recessed */
  --lb-line: var(--accent-line);
  --lb-line2: var(--accent-line);
  --lb-fill: var(--accent-line);
  --lb-fill2: var(--accent-soft);
  --lb-idx: var(--accent-ink);
}

#lab .lb-idx{
  font-family: var(--mono); font-size: 11px; letter-spacing: 1.6px;
  color: var(--lb-idx); margin: var(--sp-2) 0 0;
}
#lab .lb-cell h3{ font-size: 18px; margin: 0; }
#lab .lb-blurb{ color: var(--muted); font-size: 14.5px; line-height: 1.55; margin: 0; }
#lab .lb-blurb code{
  font-family: var(--mono); font-size: .9em; color: var(--accent-ink);
  white-space: nowrap;
}
#lab .lb-proof{
  margin: auto 0 0;                            /* bottom-pinned */
  padding-top: var(--sp-3); border-top: var(--hair) solid var(--lb-line);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: var(--faint);
}
#lab .lb-proof .status{ font-size: 11.5px; }

/* ---- the demo stage --------------------------------------------------------
   --panel2 is the house token for exactly this: a recessed stage inside a
   panel. The hairline plus the inset highlight is what makes it read sunken. */
#lab .lb-stage{
  height: var(--lb-stage-h);
  background: var(--lb-well);
  border: var(--hair) solid var(--lb-line);
  border-radius: var(--r-3);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.03);
  overflow: hidden;
  padding: 12px;
  display: flex; flex-direction: column; gap: 7px;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
  color: var(--muted);
}
#lab .lb-stage-mid{ justify-content: center; }

#lab .lb-t{ margin: 0; }
/* The last line of a terminal stage sits on the floor of the well, so the
   prompt reads from the top and the meta reads from the bottom. Declared after
   .lb-t on purpose: same specificity, and order is the only cascade tool. */
#lab .lb-foot{ margin-top: auto; }
#lab .lb-dim{ color: var(--faint); }
#lab .lb-sig{ color: var(--accent-ink); }
#lab .lb-key{ color: var(--accent-ink); }
#lab .lb-row{ display: flex; justify-content: space-between; gap: var(--sp-2); }

/* the typed query: an inline clip whose width steps out one character at a
   time. --n is the character count, set per instance in the markup. */
#lab .lb-type{
  display: inline-block; overflow: hidden; white-space: nowrap;
  vertical-align: bottom; padding-bottom: .14em; margin-bottom: -.14em;
  width: calc(var(--n) * 1ch);
  color: var(--text);
}
#lab .lb-caret{
  display: none;                       /* only exists while motion runs        */
  width: 6px; height: 1em; margin-left: 2px;
  vertical-align: -.15em; background: var(--accent-ink);
}

/* The result slot. The box itself never animates: it holds its shape for the
   whole loop so the stage always reads as an interface waiting on an answer,
   and only the answer inside it arrives. */
#lab .lb-hit{
  margin: 0; color: var(--text);
  padding: 6px 8px; border-radius: var(--r-1);
  background: var(--accent-soft);
  border: var(--hair) solid var(--accent-line);
}
#lab .lb-hit-i{ display: flex; align-items: baseline; gap: 8px; }
#lab .lb-score{ color: var(--accent-ink); font-weight: 700; }

/* ---- 02 vcut: the timeline strip ------------------------------------------
   Clips are fixed, gaps animate their width to 0 and the strip closes up, so
   the cut timeline is visibly shorter than the raw one. Final frame: no gaps. */
#lab .lb-strip{
  display: flex; gap: 0; height: 34px; margin: 3px 0;
}
#lab .lb-clip{
  flex: 0 0 var(--w);
  border-right: 2px solid var(--lb-well);
  border-radius: 2px;
  background: linear-gradient(180deg, var(--lb-fill), var(--lb-fill2));
}
#lab .lb-gap{
  flex: 0 0 auto;
  width: 0; opacity: 0;                /* FINAL frame: the gap is gone         */
  border-radius: 2px;
  background: repeating-linear-gradient(45deg,
    var(--lb-fill2) 0 2px, transparent 2px 5px);
}

/* ---- 03 Wardrobe: garment, then the render ---------------------------------
   Abstract shapes only. No photograph, no generated face. */
#lab .lb-tryon{
  display: flex; align-items: center; justify-content: center;
  gap: 14px; height: 96px;
}
#lab .lb-garment{
  position: relative; width: 38px; height: 46px;
  border-radius: 5px 5px 9px 9px;
  background: var(--lb-fill2);
  border: var(--hair) solid var(--lb-line2);
}
#lab .lb-garment::before,
#lab .lb-garment::after{
  content: ""; position: absolute; top: -1px; width: 11px; height: 22px;
  background: var(--lb-fill2); border: var(--hair) solid var(--lb-line2);
}
#lab .lb-garment::before{ left: -12px; border-radius: 5px 0 0 6px; border-right: 0; }
#lab .lb-garment::after{ right: -12px; border-radius: 0 5px 6px 0; border-left: 0; }

#lab .lb-arrowdot{
  width: 22px; height: 1px; background: var(--lb-line2); position: relative;
}
#lab .lb-arrowdot::after{
  content: ""; position: absolute; right: 0; top: -2px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent-ink);
}
#lab .lb-render{
  position: relative; display: block; width: 62px; height: 84px;
  border-radius: var(--r-1); overflow: hidden;
  background: var(--bg2); border: var(--hair) solid var(--lb-line2);
}
#lab .lb-skel{
  position: absolute; inset: 0; opacity: 0;    /* FINAL frame: render done     */
  background: repeating-linear-gradient(180deg,
    var(--lb-fill2) 0 6px, transparent 6px 13px);
}
#lab .lb-figure{ position: absolute; inset: 0; opacity: 1; }
#lab .lb-figure::before{      /* head */
  content: ""; position: absolute; left: 50%; top: 12px;
  width: 15px; height: 15px; margin-left: -7.5px; border-radius: 50%;
  background: var(--lb-fill2); border: var(--hair) solid var(--lb-line2);
}
#lab .lb-figure::after{       /* the garment, worn */
  content: ""; position: absolute; left: 50%; top: 30px;
  width: 34px; height: 38px; margin-left: -17px;
  border-radius: 6px 6px 3px 3px;
  background: var(--accent-soft); border: var(--hair) solid var(--accent-line);
}

/* ---- 04 Bonsai: the tree ---------------------------------------------------
   Trunk extends from the pot, foliage lands cluster by cluster. Final frame:
   the whole tree. */
#lab .lb-tree{ position: relative; height: 112px; }
#lab .lb-trunk{
  position: absolute; left: 50%; bottom: 15px; margin-left: -2px;
  width: 4px; height: 74px; border-radius: 2px;
  transform-origin: 50% 100%; transform: scaleY(1);
  background: linear-gradient(180deg, var(--lb-fill2), var(--lb-fill));
}
#lab .lb-leaf{
  position: absolute; left: 50%; bottom: 15px; margin-left: -12px;
  width: 24px; height: 15px; border-radius: 50%;
  background: var(--accent-soft); border: var(--hair) solid var(--accent-line);
  transform: translate(var(--x), var(--y)) scale(1);
}
#lab .lb-pot{
  position: absolute; left: 50%; bottom: 0; margin-left: -22px;
  width: 44px; height: 15px; border-radius: 2px 2px 8px 8px;
  background: var(--lb-fill2); border: var(--hair) solid var(--lb-line2);
}

/* ---- 05 Deal Scout: ask against the comp ----------------------------------
   A price axis, not a price. The ask marker lands, then the spread band fills
   between it and the sold-data comp. No figure is stated because none is
   sourced. */
#lab .lb-comp{ position: relative; height: 30px; margin: 4px 0; }
#lab .lb-comp-line{
  position: absolute; left: 0; right: 0; bottom: 9px;
  height: 1px; background: var(--lb-line2);
}
#lab .lb-comp-band{
  position: absolute; left: 22%; width: 56%; bottom: 5px; height: 11px;
  border-radius: 2px; background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  transform-origin: 0 50%; transform: scaleX(1);
}
#lab .lb-comp-ask{
  position: absolute; left: 22%; bottom: 3px; width: 2px; height: 15px;
  background: var(--accent-ink); opacity: 1;
}
#lab .lb-comp-hi{
  position: absolute; left: 78%; bottom: 3px; width: 2px; height: 15px;
  background: var(--text); opacity: .55;
}

/* ---- 06 image-gen: the cap ------------------------------------------------ */
#lab .lb-cap{
  margin-top: auto; padding-left: 8px;
  border-left: 2px solid var(--accent-line); color: var(--faint);
}

/* ---- 07 Build recorder: the filmstrip -------------------------------------
   The reel is 200% wide and holds each frame twice, so translating it by
   exactly -50% lands on a visually identical position: the loop point is
   invisible and the final frame is also the first. */
#lab .lb-film{
  height: 46px; overflow: hidden; border-radius: 4px;
  background: var(--bg2); border: var(--hair) solid var(--lb-line);
}
#lab .lb-reel{
  display: flex; width: 200%; height: 100%;
  transform: translateX(-50%);
  background:
    repeating-linear-gradient(90deg, var(--lb-fill2) 0 4px, transparent 4px 13px)
      top left / 100% 5px no-repeat,
    repeating-linear-gradient(90deg, var(--lb-fill2) 0 4px, transparent 4px 13px)
      bottom left / 100% 5px no-repeat;
}
#lab .lb-frame{
  flex: 1 1 0; min-width: 0; margin: 8px 0;
  border-right: 2px solid var(--bg2);
  background: linear-gradient(180deg, var(--lb-fill2), transparent);
}
#lab .lb-frame.is-mark{
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
#lab .lb-tc{
  color: var(--faint); border: var(--hair) solid var(--lb-line);
  border-radius: 3px; padding: 1px 5px; margin-right: 4px;
}
#lab .lb-mark{
  color: var(--accent-ink); border-color: var(--accent-line);
  background: var(--accent-soft); opacity: 1;
  display: inline-block;
}

/* ---- 08 PACT: the check-in row -------------------------------------------- */
#lab .lb-mini{
  width: 152px; margin: 0 auto; padding: 11px;
  border-radius: var(--r-2); background: var(--bg2);
  border: var(--hair) solid var(--lb-line2);
}
#lab .lb-mini-head{
  display: block; height: 6px; width: 62%; border-radius: 3px;
  background: var(--lb-fill2);
}
#lab .lb-days{ display: flex; gap: 4px; margin-top: 11px; }
#lab .lb-day{
  position: relative; flex: 1 1 0; height: 18px; border-radius: 3px;
  border: var(--hair) solid var(--lb-line);
}
#lab .lb-day.is-on{ background: var(--accent-soft); border-color: var(--accent-line); }
#lab .lb-day-fill{
  position: absolute; inset: -1px; border-radius: 3px; opacity: 1;
  background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-line);
}

/* ---- the footer rail ------------------------------------------------------- */
#lab .lb-rail{
  grid-column: 1 / -1;
  background: var(--bg);
  padding: 20px;
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--sp-4);
}
#lab .lb-rail-note{ margin: 0; color: var(--muted); font-size: 14.5px; max-width: 46ch; }
#lab .lb-rail-cta{ display: flex; gap: var(--sp-3); flex-wrap: wrap; }

/* ============================================================================
   MOTION. Everything below is gated on html.motion-ready, which JS adds and
   which never appears under prefers-reduced-motion. Above this line the page
   is already showing every final frame.

   Loop durations are deliberately all different so eight cells never tick in
   lockstep, and every loop holds its final frame for at least a third of its
   length so a demo can actually be read.
   ========================================================================== */

/* THE SHORTHAND IS BANNED IN THIS BLOCK, and this is not a style preference.
   `animation:` sets animation-play-state to running as part of the shorthand.
   These selectors carry an id, so they out-specify the lib's pause gate
   ([data-inview="false"] [data-loop] { animation-play-state: paused }), and the
   section would keep burning frames off screen. Longhands only: nothing here
   ever writes animation-play-state, so the gate stays the only thing that owns
   it. Measured before and after: 26 animations running off screen with the
   shorthand, 0 with longhands. */
html.motion-ready #lab [data-loop]{
  animation-iteration-count: infinite;
  animation-timing-function: var(--ease);
}

/* 01 semsearch. The typed query needs its step count to equal its character
   count, and a keyframe-level timing function is the only thing that can step
   the typing segment and leave the hold alone, so the two typed cells get one
   keyframe block each. */
html.motion-ready #lab [data-demo="semsearch"] .lb-type{
  animation-name: lb-type-16;
  animation-duration: 6.4s;
  animation-timing-function: linear;
}
html.motion-ready #lab [data-demo="semsearch"] .lb-caret{
  display: inline-block;
  animation-name: lb-blink, lb-caretout;
  animation-duration: .9s, 6.4s;
  animation-timing-function: step-end, linear;
}
html.motion-ready #lab [data-demo="semsearch"] .lb-hit-i{
  animation-name: lb-surface;
  animation-duration: 6.4s;
}

/* 06 image-gen, the same two primitives on its own clock */
html.motion-ready #lab [data-demo="imgen"] .lb-type{
  animation-name: lb-type-12;
  animation-duration: 6s;
  animation-timing-function: linear;
}
html.motion-ready #lab [data-demo="imgen"] .lb-caret{
  display: inline-block;
  animation-name: lb-blink, lb-caretout;
  animation-duration: .9s, 6s;
  animation-timing-function: step-end, linear;
}
html.motion-ready #lab [data-demo="imgen"] .lb-hit-i{
  animation-name: lb-surface;
  animation-duration: 6s;
}

/* 02 vcut */
html.motion-ready #lab .lb-gap{
  animation-name: lb-cut;
  animation-duration: 5.6s;
}

/* 03 Wardrobe */
html.motion-ready #lab .lb-skel{
  animation-name: lb-skel;
  animation-duration: 5s;
}
html.motion-ready #lab .lb-figure{
  animation-name: lb-figin;
  animation-duration: 5s;
}

/* 04 Bonsai. The per-leaf delay is what makes the canopy fill in cluster by
   cluster; backwards fill means a leaf renders its 0% frame during its delay
   instead of flashing its final one. */
html.motion-ready #lab .lb-trunk{
  animation-name: lb-grow;
  animation-duration: 6.2s;
}
html.motion-ready #lab .lb-leaf{
  animation-name: lb-leafin;
  animation-duration: 6.2s;
  animation-fill-mode: backwards;
  animation-delay: calc(var(--i) * .14s);
}

/* 05 Deal Scout */
html.motion-ready #lab .lb-comp-band{
  animation-name: lb-band;
  animation-duration: 5.2s;
}
html.motion-ready #lab .lb-comp-ask{
  animation-name: lb-ask;
  animation-duration: 5.2s;
}

/* 07 Build recorder */
html.motion-ready #lab .lb-reel{
  animation-name: lb-reel;
  animation-duration: 8.4s;
  animation-timing-function: var(--ease-inout);
}
html.motion-ready #lab .lb-mark{
  animation-name: lb-mark;
  animation-duration: 8.4s;
}

/* 08 PACT */
html.motion-ready #lab .lb-day-fill{
  animation-name: lb-dayfill;
  animation-duration: 4.6s;
}

/* ---- keyframes. 100% is always the readable final frame -------------------- */

@keyframes lb-type-16{
  0%   { width: 0; animation-timing-function: steps(16, end); }
  8%   { width: 0; animation-timing-function: steps(16, end); }
  44%  { width: calc(var(--n) * 1ch); }
  100% { width: calc(var(--n) * 1ch); }
}
@keyframes lb-type-12{
  0%   { width: 0; animation-timing-function: steps(12, end); }
  8%   { width: 0; animation-timing-function: steps(12, end); }
  44%  { width: calc(var(--n) * 1ch); }
  100% { width: calc(var(--n) * 1ch); }
}
@keyframes lb-blink{
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}
@keyframes lb-caretout{
  0%, 45%   { visibility: visible; }
  46%, 100% { visibility: hidden; }
}
@keyframes lb-surface{
  0%, 48%   { opacity: 0; transform: translateY(5px); }
  62%, 100% { opacity: 1; transform: none; }
}
@keyframes lb-cut{
  0%, 30%   { width: var(--w); opacity: 1; }
  58%, 100% { width: 0; opacity: 0; }
}
@keyframes lb-skel{
  0%, 34%   { opacity: 1; }
  58%, 100% { opacity: 0; }
}
@keyframes lb-figin{
  0%, 34%   { opacity: 0; transform: scale(.94); }
  58%, 100% { opacity: 1; transform: none; }
}
@keyframes lb-grow{
  0%, 4%    { transform: scaleY(.34); }   /* a sapling, not an empty pot: this
                                             is the frame an off-screen cell is
                                             parked on before you reach it     */
  42%, 100% { transform: scaleY(1); }
}
@keyframes lb-leafin{
  0%, 10%   { transform: translate(var(--x), var(--y)) scale(0); }
  48%, 100% { transform: translate(var(--x), var(--y)) scale(1); }
}
@keyframes lb-band{
  0%, 40%   { transform: scaleX(0); }
  66%, 100% { transform: scaleX(1); }
}
@keyframes lb-ask{
  0%, 14%   { opacity: 0; transform: translateX(58px); }
  40%, 100% { opacity: 1; transform: none; }
}
@keyframes lb-reel{
  0%, 10%   { transform: translateX(0); }
  74%, 100% { transform: translateX(-50%); }
}
/* The chip lights up rather than appearing, so the shot-list line never loses
   a word and never reflows. */
@keyframes lb-mark{
  0%, 52%   { color: var(--faint); background: transparent; border-color: var(--lb-line); }
  66%, 100% { color: var(--accent-ink); background: var(--accent-soft); border-color: var(--accent-line); }
}
@keyframes lb-dayfill{
  0%, 38%   { opacity: 0; transform: scale(.7); }
  62%, 100% { opacity: 1; transform: none; }
}

/* ============================================================================
   Breakpoints. Two columns, then one. The cells are not squeezed: the stage
   loses height and the cell loses padding so a phone reads one demo at a time
   at full width instead of four postage stamps.
   ========================================================================== */
@media (max-width: 1080px){
  #lab .lb-grid{ --sz-grid-cols: 2; }
  #lab .lb-cell{ --lb-stage-h: 178px; }
}
@media (max-width: 620px){
  #lab .lb-grid{ --sz-grid-cols: 1; margin-top: var(--sp-6); }
  #lab .lb-cell{ --lb-stage-h: 162px; padding: 18px; }
  #lab .lb-rail{ padding: 18px; align-items: flex-start; }
  #lab .lb-rail-cta .btn{ flex: 1 1 auto; justify-content: center; }
}

/* ============================================================================
   prefers-reduced-motion. .motion-ready never arrives, so every rule in the
   motion block above is already inert and the base values (the final frames)
   are what render. This block is the belt to that pair of braces: it kills any
   animation or transition inside the section outright, and drops the caret,
   which is the one element that exists only while motion is running.

   The only !important in this file is here, which is where the contract allows
   it.
   ========================================================================== */
@media (prefers-reduced-motion: reduce){
  #lab *,
  #lab *::before,
  #lab *::after{
    animation: none !important;
    transition: none !important;
  }
  #lab .lb-caret{ display: none !important; }
}


/* ==========================================================================
   sections/work.css
   ========================================================================== */
/* ============================================================================
   work.css   #work  Camera: MindBlend case + the shot index
   ----------------------------------------------------------------------------
   Every rule is scoped under #work. Keyframes: none. This section has zero
   running animation at rest by construction, so it never spends any of the
   page's two-band motion ration.

   Two things this file deliberately overrides in lib/motion.css, both at higher
   specificity and both for the same reason (CONTRACT §8a: nothing may sit at
   opacity 0 in a browser with no JavaScript):

     motion.css keeps every .sz-vi-frame at opacity 0 / visibility hidden except
     the first, whether or not a script ran. Here the frames render as a plain
     stacked gallery until SZ.videoindex writes data-live="1" on the root, and
     only then do they collapse into one absolutely positioned stage. So JS-off
     shows all four frames, all four posters, and all four blocks of prose.
   ========================================================================== */

#work{
  /* local values. Both are surface treatments with no token equivalent. */
  --cm-scrim: linear-gradient(180deg, rgba(7,8,10,0) 0%, rgba(7,8,10,.55) 42%, rgba(7,8,10,.90) 100%);
  --cm-hatch: repeating-linear-gradient(135deg,
                rgba(255,255,255,.035) 0 2px, rgba(255,255,255,0) 2px 10px);
  --cm-stage-ar: 16 / 10;   /* measured against the 1600x900 team still, which
                               loses almost nothing at this crop              */
}


/* ---------------------------------------------------------------- the case */

#work .cm-case{ margin-top: var(--sp-7); }

#work .cm-case-top{
  display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap; margin-bottom: var(--sp-4);
}

#work .cm-case h3{ max-width: 30ch; }

#work .cm-deliv{
  list-style: none;
  margin: var(--sp-5) 0 0;
  display: grid; gap: var(--sp-2);
  color: var(--muted); font-size: 15.5px;
}
#work .cm-deliv li{ position: relative; padding-left: var(--sp-5); }
#work .cm-deliv li::before{
  content: ""; position: absolute; left: 0; top: .68em;
  width: 12px; height: var(--hair); background: var(--accent-line);
}

#work .cm-kit{
  margin-top: var(--sp-5);
  color: var(--muted); font-size: 15.5px; max-width: 68ch;
}

#work .cm-pull{
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: var(--hair) solid var(--line);
  color: var(--text); font-size: 17px; letter-spacing: -.2px; max-width: 60ch;
}


/* ------------------------------------------------------- shot index shared */

#work .cm-shots{ margin-top: var(--sp-8); }
#work .cm-h3{ margin-bottom: var(--sp-3); }
#work .cm-sub{ color: var(--muted); max-width: 62ch; font-size: 15.5px; }

/* the numbered row, used by both components */
#work .cm-n{ font-size: 12px; color: var(--faint); letter-spacing: .5px; }
#work .cm-t{ font-size: 17px; font-weight: 640; letter-spacing: -.2px; color: var(--text); }
#work .cm-m{ font-size: 11.5px; color: var(--faint); }

/* the reserved-slot plate. No src, no poster file, no invented asset. */
#work .cm-hold{
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-6);
  background: var(--cm-hatch), var(--panel2);
}
#work .cm-hold-k{
  font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--accent-ink);
}
#work .cm-hold-h{ font-size: 20px; font-weight: 640; letter-spacing: -.3px; color: var(--text); }
#work .cm-hold-p{ font-size: 14px; color: var(--muted); max-width: 54ch; }


/* ====================================================== desktop: hover index */

#work .cm-index{
  margin-top: var(--sp-6);
  display: grid;
  grid-template-columns: 268px minmax(0, 1fr);
  gap: var(--sp-6);
  align-items: start;
}

/* --- the rows ------------------------------------------------------------- */

#work .cm-list{ border-top: var(--hair) solid var(--line); }

#work .cm-row{
  width: 100%;
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  align-items: baseline;
  gap: var(--sp-1) var(--sp-3);
  min-height: 64px;
  padding: var(--sp-4) var(--sp-3) var(--sp-4) var(--sp-2);
  text-align: left;
  background: none;
  border: 0; border-bottom: var(--hair) solid var(--line);
  border-left: 2px solid transparent;
  cursor: pointer;
  transition: background-color var(--t-base) var(--ease),
              border-left-color var(--t-base) var(--ease),
              padding-left var(--t-base) var(--ease);
}
#work .cm-row .cm-m{ grid-column: 2; }

#work .cm-row:hover .cm-t,
#work .cm-row:focus-visible .cm-t{ color: var(--accent-ink); }
#work .cm-row:hover .cm-n,
#work .cm-row:focus-visible .cm-n{ color: var(--text); }

#work .cm-row[aria-selected="true"]{
  background: var(--accent-soft);
  border-left-color: var(--accent-line);
  padding-left: var(--sp-4);
}
#work .cm-row[aria-selected="true"] .cm-n{ color: var(--accent-ink); }
#work .cm-row[aria-selected="true"] .cm-t{ color: var(--text); }

/* --- the stage ------------------------------------------------------------ */

/* Base = the no-JS rendering: a plain stacked gallery, everything visible,
   1px seams drawn by the wrapper's own background showing through the gap. */
#work .cm-stage{
  position: relative;
  display: grid;
  gap: var(--hair);
  background: var(--seam);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-5);
  overflow: hidden;
}

#work .cm-frame{
  position: relative;
  opacity: 1; visibility: visible;
  aspect-ratio: var(--cm-stage-ar);
  overflow: hidden;
  background: var(--panel2);
}

/* Live = SZ.videoindex has taken over. One box, one frame on at a time. */
#work [data-videoindex][data-live="1"] .cm-stage{
  display: block;
  gap: 0;
  aspect-ratio: var(--cm-stage-ar);
}
#work [data-videoindex][data-live="1"] .cm-frame{
  position: absolute; inset: 0;
  aspect-ratio: auto;
  opacity: 0; visibility: hidden;
  transition: opacity var(--t-slow) var(--ease);
}
#work [data-videoindex][data-live="1"] .cm-frame[data-on="1"]{
  opacity: 1; visibility: visible;
}

/* the vertical clip is contained, not cropped: a 9:16 loop in a 16:10 box. The
   letterbox either side carries the same hatch as a reserved slot, so the empty
   field reads as part of the stage rather than as a loading state. */
#work .cm-frame-vert{ background: var(--cm-hatch), var(--panel2); }
#work .cm-frame-vert .sz-vi-poster,
#work .cm-frame-vert video{ object-fit: contain; }

/* stills: the 16:9 team frame fills the stage, the 4:3 directing frame sits in
   as a second card, so neither one gets cropped to a shape it was not shot in */
#work .cm-inset{
  position: absolute; z-index: 1;
  right: var(--sp-4); bottom: var(--sp-8);
  width: 32%; max-width: 232px;
  /* height:auto is load-bearing: the img carries width/height attributes, and
     a presentational height of 1200px would win over aspect-ratio, which only
     resolves when one axis is auto. */
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-3);
  border: var(--hair) solid var(--line2);
  box-shadow: var(--shadow-2);
}

#work .cm-cap{
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 0;
  padding: var(--sp-6) var(--sp-4) var(--sp-3);
  font-size: 12px; letter-spacing: .2px; color: var(--text);
  background: var(--cm-scrim);
  pointer-events: none;
}

#work .cm-frame-hold .cm-hold{ position: absolute; inset: 0; }
#work .cm-frame-hold .cm-hold-p{ margin-bottom: var(--sp-5); }


/* ================================================== mobile: its own component */

#work .cm-mobile{ display: none; }

#work .cm-mrows{ list-style: none; border-top: var(--hair) solid var(--line); }
#work .cm-mrow{ border-bottom: var(--hair) solid var(--line); }

#work .cm-mh{ font-size: inherit; font-weight: inherit; letter-spacing: 0; }

#work .cm-mbtn{
  width: 100%; min-height: 56px;
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto 14px;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-1);
  text-align: left;
  background: none; border: 0;
  cursor: pointer;
}
#work .cm-mbtn::after{
  content: "";
  grid-column: 4;
  width: 9px; height: 9px;
  border-right: 1.5px solid var(--faint);
  border-bottom: 1.5px solid var(--faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--t-slow) var(--ease), border-color var(--t-base);
}
#work .cm-mrow[data-open="0"] .cm-mbtn::after{
  transform: rotate(-45deg) translate(-1px, 1px);
}
#work .cm-mrow[data-open="1"] .cm-n{ color: var(--accent-ink); }

#work .cm-mpanel{ padding: var(--sp-2) 0 var(--sp-5); }
#work .cm-mrow[data-open="0"] .cm-mpanel{ display: none; }

#work .cm-hold-m{
  border: var(--hair) solid var(--line);
  border-radius: var(--r-4);
  padding: var(--sp-5);
  gap: var(--sp-2);
}

/* the media block. The player is a handoff target, not a layout element: it
   only exists on screen once a finger has asked for it. */
#work .cm-mmedia{
  position: relative;
  /* a vertical clip presented as a vertical card, capped so it does not eat the
     whole viewport on a phone */
  max-width: 320px; margin-inline: auto;
  border-radius: var(--r-4);
  overflow: hidden;
  background: var(--panel2);
  border: var(--hair) solid var(--line);
}
#work .cm-mposter{
  display: block; width: 100%; height: auto;
  aspect-ratio: 3 / 4; object-fit: cover;
}
#work .cm-mplayer{ display: none; width: 100%; height: auto; }
#work .cm-mmedia[data-playing="1"] .cm-mposter{ display: none; }
#work .cm-mmedia[data-playing="1"] .cm-mplayer{ display: block; }
#work .cm-mmedia[data-playing="1"] .cm-watch{ display: none; }

#work .cm-watch{
  position: absolute; left: 50%; bottom: var(--sp-5);
  transform: translateX(-50%);
  min-height: 44px; padding: 12px 20px;
  border-radius: var(--r-pill);
  border: var(--hair) solid var(--line2);
  background: var(--bg2);
  color: var(--text);
  font-size: 14.5px; font-weight: 600; font-family: var(--sans);
  cursor: pointer;
}
#work .cm-mnote{
  margin-top: var(--sp-3);
  font-size: 11.5px; color: var(--faint); text-align: center;
}

/* stills rail. Full bleed to the viewport edges, snap line on the content edge,
   card width solved by SZ.snappeek so the peek is 15% at every width. */
#work .cm-mstills{
  --sz-rail-edge: var(--gutter);
  --sz-rail-gap: var(--sp-3);
  margin-inline: calc(var(--gutter) * -1);
}
#work .cm-mstills .sz-rail-track{ padding-block: var(--sp-1); }
#work .cm-mstills .sz-rail-card img{
  display: block; width: 100%; height: auto;
  aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--r-3);
  border: var(--hair) solid var(--line);
}
#work .cm-mstills .sz-rail-card figcaption{
  margin-top: var(--sp-2); font-size: 11.5px; color: var(--faint);
}
#work .cm-mcount{
  margin: var(--sp-3) var(--gutter) 0;
  font-size: 11.5px; color: var(--faint);
}


/* ---------------------------------------------------------------- breakpoints */

/* the tablet middle: the rail column narrows before the component swaps */
@media (max-width: 1040px){
  #work .cm-index{ grid-template-columns: 216px minmax(0, 1fr); gap: var(--sp-5); }
}

/* the swap. Exactly one component is displayed, so no asset is fetched twice. */
@media (max-width: 900px){
  #work .cm-index{ display: none; }
  #work .cm-mobile{ display: block; }
}

@media (max-width: 760px){
  #work .cm-case{ padding: 20px; }
  #work .cm-pull{ font-size: 15.5px; }
  #work .cm-shots{ margin-top: var(--sp-7); }
}


/* ------------------------------------------------------------ reduced motion */
/* Zero running animation, final rendered values, layout unchanged. The index
   keeps working as a still swapper (SZ.videoindex's documented calm path:
   nothing autoplays, controls are exposed) and motion.css drops the frame
   videos to display:none with the poster underneath holding the box. */

@media (prefers-reduced-motion: reduce){
  #work *,
  #work *::before,
  #work *::after{
    animation: none !important;
    transition: none !important;
  }
  /* Nothing else is forced here on purpose. The frames' final values are the
     values they already carry: the gallery renders composed, the live stage
     renders its selected frame's poster, and no opacity is animated into
     place, so there is no first frame to correct. Forcing opacity:1 on the
     frames with !important would defeat the stage's own tab semantics and
     stack all four frames on top of each other. */
}


/* ==========================================================================
   sections/story.css  (carries #story AND #shipping)
   ========================================================================== */
/* ============================================================================
   story.css   #story  eight years to here   +   #shipping  still shipping
   ----------------------------------------------------------------------------
   Two sections, one stylesheet, clearly separated below. Every rule is scoped
   under #story or #shipping, including the media queries and the reduced-motion
   kill block. No keyframes are declared here: the only animation either section
   runs is the clock separator's blink, which lives in lib/motion.css as
   sz-blink, and this file parks it when the band is off screen.

   Nothing in this file introduces a hidden state. The three pre-reveal states in
   play (.rv, the line masks, the word swap) are all authored in tokens.css and
   motion.css behind html.motion-ready, so a browser with no JavaScript renders
   both sections composed: the full timeline, the final numbers, the finished
   headline, and every dated row.
   ========================================================================== */


/* ==========================================================================
   1. #story
   ========================================================================== */

#story{
  /* measured geometry, no token equivalent */
  --st-date-col: 132px;   /* the date gutter. fits "2021 to 2022" at 12px mono  */
  --st-dot: 7px;          /* rail node diameter                                 */
}

/* ------------------------------------------------------------- the head band */

#story .st-head{ max-width: 940px; }

/* The headline runs long once the swap lands on its second line, so it gets its
   own ramp rather than the global h2 clamp. At 1080 (the reading column) the
   finished sentence still sets on one line, which is the point: the reader
   watches one line rewrite itself, not a paragraph reflow. */
#story .st-h2{
  font-size: clamp(25px, 3.6vw, 38px);
  line-height: 1.08;
  margin-bottom: var(--sp-4);
}

/* The old words are absolutely positioned over the new ones while the swap is
   live, so the line never resizes mid-scrub. Both halves stay on one line from
   901px up, which is exactly where wordswap.js arms. */
#story .st-swap{ vertical-align: bottom; }
@media (min-width: 901px){
  #story .st-swap .sz-swap-old,
  #story .st-swap .sz-swap-new{ white-space: nowrap; }
}

#story .lead{ margin-top: 0; }

/* the three count-ups: in context, in prose, never a brag band */
#story .st-num,
#story .st-n{
  color: var(--accent-ink);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
#story .lead .st-num{ font-size: 1.05em; }

/* --------------------------------------------------------------- the timeline */

#story .st-tl{
  list-style: none;
  margin: var(--sp-8) 0 0;
  padding: 0;
}

#story .st-e{
  display: grid;
  grid-template-columns: var(--st-date-col) minmax(0, 1fr);
  align-items: start;
}

#story .st-when{
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
  padding-top: 4px;
  margin: 0;
}

/* The rail is the body column's left border, so it is one hairline the whole
   way down with no absolutely positioned track to keep in sync. */
#story .st-e-body{
  position: relative;
  border-left: var(--hair) solid var(--line);
  padding: 0 0 var(--sp-7) var(--sp-6);
}

/* Last entry: the rail fades out instead of stopping dead. The gradient is
   painted through the transparent border box (background-clip defaults to
   border-box), so it lands on the same 1px column the border occupied. */
#story .st-e:last-child .st-e-body{
  padding-bottom: 0;
  border-left-color: transparent;
  background: linear-gradient(180deg, var(--line) 0%, var(--line) 58%, transparent 100%)
              left top / 1px 100% no-repeat;
}

/* rail node, centred on the 1px rail */
#story .st-e-body::before{
  content: "";
  position: absolute;
  left: calc(var(--st-dot) / -2 - 1px);
  top: 7px;
  width: var(--st-dot);
  height: var(--st-dot);
  border-radius: 50%;
  background: var(--bg);
  border: var(--hair) solid var(--line2);
}

/* the now node */
#story .st-e:last-child .st-e-body::before{
  background: var(--accent);
  border-color: transparent;
  box-shadow: 0 0 0 4px var(--accent-soft);
}

/* Vertical rhythm has to survive linemask.js: after the split, the direct
   children of .st-e-body are .sz-line wrappers, not the h3 and p. * + * matches
   both shapes, so the spacing is identical with and without JavaScript. */
#story .st-e-body > * + *{ margin-top: var(--sp-3); }

#story .st-e-body h3{
  margin: 0;
  color: var(--text);
  font-weight: 700;
}

#story .st-co{
  display: block;
  margin-top: 2px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .01em;
  color: var(--accent-ink);
}

#story .st-e-body p{
  margin: 0;
  color: var(--muted);
  font-size: 16px;
  line-height: 1.62;
  max-width: 62ch;
}

/* ------------------------------------------------------------------ the close */

#story .st-close{
  margin: var(--sp-7) 0 0;
  padding-top: var(--sp-5);
  border-top: var(--hair) solid var(--line);
  color: var(--text);
  font-size: 19px;
  letter-spacing: -.2px;
  max-width: 40ch;
}

/* --------------------------------------------------------------- #story, small */

/* Not a squeezed desktop: the rail and the date gutter both go, and each entry
   becomes a hairline-separated block with the years as a kicker. */
@media (max-width: 760px){
  #story .st-e{
    display: block;
    margin-top: var(--sp-5);
    padding-top: var(--sp-5);
    border-top: var(--hair) solid var(--line);
  }
  #story .st-e:first-child{
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }
  #story .st-when{
    padding-top: 0;
    margin-bottom: var(--sp-2);
  }
  #story .st-e-body,
  #story .st-e:last-child .st-e-body{
    border-left: 0;
    background: none;
    padding: 0;
  }
  #story .st-e-body::before,
  #story .st-e:last-child .st-e-body::before{ display: none; }
  #story .st-e-body p{ font-size: 15.5px; }
  #story .st-close{ font-size: 17px; }
}


/* ==========================================================================
   2. #shipping
   ========================================================================== */

#shipping{
  --sh-date-col: 108px;   /* fits "08 Aug 2026" as a pill at 11px mono */
}

#shipping .sh-grid{
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-7);
  margin-top: var(--sp-7);
}

@media (min-width: 901px){
  #shipping .sh-grid{
    grid-template-columns: minmax(0, 1fr) 296px;
    gap: var(--sp-9);
    align-items: start;
  }
}

/* Between the two-column breakpoint and the full reading column, the heartbeat
   panel is a fixed 296px, so the 88px gutter is taken straight out of the log's
   text measure. Measured at 901px: 88px of gutter leaves rows 345px wide, and
   48px leaves them 385px. */
@media (min-width: 901px) and (max-width: 1080px){
  #shipping .sh-grid{ gap: var(--sp-7); }
}

/* ---------------------------------------------------------------- the log rows */

#shipping .sh-log{
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: var(--hair) solid var(--line);
}

#shipping .sh-row{ border-bottom: var(--hair) solid var(--line); }

#shipping .sh-row-i{
  display: grid;
  grid-template-columns: var(--sh-date-col) minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: baseline;
  margin: 0;
  padding: var(--sp-4) 0;
}

#shipping .sh-date{
  justify-self: start;
  display: inline-block;
  padding: 4px 10px;
  border: var(--hair) solid var(--line);
  border-radius: var(--r-pill);
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--faint);
  white-space: nowrap;
}

#shipping .sh-what{
  color: var(--muted);
  font-size: 15.5px;
  line-height: 1.6;
}
#shipping .sh-what b{ color: var(--text); font-weight: 650; }

/* Optional crop slot. Renders nothing until a row is given an <img>. */
#shipping .sh-crop{
  grid-column: 2;
  display: block;
  width: 148px;
  max-width: 100%;
  height: auto;
  margin-top: var(--sp-3);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-1);
}

/* ----------------------------------------------------------------- the heartbeat */

#shipping .sh-beat{
  border: var(--hair) solid var(--line);
  border-radius: var(--r-4);
  background: var(--panel-fill);
  padding: var(--sp-5);
}

/* Sticky is layout, not animation, so it survives reduced motion untouched. */
@media (min-width: 901px){
  #shipping .sh-beat{
    position: sticky;
    top: calc(var(--nav-h) + var(--sp-5));
  }
}

#shipping .sh-beat-h{
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--faint);
}

#shipping .sh-place{ margin: var(--sp-3) 0 0; }

/* The clock. Only the separator moves; the digits are formatted by the platform
   for the right zone, which is the whole reason this reads as precision. */
#shipping .sh-clock{
  display: block;
  margin: var(--sp-2) 0 var(--sp-4);
  font-family: var(--mono);
  font-size: clamp(30px, 3.2vw, 38px);
  line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
#shipping .sh-clock .sz-clock-sep{ color: var(--accent-ink); padding: 0 .03em; }
/* formatToParts emits the locale's spaces as their own literal spans. They are
   hidden so the gaps below are the only ones, and the meridiem and the zone can
   be set at their own sizes without a full-size space wedged in front. */
#shipping .sh-clock .sz-clock-literal{ display: none; }
#shipping .sh-clock .sz-clock-dayPeriod{
  font-size: .46em;
  color: var(--muted);
  margin-left: .3em;
  letter-spacing: 0;
}
#shipping .sh-clock .sz-clock-timeZoneName{
  font-size: .34em;
  color: var(--faint);
  margin-left: .45em;
  letter-spacing: .1em;
}

/* Off-screen the band runs nothing. story.js writes data-inview on .sh-beat and
   the blink parks here; the clock keeps updating its text node, because a
   stopped clock would be a lie and a text write is not an animation. */
#shipping .sh-beat[data-inview="false"] .sz-clock-sep{ animation-play-state: paused; }

#shipping .sh-beat-note{
  margin: 0;
  color: var(--muted);
  font-size: 14px;
  line-height: 1.55;
}

/* Stacked, not a two-column row: a formatted stamp with a zone name is longer
   than the panel is wide, and a wrapped value beside a wrapped label reads as a
   layout bug rather than a timestamp. */
#shipping .sh-deploy{
  margin: var(--sp-4) 0 0;
  padding-top: var(--sp-4);
  border-top: var(--hair) solid var(--line);
}
#shipping .sh-deploy[hidden]{ display: none; }

#shipping .sh-deploy-k{
  display: block;
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
}
#shipping .sh-deploy-v{
  display: block;
  margin-top: 3px;
  font-size: 13px;
  color: var(--muted);
}

/* ------------------------------------------------------------ #shipping, small */

@media (max-width: 560px){
  #shipping .sh-row-i{
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-2);
    padding: var(--sp-5) 0;
  }
  #shipping .sh-crop{ grid-column: 1; }
  #shipping .sh-clock{ font-size: 32px; }
}


/* ==========================================================================
   3. Failsafe and reduced motion
   ========================================================================== */

/* motion.css force-reveals [data-reveal], cascade items, line masks, and demo
   steps at the 8000ms failsafe, but .rv is a tokens.css primitive and is not on
   that list. These two rules put this section's .rv elements back on it, scoped,
   with no !important needed. */
html.motion-failsafe #story .rv,
html.motion-failsafe #shipping .rv{ opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce){
  #story *, #story *::before, #story *::after,
  #shipping *, #shipping *::before, #shipping *::after{
    animation: none !important;
    transition: none !important;
  }

  /* final rendered values, not first frames */
  html.motion-ready #story .rv,
  html.motion-ready #shipping .rv{ opacity: 1 !important; transform: none !important; }

  /* line masks render open, and the clip boxes stop clipping */
  #story .sz-line, #shipping .sz-line{ overflow: visible !important; }
  #story .sz-line-i, #shipping .sz-line-i{ transform: none !important; }

  /* the headline renders its second line statically, which is the finished
     reframe and the one that should be read anyway */
  #story .sz-swap-old{ display: none !important; }
  #story .sz-swap-new{ display: inline !important; transform: none !important; opacity: 1 !important; }
}


/* ==========================================================================
   sections/about.css
   ========================================================================== */
/* =============================================================================
   about.css   #about  One person, end to end
   -----------------------------------------------------------------------------
   Every rule is scoped under #about, including media queries. Prefix ab-.
   No keyframes are declared here: this section has no loop of its own. Its
   three moving parts (odometer reels, snap rail, directional hover fill) are
   lib modules and their mechanical CSS already lives in lib/motion.css. What
   follows is colour, geometry, and the two breakpoint components.

   Nothing here hides content. There is no opacity:0 rule in this file outside
   the .rv primitive, which tokens.css already gates behind html.motion-ready.
   ============================================================================= */

#about{
  /* Local values, declared inside the section scope so they never leak.
     --ab-scrim is a flat translucent fill, deliberately NOT a backdrop-filter:
     the page's whole composited-glass budget is spent on the nav. */
  --ab-scrim: rgba(7, 8, 10, .84);
  --ab-card-r: var(--r-6);
  --ab-media-ar: 16 / 10;
}


/* ============================================================ beat 0: bio */

/* Desktop: headline and body share the left column, the frame holds the right.
   Mobile: one column in DOM order, which puts the frame between the headline
   and the body. That is the order the direction memo asks for, and it also
   keeps the section heading first for a screen reader. */
#about .ab-bio{
  display: grid;
  gap: var(--sp-5) var(--sp-8);
  grid-template-columns: minmax(0, 1.02fr) minmax(0, .98fr);
  /* auto then 1fr, not two autos. The frame spans both rows and is taller than
     the copy, and a spanning item distributes its height across the rows it
     spans: with two auto rows that surplus lands half in the headline row and
     pushes the body copy into the middle of nowhere. Pinning row 1 to the
     headline's own height and letting row 2 absorb the rest keeps the
     paragraph directly under the headline where it belongs. */
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "head fig"
    "body fig";
  align-items: start;
}
#about .ab-h2{ grid-area: head; margin-bottom: 0; }
#about .ab-fig{ grid-area: fig; }
#about .ab-lead{ grid-area: body; max-width: 34em; }

/* height:auto is load-bearing, not tidiness. The <img> carries width and height
   attributes (they reserve the box and stop layout shift), and the height
   attribute lands as a presentational hint of 1200px. aspect-ratio only applies
   when one axis is auto, so without this the frame renders 1200px tall and the
   hint silently wins. */
#about .ab-fig img{
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-5);
  border: var(--hair) solid var(--line);
}
#about .ab-figcap{
  margin-top: var(--sp-3);
  color: var(--faint);
  font-size: 12px;
  letter-spacing: .2px;
}


/* ================================================== shared block furniture */

#about .ab-block{ margin-top: var(--sp-10); }

#about .ab-h3{
  font-size: 13px;
  font-family: var(--mono);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 500;
  margin-bottom: var(--sp-5);
}


/* =================================================== beat 1: persona rail */

/* The two-tone split inside one sentence: the audience list recedes, the
   promise lands in full ink. One sentence, no second paragraph. */
#about .ab-split{
  font-size: clamp(21px, 2.7vw, 29px);
  line-height: 1.38;
  letter-spacing: -.4px;
  max-width: min(920px, 100%);
  color: var(--text);
  margin-bottom: var(--sp-7);
}
#about .ab-split .ab-dim{ color: var(--muted); }
#about .ab-split b{ color: var(--text); font-weight: 700; }

/* Base state is the rail, so a browser that never matches the desktop query
   still gets a working component. --sz-rail-edge and --sz-rail-gap are
   motion.css's own knobs for the snap track. */
#about .ab-rail{
  --sz-rail-gap: var(--sp-4);
  --sz-rail-edge: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
}
#about .ab-track{ padding-block: 2px; align-items: stretch; }

#about .ab-railcount{
  margin-top: var(--sp-4);
  padding-inline: var(--gutter);
  color: var(--faint);
  font-size: 11.5px;
  letter-spacing: 1px;
}

#about .ab-card{
  display: flex;
  flex-direction: column;
  background: var(--panel-fill);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-5);
  overflow: hidden;
  transition: border-color var(--t-base) var(--ease);
}
#about .ab-card:hover{ border-color: var(--line2); }

#about .ab-media{
  position: relative;
  aspect-ratio: var(--ab-media-ar);
  overflow: hidden;
  border-bottom: var(--hair) solid var(--line);
  background: var(--panel2);
}
#about .ab-media img{
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* The result chip replaces Flowty's cluster of three stock faces. A real
   outcome in the corner where an invented crowd used to sit. */
#about .ab-chip{
  position: absolute;
  left: var(--sp-3);
  bottom: var(--sp-3);
  right: var(--sp-3);
  width: fit-content;
  max-width: calc(100% - var(--sp-3) * 2);
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 6px 10px;
  border-radius: var(--r-pill);
  border: var(--hair) solid var(--line2);
  background: var(--ab-scrim);
  color: var(--text);
  font-size: 11.5px;
  letter-spacing: .2px;
  line-height: 1.35;
}
#about .ab-d{
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ab-dot, var(--live));
  box-shadow: 0 0 7px var(--ab-dot, var(--live));
}

/* The reserved frame. Same grammar the wall uses for a build that could not be
   captured: a dashed box that says what is missing instead of a stand-in
   image. The full description stays in the DOM for a screen reader. */
#about .ab-reserved{
  background: none;
  border: 1px dashed var(--line2);
  border-radius: calc(var(--r-5) - 1px) calc(var(--r-5) - 1px) 0 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  justify-items: center;
  gap: 3px;
  text-align: center;
  padding: var(--sp-5);
}
#about .ab-res-name{ color: var(--text); font-size: 14px; font-weight: 640; }
#about .ab-res-note{ color: var(--faint); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; }

#about .ab-cbody{
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-5);
  flex: 1 1 auto;
}
#about .ab-ctitle{ font-size: 17px; letter-spacing: -.3px; line-height: 1.25; }
#about .ab-ctext{ color: var(--muted); font-size: 14.5px; }
#about .ab-cfoot{ margin-top: auto; padding-top: var(--sp-2); }


/* ====================================================== beat 2: the number */

/* One number, alone on its own band. The plate behind it is a CSS gradient,
   not a raster: nothing here is worth a network request. */
#about .ab-band{
  margin: var(--sp-10) 0 0;
  padding: var(--sp-9) 0;
  border-top: var(--hair) solid var(--line);
  border-bottom: var(--hair) solid var(--line);
  background:
    radial-gradient(120% 130% at 50% 0%, var(--accent-soft), transparent 62%),
    var(--bg2);
}
#about .ab-odo{
  display: grid;
  gap: var(--sp-5) var(--sp-8);
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  align-items: center;
}
#about .ab-num{
  font-size: clamp(58px, 11vw, 132px);
  font-weight: 800;
  letter-spacing: -.05em;
  /* line-height 1 is load-bearing: odometer.js clips each reel to exactly 1em
     and stacks twenty 1em rows inside it, so any other value misaligns the
     landing row against the static "/" beside it. */
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
#about .ab-odo-h3{
  font-size: 21px;
  letter-spacing: -.3px;
  margin-bottom: var(--sp-3);
}
#about .ab-odo-p{
  color: var(--muted);
  font-size: 16px;
  max-width: 46em;
}
/* The citation is underlined on purpose. It is the difference between a number
   you are asked to believe and a number you can go and check. */
#about .ab-cite{
  color: var(--accent-ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-thickness var(--t-fast) var(--ease);
}
#about .ab-cite:hover{ text-decoration-thickness: 2px; }


/* ===================================================== beat 3: the receipts */

#about .ab-sub{
  color: var(--text);
  font-size: 18px;
  max-width: 46em;
  margin-bottom: var(--sp-6);
}

#about .ab-receipts{
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
}

#about .ab-rc{
  /* motion.css already gives [data-hoverfill] position:relative and
     isolation:isolate, which is what lets the z-index:-1 fill tile paint over
     this background and under this text. Only the ink values are ours. */
  --sz-fill-ink: var(--text);
  --sz-fill-on: var(--bg);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  height: 100%;
  min-height: 44px;
  padding: var(--sp-5);
  background: var(--panel-fill);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-4);
  overflow: hidden;
  transition: border-color var(--t-base) var(--ease), color var(--t-slow) var(--ease);
}
#about .ab-rc:hover{ border-color: var(--line2); }

#about .ab-rc-top{ display: block; margin-bottom: var(--sp-1); }
#about .ab-rc-name{ font-size: 16.5px; font-weight: 650; letter-spacing: -.2px; }
#about .ab-rc-note{ color: var(--muted); font-size: 13.5px; line-height: 1.55; }
#about .ab-rc-go{
  margin-top: auto;
  padding-top: var(--sp-4);
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  color: var(--accent-ink);
  overflow-wrap: anywhere;
}
#about .ab-arrow{ transition: transform var(--t-base) var(--ease); }
#about .ab-rc:hover .ab-arrow{ transform: translateX(3px); }

/* Filled state. hoverfill sets data-filled and motion.css flips the root's
   colour to --sz-fill-on; every child that carried its own colour has to
   follow it or the tile ends up with mint type on a near-white ground. The
   status dot borrows currentColor for the same reason. */
#about .ab-rc[data-filled="1"] .ab-rc-note,
#about .ab-rc[data-filled="1"] .ab-rc-go,
#about .ab-rc[data-filled="1"] .status{ color: inherit; }
#about .ab-rc[data-filled="1"] .status .d{ background: currentColor; box-shadow: none; }


/* ============================================================= breakpoints */

/* Desktop: the rail stops being a rail. Three cards, one grid, no scroll.
   snappeek keeps measuring and keeps writing --sz-card-w, which a grid item
   ignores, so there is nothing to tear down at this width. */
@media (min-width: 901px){
  #about .ab-rail{
    --sz-rail-edge: 0px;
    margin-inline: 0;
  }
  #about .ab-track{
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    overflow: visible;
    scroll-snap-type: none;
  }
  #about .ab-card{ border-radius: var(--ab-card-r); }
  #about .ab-reserved{ border-radius: calc(var(--ab-card-r) - 1px) calc(var(--ab-card-r) - 1px) 0 0; }
  #about .ab-railcount{ display: none; }
}

/* Between the rail and the full desktop grid: two cards, still a rail. */
@media (max-width: 900px){
  #about .ab-bio{
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "fig" "body";
    gap: var(--sp-5);
  }
  #about .ab-fig img{ aspect-ratio: 3 / 2; }
  #about .ab-lead{ max-width: none; }

  #about .ab-odo{
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  #about .ab-receipts{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 620px){
  #about .ab-block{ margin-top: var(--sp-9); }
  #about .ab-band{ margin-top: var(--sp-9); padding: var(--sp-8) 0; }
  #about .ab-receipts{ grid-template-columns: minmax(0, 1fr); }
  #about .ab-media{ aspect-ratio: 3 / 2; }
  #about .ab-chip{ font-size: 11px; }
  #about .ab-sub{ font-size: 17px; }
}


/* ========================================================== reduced motion */
/* Zero running animation, final rendered values, layout unchanged. The three
   lib modules decline to arm on their own (odometer never builds the reels,
   hoverfill never binds, snappeek only drops smooth scrolling), so what is
   left for this file is its own transitions. The odometer renders 100/100 as
   plain authored digits, which is the point of authoring it as text. */
@media (prefers-reduced-motion: reduce){
  #about *,
  #about *::before,
  #about *::after{
    animation: none !important;
    transition: none !important;
  }
  #about .ab-track{ scroll-behavior: auto !important; }
}


/* ==========================================================================
   sections/lightzone.css  (#cv, #faq, #contact)
   ========================================================================== */
/* ============================================================================
   THE LIGHT ZONE: #cv, #faq, #contact.
   ----------------------------------------------------------------------------
   Every rule in this file is scoped under #cv, #faq or #contact. There are no
   keyframes here at all: the band's one "effect" is the palette flip itself,
   which is a state, not an animation, and the FAQ is a reading surface.

   THE PALETTE IS NOT THIS FILE'S JOB. data-zone="light" in the markup redeclares
   every token for these three subtrees (tokens.css §2), so everything below asks
   for --text, --panel, --line, --accent-ink exactly as a dark section would and
   resolves to the light values without knowing it. Not one hex code in this file
   is a colour: the only raw values are measured geometry (form width, chevron
   size, the select caret data URI, touch-target minimums).

   WHAT MOVES, AND FOR HOW LONG
     · accordion chevron        --t-slow (.22s), the top of the interaction band
     · input border on focus    --t-base (.16s)
     · button hover             tokens.css owns it
     · the pointer bloom        lib/bloom.js, desktop only, declines under
                                reduced motion (its layer is display:none)
   Nothing else. At rest this band runs zero animations, which is what keeps the
   page inside its 2-band ration when a reader parks on the CV form.

   NOTHING HERE IS EVER HIDDEN WITHOUT THE GATE. The only display:none states in
   this file are #cverr and #cvok, which are empty status surfaces that JS fills.
   With JS off they are correctly absent, not content withheld. Every word of
   copy, every form control, and all six FAQ answers render with no JavaScript.
   ========================================================================== */


/* ============================================================================
   §0  The failsafe hole, patched for this band
   ----------------------------------------------------------------------------
   tokens.css hides .rv under html.motion-ready. lib/motion.css's 8000ms
   .motion-failsafe force-reveal lists [data-reveal], [data-cascade], line masks
   and demo steps, but NOT .rv, so a script that dies before the reveal observer
   is armed would leave every .rv element invisible past the failsafe. Three
   id-scoped selectors close that hole for this band without editing a file this
   section does not own. (#cv is the ask: it is the last thing on the page that
   may quietly fail to render.)
   ========================================================================== */
html.motion-failsafe #cv .rv,
html.motion-failsafe #faq .rv,
html.motion-failsafe #contact .rv{ opacity: 1; transform: none; }


/* ============================================================================
   §1  #cv, the decision band
   ========================================================================== */

/* Centred, the way the shipping gate is: this is the one place on the page
   asking for something, and a centred column reads as an ask rather than as
   another chapter of the walkthrough. */
#cv{ text-align: center; }
#cv .label{ justify-content: center; }
#cv .lead{ margin-left: auto; margin-right: auto; }

/* ---- the form ------------------------------------------------------------
   520px is the measured comfortable width for one email field plus one select:
   wide enough that a long address does not scroll, narrow enough that the eye
   does not have to travel back across the band to find the next control.

   This element is also the [data-bloom] root. lib/motion.css gives it
   position:relative and lifts its children above the injected layer. The bloom
   colours below are retuned for the light ground, because the library defaults
   were measured against black. */
#cv .cv-form{
  max-width: 520px;
  margin: var(--sp-6) auto 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  text-align: left;

  --sz-bloom-c1: rgba(13,111,91,.30);    /* the light zone accent, teal        */
  --sz-bloom-c2: rgba(30,79,216,.16);    /* --accent2's family                 */
  --sz-bloom-c3: rgba(20,22,26,.10);     /* a plain shadow, to ground the pair */
  --sz-bloom-r1: 190px;
  --sz-bloom-r2: 260px;
  --sz-bloom-r3: 330px;
}

#cv .cv-form input,
#cv .cv-form select{
  width: 100%;
  min-height: 48px;                       /* touch target, and it sets rhythm  */
  padding: 13px 16px;
  background: var(--panel);
  border: var(--hair) solid var(--line2);
  border-radius: var(--r-2);
  color: var(--text);
  font-family: var(--sans);
  font-size: 15px;
  appearance: none;
  -webkit-appearance: none;
  transition: border-color var(--t-base) var(--ease);
}

/* The caret has to be drawn, because appearance:none removes the native one.
   Stroke is --muted's value: it cannot be a token inside a data URI. */
#cv .cv-form select{
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%234e545e' stroke-width='2' fill='none'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 42px;
}

#cv .cv-form input::placeholder{ color: var(--faint); }

/* Focus moves the border AND keeps the global focus ring. The shipping page
   set outline:none here; that is the one thing CONTRACT §5 will not allow, so
   the border is an addition to the ring rather than a replacement for it. */
#cv .cv-form input:focus,
#cv .cv-form select:focus{ border-color: var(--accent-line); }

#cv .cv-form input[aria-invalid="true"]{ border-color: var(--build); }

#cv .cv-send{
  justify-content: center;
  min-height: 48px;
  margin-top: var(--sp-1);
}

/* ---- validation and status ------------------------------------------------
   Both are empty-by-default surfaces the script fills. .show is the class the
   gate's own code toggles and it is kept exactly as coded. */
#cv .cv-err{
  display: none;
  margin-top: calc(var(--sp-1) * -0.5);
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--build);
}
#cv .cv-err.show{ display: block; }

#cv .cv-ok{
  display: none;
  max-width: 520px;
  margin: var(--sp-5) auto 0;
  padding: 16px 20px;
  border: var(--hair) solid var(--accent-line);
  background: var(--accent-soft);
  border-radius: var(--r-3);
  color: var(--text);
  font-size: 15px;
  text-align: left;
}
#cv .cv-ok.show{ display: block; }
#cv .cv-ok .d{
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--live);
  margin-right: 9px;
}

#cv .cv-note{
  margin-top: var(--sp-4);
  font-family: var(--mono);
  font-size: 12px;
  color: var(--faint);
}


/* ============================================================================
   §2  #faq, the reading surface
   ----------------------------------------------------------------------------
   Native <details>/<summary>. No JS accordion, so it works with scripting off,
   it is keyboard-operable for free, and find-in-page can open a closed answer.
   ========================================================================== */

/* Tighter top than a full band: #cv and #faq are two beats of one chapter, and
   a full --sp-band on both makes the light zone read as two separate pages. */
#faq{ padding-top: var(--sp-8); }

#faq .fq-list{
  margin-top: var(--sp-6);
  max-width: 820px;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

#faq .fq-item{
  border: var(--hair) solid var(--line);
  border-radius: var(--r-3);
  background: var(--panel);
  overflow: hidden;
  transition: border-color var(--t-base) var(--ease);
}
#faq .fq-item:hover{ border-color: var(--line2); }
#faq .fq-item[open]{ border-color: var(--line2); }

#faq summary{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  min-height: 56px;                       /* comfortably past the 44px floor   */
  padding: 16px 20px;
  cursor: pointer;
  list-style: none;
  color: var(--text);
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.35;
}
#faq summary::-webkit-details-marker{ display: none; }
#faq summary::marker{ content: ""; }

/* The chevron: two borders on a rotated square. Down when closed, up when open.
   .22s is the top of the interaction band and the value CONTRACT §5 names for
   accordion chevrons specifically. The translate keeps the optical centre still
   while the box rotates around its own middle. */
#faq .fq-chev{
  flex: none;
  width: 9px; height: 9px;
  border-right: 2px solid var(--accent-ink);
  border-bottom: 2px solid var(--accent-ink);
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--t-slow) var(--ease);
}
#faq details[open] .fq-chev{ transform: translateY(2px) rotate(-135deg); }

#faq .fq-a{
  padding: 0 20px 18px;
  color: var(--muted);
  font-size: 15.5px;
  line-height: 1.62;
  max-width: 68ch;
}
#faq .fq-a a{
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}


/* ============================================================================
   §3  #contact, the closer
   ========================================================================== */

#contact{ text-align: center; padding-top: var(--sp-8); }
#contact .label{ justify-content: center; }
#contact .lead{ margin-left: auto; margin-right: auto; }

/* The bloom root is padded rather than tight to the buttons, so the pointer
   crosses into the container and the blobs start fanning out before the cursor
   reaches a target. Negative side margins keep the padding from narrowing the
   reading column. Same colour retune as the CV form. */
#contact .ct-bloom{
  margin: var(--sp-6) calc(var(--sp-5) * -1) 0;
  padding: var(--sp-6) var(--sp-5);

  --sz-bloom-c1: rgba(13,111,91,.30);
  --sz-bloom-c2: rgba(30,79,216,.16);
  --sz-bloom-c3: rgba(20,22,26,.10);
  --sz-bloom-r1: 190px;
  --sz-bloom-r2: 280px;
  --sz-bloom-r3: 360px;
}

#contact .ct-links{
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  justify-content: center;
}
#contact .ct-links .btn{ min-height: 48px; }


/* ============================================================================
   §4  Mobile
   ----------------------------------------------------------------------------
   The band is a column of controls and a stack of accordions at every width, so
   there is no separate mobile component to build here. What changes is what a
   thumb needs: full-width controls, a stacked CTA column instead of a wrapped
   row (three wrapped pill buttons on a 390px screen produce a ragged two-line
   block with one orphan), and tighter accordion padding.
   ========================================================================== */

@media (max-width: 760px){
  #cv .cv-form{ max-width: none; }
  #cv .cv-ok{ max-width: none; }

  #faq{ padding-top: var(--sp-7); }
  #faq summary{ padding: 15px 16px; font-size: 16px; }
  #faq .fq-a{ padding: 0 16px 16px; font-size: 15px; }

  #contact{ padding-top: var(--sp-7); }
  #contact .ct-bloom{ margin-left: 0; margin-right: 0; padding: var(--sp-5) 0; }
  #contact .ct-links{ flex-direction: column; align-items: stretch; }
  #contact .ct-links .btn{ width: 100%; justify-content: center; }
}


/* ============================================================================
   §5  prefers-reduced-motion: the kill switch
   ----------------------------------------------------------------------------
   Nothing in this band is ever held at opacity 0 by this file, so there are no
   final values to restore: killing every transition is the whole job. The
   chevron still rotates, instantly, because open and closed must stay visually
   distinguishable. bloom.js declines to arm on its own and lib/motion.css sets
   its layer to display:none, so no bloom exists to kill here.
   ========================================================================== */

@media (prefers-reduced-motion: reduce){
  #cv *, #cv *::before, #cv *::after,
  #faq *, #faq *::before, #faq *::after,
  #contact *, #contact *::before, #contact *::after{
    animation: none !important;
    transition: none !important;
  }
}


/* ==========================================================================
   core/footer.css
   ========================================================================== */
/* ============================================================================
   footer.css: the sign-off band
   ----------------------------------------------------------------------------
   OWNER: shell agent. Loaded last. Everything is scoped to .ft / .ft-*.

   Zero animation at rest. The only moving part in this band is the fit-to-box
   measurement, which runs on layout changes (ResizeObserver, fonts.ready,
   resize) and never on a timer.
   ========================================================================== */

.ft{
  padding: var(--sp-8) 0 var(--sp-7);
  border-top: var(--hair) solid var(--line);
  background: var(--bg);          /* opaque: hard cut back to dark after the
                                     light zone, no ambient bleed-through      */
  color: var(--faint);
  font-size: 13px;
  position: relative;
  z-index: 0;
  overflow: hidden;               /* the wordmark is measured to fit, but a
                                     stale --sz-fitbox-size during resize must
                                     never
                                     produce a horizontal scrollbar            */
}


/* ---- the giant wordmark ----------------------------------------------------
   The fit-to-box mechanic: a binary search between 16px and 2000px writes
   --sz-fitbox-size on this element, accepted at the largest size where the sum
   of the letter span widths plus the gaps still fits the container. The
   variable name has to match what lib/fitbox.js actually writes: it used to
   read --fit-size here, which no module ever set, so the wordmark stayed at the
   raw clamp and overran its box at 390px. Re-run on
   ResizeObserver, fonts.ready, and resize, rAF-debounced.

   The fallback in the var() is what a JS-off browser gets. It is a clamp tuned
   to land near the fitted size at common widths, so nothing jumps when the
   script does run: 8 letters at this weight measure roughly 5.1em total, and
   1032px of container divided by 5.1 is about 202px, so the ceiling is 196px:
   measured JS-off at 1280px, a 200px cap put 1045px of letters in a 1032px box.
   The vw slope is 17.4 and
   not 19: measured at 390px the fitted size is 68.3px, and 19vw of 390 is 74.1,
   which overran the 358px box by 30px and was only being swallowed by the
   overflow:hidden above. 17.4vw of 390 is 67.9, just inside the fit.

   Colour: a vertical fade of --text rather than a flat tint. It is aria-hidden
   and purely graphic, so it carries no contrast floor. */
.ft-mark{
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.02em;
  font-family: var(--sans);
  font-weight: 800;
  font-size: var(--sz-fitbox-size, clamp(40px, 17.4vw, 196px));
  line-height: .82;
  letter-spacing: -.02em;
  margin-bottom: var(--sp-7);
  user-select: none;
  background: linear-gradient(180deg,
              color-mix(in srgb, var(--text) 24%, transparent),
              color-mix(in srgb, var(--text) 6%, transparent));
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}
.ft-mark span{ display: block; }

/* If color-mix or background-clip:text is unavailable the gradient falls back
   to nothing and `color:transparent` would erase the wordmark entirely. Guard
   it: without background-clip support, paint a flat tint instead. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)){
  .ft-mark{ background: none; color: rgba(232,234,239,.14); }
}


/* ---- rows ---------------------------------------------------------------- */
.ft-rows{ display: flex; flex-direction: column; gap: var(--sp-4); }

.ft-row{
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: var(--sp-3);
}
.ft-row-quiet{ font-size: 12px; }

.ft-copy{ color: var(--muted); }

.ft-links{ display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.ft-links a{ color: var(--muted); transition: color var(--t-fast); min-height: 44px; display: inline-flex; align-items: center; }
.ft-links a:hover{ color: var(--accent-ink); }

/* the reachable half of the glass escape hatch. Styled as a footer link because
   that is what it is; it is a <button> because it acts on the page rather than
   navigating. */
.ft-glass{
  font: inherit; color: var(--muted); background: none; border: 0; padding: 0;
  cursor: pointer; transition: color var(--t-fast);
  min-height: 44px; display: inline-flex; align-items: center;
}
.ft-glass:hover{ color: var(--accent-ink); }
.ft-glass[aria-pressed="true"]{ color: var(--accent-ink); }

/* authorship as one hyperlink: the claim links to the page that documents it */
.ft-metal{
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--line3);
  transition: color var(--t-fast), text-decoration-color var(--t-fast);
}
.ft-metal:hover{ color: var(--accent-ink); text-decoration-color: var(--accent-line); }

.ft-t{ color: var(--faint); }


@media (max-width: 760px){
  .ft{ padding: var(--sp-7) 0 var(--sp-6); }
  .ft-mark{ margin-bottom: var(--sp-6); }
  .ft-row{ flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
  .ft-links{ gap: var(--sp-5); }
}


/* Nothing animates here, so there is nothing for reduced motion to stop. The
   fit-to-box measurement is layout, not animation, and is left running: turning
   it off would leave the wordmark at its fallback size, which is a worse
   outcome than a correctly sized static element. */


/* ==========================================================================
   INTEGRATOR PATCH (owned by index.html/site.css, not by any fragment)
   ========================================================================== */

/* THE .rv FAILSAFE HOLE.
   tokens.css hides `.rv` under html.motion-ready and reveals it on `.rv.in`.
   CONTRACT §6 assigns the observer that adds `.in` to a module called motion.js
   which does not exist in lib/; site.js ships that observer instead (see its
   init block). lib/motion.css's `.motion-failsafe` force-reveal list covers
   [data-reveal], [data-cascade], line masks and demo steps but NOT `.rv`, so a
   script that dies before the observer arms would leave every section head
   invisible past the 8000ms failsafe. One rule closes it, page-wide. It only
   ever applies when something really failed, because SZ.bootOk() disarms.
   Specificity ties html.motion-ready .rv and wins on order. */
html.motion-failsafe .rv{ opacity: 1 !important; transform: none !important; }
