/* ══════════════════════════════════════════════════════════════════════
   MUUS base stylesheet — shared across every page (home + subpages).
   Page-specific section styles stay inline on their own page; anything
   here is either a design token or a component reused site-wide, so a
   future palette/type change only has to happen once. ══════════════════ */

:root{
  /* ── MUUS studio palette: warm charcoal ground, muted taupe and sage
     neutrals, soft cream text, one quiet butter-yellow accent. This is the
     DARK theme — the site's original, still the default with no JS/pref.
     Every color below that participates in the light-theme swap (see the
     "LIGHT THEME" block further down) is also expressed as a bare R,G,B
     triplet ("-rgb") so components can build their own alpha tints with
     rgba(var(--x-rgb),N) and still repaint correctly when the theme flips —
     a literal rgba(242,237,224,.12) never would. ── */
  --bg:#3d3c38;                /* warm charcoal — page ground */
  --bg-rgb:61,60,56;
  --white:#f2ede0;             /* cream — primary text on dark */
  --white-rgb:242,237,224;
  --green:#eefb87;             /* electric lime — the one accent, matches the brand mark */
  --green-rgb:238,251,135;
  --accent:#eefb87;
  --accent-rgb:238,251,135;
  --taupe:#78735f;             /* secondary neutral — decorative only, constant across themes */
  --sage:#d6e2dc;              /* tertiary cool neutral, used sparingly — constant across themes */
  --sage-rgb:214,226,220;
  --void:#1c1a17;              /* recessed dark bands — full-bleed photo/video breaks. A deliberate
     constant, not a themeable surface: these are cinematic bands (hero
     scrims, .contact, .bolt-play, the command palette) that stay dark by
     content design regardless of reader theme preference — see "NOIR"
     tokens below, which is what actually paints text/accents on top of it. */
  /* the triplet form, for the same reason every other -rgb token exists: a
     band edge has to ramp INTO the ground of its neighbour, and a neighbour
     is sometimes --void rather than --bg. Frozen alongside --void itself. */
  --void-rgb:28,26,23;
  --glass:rgba(var(--white-rgb),.12);
  --glass-line:rgba(var(--white-rgb),.16);
  /* ── SURFACE — the plate a .card / .stat paints itself on. In the dark
     theme this IS --glass, so the three values below resolve byte-for-byte
     to what those components already rendered; the token exists only
     because light needs the opposite *construction*, not a different
     opacity. --glass is an ink tint: on the dark ground it lifts a pane
     out of the page, but on the light ground it drags the card DOWN toward
     whatever photo sits behind it, so plate and background converge into
     one muddy mid-tone with no separation — measured 1.6-4.3:1 for card
     body copy, i.e. the manifesto cards reading as invisible. Light builds
     the plate from --paper instead: near-opaque stock laid on the
     photograph, which is how an editorial spread actually works. ── */
  --surface:rgba(var(--white-rgb),.12);
  --surface-line:rgba(var(--white-rgb),.16);
  --surface-hover:rgba(var(--white-rgb),.22);
  --dim:rgba(var(--white-rgb),.62);
  /* was .42 — measured contrast against real WCAG math (not eyeballed):
     2.8-3.7:1 across the site's grounds, failing AA's 4.5:1 for normal
     text everywhere it's used (.tag eyebrows, footer meta, case-page
     field labels). .55 clears 3:1 everywhere and 4.5:1 on --void/--bg,
     while staying visibly quieter than --dim — the tertiary role intact,
     just no longer illegible. */
  --dim-2:rgba(var(--white-rgb),.55);
  --paper:#48463f;             /* lifted section ground — lighter than --bg (dark-mode elevation) */
  --paper-rgb:72,70,63;
  --ink:#1f1e1b;                /* dark text ON the accent chip — flips light in the light theme,
     since the accent itself goes from light to dark there (see below). */
  --error:#ee8888;             /* form/status error red (onboard.html) */

  /* ── NOIR — a small, intentionally frozen palette for chrome that floats
     over arbitrary content (the nav bar, the primary .btn, the custom
     cursor, the command palette, the persistent case cta-bar) or that
     paints on top of the permanently-dark --void bands (.contact,
     .bolt-play). None of these six are redeclared in the light-theme block
     below — that's what keeps them constant. Without this split, flipping
     --white/--green globally would leave e.g. the "Work with MUUS" button
     rendering dark-ink text on its own dark glass shell in light mode. ── */
  --noir-fg:#f2ede0;
  --noir-dim:rgba(242,237,224,.62);
  --noir-dim-2:rgba(242,237,224,.55);
  --noir-accent:#eefb87;
  --noir-ink:#1f1e1b;
  --noir-glass:rgba(242,237,224,.12);
  --noir-glass-line:rgba(242,237,224,.16);

  --sans:'Zalando Sans SemiExpanded',sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,monospace;
  --serif:'Zalando Sans SemiExpanded',sans-serif;
  --ease:cubic-bezier(.22,1,.36,1);
  --ease-back:cubic-bezier(.34,1.56,.64,1); /* confident overshoot — cards/stats settle past 100% then ease back */
  --gut:clamp(20px,5vw,80px);

  /* ── MOTION TIMING SCALE ──────────────────────────────────────────────
     The site had 18 distinct transition durations across 91 declarations,
     which is why hovers read "almost right" — two properties on the same
     element routinely resolved on different clocks. Five roles, one value
     each. Not every declaration is migrated yet (see the notes on the
     hover/zoom families below, which are); these are the target. */
  --dur-1:.16s;   /* instant state flips — active/disabled/opacity toggles */
  --dur-2:.28s;   /* hover colour + border                                 */
  --dur-3:.5s;    /* lifts, tilts, scroll reveals                          */
  --dur-4:.9s;    /* headline masks, specular sweep                        */
  --dur-5:1.4s;   /* hero open                                             */
  --dur-zoom:1.2s;/* the one image/video ken-burns zoom on hover — was
     four different values (1.1/1.2/1.6/1.6s) for one identical gesture. */

  /* ── INTERACTION GEOMETRY ─────────────────────────────────────────────
     The timing scale above unified WHEN a hover resolves; these unify HOW
     FAR it travels. Measured across the shipped stylesheets, one gesture —
     "a surface rises to meet the cursor" — was authored at five distinct
     distances (-2 / -3 / -4 / -6 / -8px) and one gesture — "media inside a
     hovered card pushes in" — at four distinct scales (1.04 / 1.045 / 1.05
     / 1.06). Nothing distinguished them: a manifesto .card lifted 8px and a
     case card 6px for the same reason, on the same page, 400px apart. Two
     roles, one value each. The split that IS meaningful is kept: a full
     content panel moves further than a piece of chrome. */
  --lift-card:-6px;   /* a whole content panel: .card .stat .fcard .casecard
                         .capcard .captile .notecard                        */
  --lift-chip:-2px;   /* chrome: .btn, the theme toggle, a logo cell, an
                         icon badge — small things nudge, they don't leap.
                         Mirrored in JS by magnetize()'s `lift` argument.   */
  --zoom-media:1.05;  /* the one ken-burns scale, paired with --dur-zoom    */

  /* ── REVEAL CHOREOGRAPHY ──────────────────────────────────────────────
     One entrance, used everywhere, so a section always arrives the same
     way: fade a little faster than it travels (the copy is legible while
     it is still settling, which reads as arriving rather than popping),
     on the site's one easing curve, on a fixed beat between members.
     --i is written per element by site.js's sequencer, which is what turns
     "four elements crossing the fold" into "one section entering". */
  --rv-fade:.5s;    /* opacity   */
  --rv-rise:.8s;    /* transform */
  --rv-y:18px;      /* the one reveal travel distance                      */
  --rv-step:80ms;   /* the beat between two members of one sequence        */

  /* ── SPACING SCALE ────────────────────────────────────────────────────
     One geometric scale (ratio 1.5 on the fluid vh term) replacing ~60
     hand-rolled clamps. Anchored so that --sp-7 resolves to exactly the
     .zone padding that was already shipping (117px @1440x900), i.e. the
     value the page's rhythm was already built around doesn't move — every
     other section is what gets pulled onto it.

     Vertical space is vh-anchored throughout, deliberately: rhythm should
     follow the viewport's short axis. Three places previously used vw for
     vertical padding (.case-stat-hero, .case-empty, and every "space under
     a heading" margin), which inverted the section rhythm between a short
     laptop and a tall tablet. Those are converted below.

     @1440x900 the vh term governs: 15.3 / 22.95 / 34.2 / 51.75 / 77.4 / 117. */
  --sp-1:8px;                      /* hairline offsets, chip padding       */
  --sp-2:clamp(11px,1.7vh,18px);   /* label → value, dense data-grid gap   */
  --sp-3:clamp(17px,2.55vh,27px);  /* eyebrow → heading, heading → lead    */
  --sp-4:clamp(26px,3.8vh,40px);   /* inside a card, card-grid gap         */
  --sp-5:clamp(38px,5.75vh,60px);  /* heading block → its content block    */
  --sp-6:clamp(58px,8.6vh,104px);  /* minor section padding, block → block */
  --sp-7:clamp(88px,13vh,168px);   /* MAJOR section padding                */

  /* ── BAND LEAD ────────────────────────────────────────────────────────
     How far a full-bleed band takes to arrive out of, or resolve back
     into, whatever it sits on. Declared HERE rather than only in the two
     light-theme blocks, where it used to live alone.
     That was not a tidy-up. A custom property that does not exist makes
     every var() referencing it invalid AT COMPUTED-VALUE TIME, which does
     not fall back to the previous declaration — it throws the whole
     property away. So the moment a dark-theme rule used --band-lead, its
     entire `background` shorthand evaporated and the section rendered with
     no scrim at all: measured on .manifesto-bg::after, the seam below the
     hero went from 0.469 to 0.593 mean row luminance, i.e. brighter than
     before the ramp was added, because the ramp had deleted the wash it
     was supposed to sit on. Band edges are geometry, not palette, and this
     value was always identical in both themes. */
  --band-lead:clamp(56px,8vh,120px);

  /* ── MEASURE ──────────────────────────────────────────────────────────
     Was 20 different `ch` values site-wide. Because `ch` is font-size
     relative, five nominally-constrained lead paragraphs on the home page
     rendered at 437 / 898 / 939 / 1061 / 1061px — five different design
     systems on one screen. A px ceiling inside min() is what actually
     unifies them: the four ~28px leads all land on 720px (≈51 characters,
     inside the 45-75 comfortable range), while a 13px mono lead is still
     held by its own ch term rather than being stretched to 92 characters. */
  --measure-lead:min(100%,58ch,720px); /* leads, deks, subs                */
  --measure-body:min(100%,68ch,640px); /* long-form running copy           */
  --measure-head:20ch;                 /* headlines only                   */
  /* the one documented exception. CENTRED display copy floating over
     photography (.hero .sub, .bolt-play .sub) is a different reading task
     from a left-aligned column: it is scanned, not read line-by-line, and
     the cost of an extra wrap is not fatigue but composition — a third line
     drops the copy onto the bright chrome of the sculpture in both cases,
     and in the hero it pushes the headline up under the fixed nav. Two
     call sites, both stated; not a licence to reintroduce per-element ch. */
  --measure-lead-wide:min(100%,72ch,980px);

  --fs-eyebrow:clamp(.75rem,.72rem + .15vw,.8125rem);
  --fs-body:clamp(1.0625rem,1rem + .3vw,1.125rem);
  --fs-lead:clamp(1.25rem,1rem + 1.1vw,1.75rem);
  --fs-card:clamp(1.125rem,.9rem + .8vw,1.375rem);
  --fs-h3:clamp(1.375rem,1.05rem + 1.3vw,2rem);
  --fs-h2:clamp(2.375rem,6.6vw,6rem); /* = the clamp(38px,6.6vw,96px) already hand-set on
     3 of 6 marketing .phero h1 rules -- redefined to match real, already-shipped usage
     instead of an untested value, so wiring it in below is a zero-visual-diff change. */
  --fs-display:clamp(2.875rem,1.5rem + 6.5vw,9.375rem);
}

/* ══════════════════════════════════════════════════════════════════════
   LIGHT THEME — real light neutrals, not an inverted dark theme. --void
   and the NOIR tokens above are deliberately absent here (see the comment
   on --void above); everything else that governs page chrome repaints.

   Applied two ways, the standard modern pattern:
   1. `[data-theme="light"]` — explicit choice, written by the toggle in
      the nav (assets it drives: localStorage + the tiny inline script at
      the top of every page's <head>, which sets this attribute before
      first paint so there's no flash of the wrong theme).
   2. `prefers-color-scheme:light`, scoped to "no explicit choice yet" via
      :not([data-theme]) — a no-JS/first-paint fallback that mirrors the OS.

   Contrast was computed (WCAG relative-luminance formula), not eyeballed —
   see the feature's risk notes for the actual ratios. The accent in
   particular is NOT the dark theme's #eefb87 darkened by inversion; that
   lime sits well under AA against white already before you even get to a
   body-text pairing, so it's re-derived as a deeper warm gold (#7d5c12)
   that clears AA both as text on the new ground and as a chip background
   under light text. ── */
@media (prefers-color-scheme:light){
  :root:not([data-theme]){
    --bg:#f3efe3;--bg-rgb:243,239,227;
    --white:#211e17;--white-rgb:33,30,23;             /* primary ink — dim/dim-2/glass below derive from this */
    --green:#77560f;--green-rgb:119,86,15;
    --accent:#77560f;--accent-rgb:119,86,15;
    --paper:#faf6ea;--paper-rgb:250,246,234;--ink:#fbf6ea;
    --dim:rgba(var(--white-rgb),.64);                 /* ≈4.75:1 on --bg */
    --dim-2:rgba(var(--white-rgb),.70);                /* ≈5.74:1 on --bg */
    --error:#a63b2e;
    --surface:rgba(var(--paper-rgb),.93);
    --surface-line:rgba(var(--white-rgb),.16);
    --surface-hover:rgba(var(--paper-rgb),.99);
  }
}
:root[data-theme="light"]{
  --bg:#f3efe3;--bg-rgb:243,239,227;
  --white:#211e17;--white-rgb:33,30,23;
  /* deepened from #7d5c12 (2026-08). That value cleared AA on the flat
     grounds (5.35 on --bg) but not on the surfaces the accent actually
     lands on once something is layered over them — 4.42 on the shader-mist
     hero, 3.62 on a glass tile. #77560f holds the same warm gold to the
     eye while clearing 4.83 and 3.95 respectively, and it raises the
     chip case (light --ink on the accent) from 5.71 to 6.24. */
  --green:#77560f;--green-rgb:119,86,15;
  --accent:#77560f;--accent-rgb:119,86,15;
  --paper:#faf6ea;--paper-rgb:250,246,234;--ink:#fbf6ea;
  /* the editorial plate — see the --surface note in :root. .93 keeps a
     trace of the photograph alive under the card (the backdrop-filter
     still reads) while putting card copy on real stock: --dim on the
     resulting surface measures 4.87:1 over the manifesto field photo,
     against 1.62:1 for the ink-tint version it replaces. */
  --surface:rgba(var(--paper-rgb),.93);
  --surface-line:rgba(var(--white-rgb),.16);
  --surface-hover:rgba(var(--paper-rgb),.99);
  /* how far the paper ground ramps into a dark cinematic band before the
     band takes over. Light-only: in dark there is nothing to ramp from,
     the page is already the same value as the band. */
  --dim:rgba(var(--white-rgb),.64);
  --dim-2:rgba(var(--white-rgb),.70);
  --error:#a63b2e;
}
*{margin:0;padding:0;box-sizing:border-box;}
/* NOTE: no `scroll-snap-type:y` here any more. Lenis (site-motion.js) writes
   scrollTop programmatically every frame, and the UA's proximity snapper then
   re-adjusted at scroll-end — two owners of one value, which is exactly the
   "grabs and re-settles" feel at section boundaries. Lenis wins; the five
   `scroll-snap-align:start` declarations that fed it are gone with it. The
   HORIZONTAL snap on .film/.filmtrack is untouched and still works. */
html{-webkit-font-smoothing:antialiased;}
body{background:var(--bg);color:var(--white);font-family:var(--sans);font-weight:400;font-size:var(--fs-body);line-height:1.5;overflow-x:hidden;}
::selection{background:var(--green);color:var(--ink);}
a{color:inherit;text-decoration:none;}
img,video{display:block;max-width:100%;}
.wrap{max-width:1600px;margin:0 auto;padding:0 var(--gut);}
.mono{font-family:var(--mono);text-transform:uppercase;letter-spacing:.06em;}
.up{text-transform:uppercase;}
.serif-i{font-family:var(--sans);font-weight:600;color:var(--accent);}

/* film grain — sits above every page, mix-blend-mode:overlay so it reads
   on both the dark ground and the accent chip alike. */
.grain{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:.16;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.06'/%3E%3C/svg%3E");}

/* generic media-grade utility — apply to any full-bleed photo/video that
   isn't already covered by a page's own selector list. */
.grade{filter:contrast(1.04) saturate(1.08) brightness(.99);}

.skip{position:absolute;left:-999px;background:var(--noir-accent);color:var(--noir-ink);padding:12px 18px;font-family:var(--mono);font-size:11px;text-transform:uppercase;z-index:1000;}
.skip:focus{left:12px;top:12px;}

/* visually-hidden but still announced by screen readers -- for headings/
   labels that exist to keep the document outline correct without adding
   visible copy the design doesn't call for. */
.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;}

/* keyboard focus — every interactive surface gets a real visible ring,
   not just the browser default (which several of these override via
   outline:none-adjacent styling like border-radius/overflow:hidden). Scoped
   to :focus-visible so mouse/touch clicks stay clean and only keyboard
   navigation shows the ring. */
a:focus-visible,button:focus-visible,.btn:focus-visible,.casecard:focus-visible,
.case-player:focus-visible,.nav .r a.lnk:focus-visible,.case-nav a:focus-visible,
.theme-toggle:focus-visible{
  outline:2px solid var(--green);outline-offset:3px;border-radius:6px;}

/* ── button: glass shell + accent fill ── */
/* text is the static --noir-fg, not the theme-flipping --white: .btn shows
   up floating over hero video, void sections (.contact) and the light-mode
   page alike, so it keeps one fixed dark-glass-and-cream treatment rather
   than trying to repaint per theme (see the NOIR comment in :root above). */
.btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:10px;
  font-family:var(--sans);font-weight:500;text-transform:uppercase;letter-spacing:.08em;font-size:14px;color:var(--noir-fg);
  padding:17px 30px;border-radius:100px;background:var(--glass);border:1px solid var(--glass-line);
  backdrop-filter:blur(14px) saturate(1.15);-webkit-backdrop-filter:blur(14px) saturate(1.15);overflow:hidden;
  box-shadow:0 0 26px rgba(var(--accent-rgb),.22);transition:box-shadow var(--dur-2) var(--ease);cursor:pointer;}
.btn::before{content:"";position:absolute;inset:5px;border-radius:100px;background:rgba(238,251,135,.55);
  z-index:-1;transition:inset var(--dur-2) var(--ease),background var(--dur-2) var(--ease);}
.btn:hover{box-shadow:0 0 40px rgba(var(--accent-rgb),.4);}
/* ONE OWNER PER TRANSFORM. site-motion.js magnetize() drives .btn's x/y with
   a .4s quickTo pair, which writes a full inline `transform` — so the CSS
   `:hover{transform:translateY(-2px)}` below could never take effect once the
   button had been hovered even once, and `transition:transform .4s` was
   double-tweening GSAP's per-frame writes on top of that (a .4s CSS ease
   chasing a .4s GSAP ease = the mushy double-lag). magnetize() now adds .mag
   to every element it takes over and folds the -2px lift into its own tween;
   this rule is the no-JS / CDN-failed fallback for everything it didn't. */
.btn:not(.mag){transition:transform var(--dur-3) var(--ease),box-shadow var(--dur-2) var(--ease);}
.btn:not(.mag):hover{transform:translateY(var(--lift-chip));}
.btn:hover::before{inset:2px;background:rgba(238,251,135,.75);}
.btn.sm{padding:12px 22px;font-size:12px;}
.btn svg{width:16px;height:16px;flex-shrink:0;}

/* Houdini animated gradient ring on .card hover/focus — @property lets the
   browser tween --border-angle smoothly across the keyframe below (a plain
   unregistered custom property can't be interpolated, it just snaps).
   Skipped on .btn: its ::before slot already holds the real glass-fill
   hover effect, so this only applies where the pseudo-element is free.
   Browsers without @property support silently ignore the at-rule and fall
   through to the plain border-color transition below untouched. */
@property --border-angle{syntax:'<angle>';inherits:false;initial-value:0deg;}
@keyframes border-angle-spin{to{--border-angle:360deg;}}
/* every component of a transition gets the SAME easing token. 31% of the
   site's transitioned properties previously specified a duration only and
   silently inherited the browser's symmetric `ease` — so a card hover moved
   its geometry on an expo curve and its colour on a symmetric one, over the
   same 500ms. That mismatch is most of the "almost right" feel on hover. */
.card{transition:transform var(--dur-3) var(--ease),border-color var(--dur-3) var(--ease),background var(--dur-3) var(--ease);}
.card::before{content:"";position:absolute;inset:0;border-radius:inherit;padding:1px;pointer-events:none;
  background:conic-gradient(from var(--border-angle),var(--accent) 0deg,var(--sage) 90deg,var(--accent) 180deg,var(--taupe) 270deg,var(--accent) 360deg);
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;
  opacity:0;animation:border-angle-spin 2.6s linear infinite;animation-play-state:paused;transition:opacity .35s var(--ease);}
.card:hover::before,.card:focus-visible::before{opacity:1;animation-play-state:running;}
@media(prefers-reduced-motion:reduce){.card::before{animation:none;}}

/* ── NAV ── */
/* Deliberately NOT theme-flipping (uses the --noir-* statics throughout,
   not --white/--dim/--green): the bar sits transparent over the hero photo
   at the top of every page and only gains its own backing once scrolled,
   so it has to stay legible over arbitrary media regardless of reader
   theme — same reasoning as .btn above. A fixed dark-glass toolbar is also
   just a common, deliberate choice for editorial/cinematic brand chrome. */
.nav{position:fixed;top:0;left:0;right:0;z-index:100;display:flex;align-items:center;justify-content:space-between;
  padding:22px var(--gut);transition:background var(--dur-2) var(--ease),border-color var(--dur-2) var(--ease),padding var(--dur-2) var(--ease);border-bottom:1px solid transparent;}
.nav.scrolled{background:rgba(61,60,56,.82);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom-color:rgba(242,237,224,.1);padding-top:15px;padding-bottom:15px;}
/* pages whose hero is text + mist only (no photo/video) have nothing dark
   behind the nav until you scroll -- the noir-* nav text is invisible
   against the page's own background there in light theme specifically,
   since --bg (unlike --noir-fg) DOES theme-flip to a near-white cream.
   Give those pages the same dark-glass toolbar permanently instead of
   only on .scrolled, so the always-noir nav always has a backdrop to sit
   on regardless of theme or scroll position. */
body.text-hero .nav{background:rgba(61,60,56,.82);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom-color:rgba(242,237,224,.1);}
/* ── shared text-hero shell (.phero) ──
   Was independently re-declared inline on all 8 text-hero pages with
   values that had quietly drifted (min-height 40svh-80svh, h1 caps split
   across five numbers with no content reason). Centralized here with the
   most common values as the default; each page keeps only its genuine
   intentional override (min-height/padding for hero weight, h1 max-size,
   .sub max-width) in its own <style>, same pattern .case-hero already
   uses for case pages below. */
/* SIX hero paddings existed for what is one hero: base 18vh/6vh, about+
   process+future-of-ai 20vh/9vh, careers+privacy+404 17vh/5vh, capabilities
   18vh/7vh, onboard 18vh/6vh-but-max-200. Ten-pixel max-rail differences
   with no content reason behind any of them — the comment above says this
   drift was already fixed once, and it drifted again because the fix left
   each page free to redeclare `padding`. Now expressed in the shared scale
   and the eight page-level padding overrides are deleted, so it can't drift
   a third time. Each page keeps its own min-height, which IS a real
   editorial decision (a 40svh utility hero vs an 80svh statement hero). */
.phero{position:relative;display:flex;flex-direction:column;justify-content:center;overflow:hidden;
  min-height:56svh;padding:calc(var(--sp-7) * 1.4) 0 var(--sp-6);}
.phero h1{font-family:var(--sans);font-weight:700;text-transform:uppercase;line-height:.96;letter-spacing:-.02em;
  font-size:var(--fs-h2);max-width:17ch;}
.phero .sub{margin-top:var(--sp-3);max-width:var(--measure-lead);color:var(--dim);font-size:var(--fs-lead);line-height:1.5;}
/* shader-mist canvas (site.js) sits where the CSS mist blobs used to be
   full-bleed, drawn under the hero content -- only added to the DOM once
   the shader actually compiled, so .has-shader-mist is the signal to
   hide the now-redundant blobs rather than something set unconditionally. */
.phero canvas.shader-mist{position:absolute;inset:0;z-index:0;pointer-events:none;}
.phero.has-shader-mist .mist{display:none;}
/* The shader-mist canvas paints a permanently-dark, animated backdrop
   regardless of theme (same category as the frozen NOIR surfaces), so
   text sitting on it needs the frozen noir tokens + a hard shadow, not
   the theme-flipping --dim/--green -- those go near-invisible in light
   theme, and --green is a deliberate exact match to the shader's own
   cLime highlight (see cLime in site.js), so it visually merges with
   the mist's bright passes without a shadow holding its edge. */
.phero.has-shader-mist h1{text-shadow:0 4px 28px rgba(15,14,12,.55),0 1px 4px rgba(15,14,12,.5);}
.phero.has-shader-mist .sub{color:var(--noir-dim);text-shadow:0 2px 14px rgba(15,14,12,.6);}
.phero.has-shader-mist .tag{text-shadow:0 2px 10px rgba(15,14,12,.75);}

/* ══ THE PHOTOGRAPHIC PAGE HEADER (.phero.has-photo) ══════════════════
   The home hero's treatment, generalised. Seven interior pages carried
   the SAME procedural shader-mist and no photograph at all; this is the
   shared shell that replaces it, and it is deliberately the same four
   parts index.html's hero is made of, in the same order:

     1  a photograph, framed by --frame-top / --frame-h (never a
        transform — see THE OPENING FRAME in index.html; the reason
        there is that GSAP owns transform on that element, and keeping
        the same expression here is what makes the two one system
        rather than two that happen to look alike),
     2  one filmic grade shared with every other full-bleed photo,
     3  a wash shaped to the page's MEASURED type band map, opening
        where no type lands,
     4  a lead-out that resolves to the ground the next section starts
        from, so the hero hands over without a seam.

   WHAT FLIPS. index's photograph is the wildflower field, mean 123.6/255,
   and it takes INK type. These seven are dark — measured mean over the
   whole file, 25 / 38 / 46 / 51 / 66 / 81 / 81 / 102 out of 255 — so they
   take CREAM, and the wash DARKENS instead of lifting. That is the whole
   polarity change: one photograph, one wash, one cream/ink decision per
   page, and both themes get the same one.

   WHY THE WASH IS FLATTER THAN INDEX'S. index is top-anchored with a
   477px gap between the CTA and the scroll cue, so its wash can drop to
   .04 through 54-82% and let the sculpture come through at nearly full
   strength. These heroes are vertically CENTRED and their stacks fill the
   box: measured across all seven pages at 1440x900 / 1366x768 / 1280x720 /
   390x844, the eyebrow's top never starts later than 40.1% and never
   earlier than 16.5%, and the lead's last row never ends before 81.0% and
   never after 92.7%. There is no interior aperture to open, so the
   aperture that exists is (a) the top band above the eyebrow, which only
   the nav occupies and the nav carries its own plate, and (b) the
   horizontal one: the copy column ends at 53-59% of the viewport for the
   lead and 58.9-87.0% for the headline at desktop widths, so the RIGHT of
   the frame is free. --wx is that second aperture, a left-weighted layer
   that supplies extra alpha only under the copy and fades out by 86%,
   which is why the subject in each frame is placed right of centre where
   the picture allows it. At <=820px the headline runs to 74.9-94.8% of the
   viewport and there is no horizontal aperture left, so --wx goes to 0 and
   the vertical stops take over — the same trade index makes at the same
   breakpoint. */
.phero.has-photo{
  /* frozen, not themed, and for exactly index's reason: the photograph is
     the same photograph in both themes and the type on it is cream in
     both, so the ground they need is the same in both. */
  --hero-shade:16,15,13;
  --frame-top:0%;--frame-h:100%;--frame-x:50%;
  /* the vertical band map. Defaults are the worst case across the seven;
     every page overrides the ones its own measured map needs. */
  --p1:.48;   /* 0%   nav band — nav has its own .82 plate over this  */
  --p2:.58;   /* 10%  shut by the time the earliest eyebrow starts    */
  --p3:.64;   /* 22%                                                  */
  --p4:.64;   /* 55%                                                  */
  --p5:.62;   /* 80%                                                  */
  --p6:.58;   /* 93%  last row of type clears at 92.7%                */
  --wx:.12;   /* horizontal relief, left (copy) side only             */
  --lead:93%; /* where the hand-off to the next section starts        */
  /* WHAT THE HAND-OFF RESOLVES TO. The note below says "the section under
     every one of these heroes starts from the page ground". That was true
     when it was written and is not true any more: the band rhythm puts a
     --void band directly under capabilities' hero. A 66px marquee cannot
     host a --band-lead ramp of its own (the 22% clamp cuts it to ~15px,
     measured 0.098 per row in light), so the hero has to arrive at the
     right colour instead. An rgb triplet, defaulting to the page ground —
     every page whose next band is not the page ground names its own. */
  --hero-out:var(--bg-rgb);
}
.phero.has-photo .bgimg{position:absolute;inset:0;z-index:0;overflow:hidden;}
.phero.has-photo .bgimg img{position:absolute;left:0;top:var(--frame-top);width:100%;height:var(--frame-h);
  object-fit:cover;object-position:var(--frame-x) 50%;
  /* the same one filmic grade index applies to every full-bleed photo and
     video on the site, rather than showing raw source flat/desaturated */
  filter:contrast(1.04) saturate(1.08) brightness(.99);}
/* ATMOSPHERE, not legibility — the same separation index draws. site.js's
   heroAtmosphere() writes --scrim-1/--scrim-2 off the Austin/LA clock and
   now reaches these heroes too; all three moods it can produce are dark
   rgba, so on cream type this layer can only ever help, and it is measured
   with every number in this file swept across all three of them. It sits
   INSIDE .bgimg, i.e. under the wash, and carries no contrast duty. */
.phero.has-photo .bgimg::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  transition:background 3s ease;
  background:linear-gradient(180deg,var(--scrim-1,rgba(12,12,14,.2)) 0%,rgba(12,12,14,0) 20%,
    rgba(12,12,14,0) 84%,var(--scrim-2,rgba(61,60,56,.5)) 100%);}
/* ::after is the LAST child, so at equal z-index it stacks over .bgimg and
   still sits below .wrap (z 1, set per page) — index's placement argument,
   unchanged. Three layers, painted top-down as written:
     1  the hand-off. Opaque --bg at 100%, transparent until --lead. The
        section under every one of these heroes starts from the page
        ground: .zone is transparent over <body>, .band-cut's own lead-in
        ramp starts at rgb(var(--bg-rgb)), and .careers is unpainted. So
        both sides of the seam are literally the same colour and the step
        across it is whatever the ramps make it. It is THEMED, unlike
        index's frozen paper, because unlike index's the next section is:
        in dark this ramp runs near-black to charcoal (L .006 -> .045) and
        is very nearly a no-op, in light it does the real work — exactly
        the trade .band-cut already makes for every dark band on the site.
     2  the horizontal aperture (see --wx above).
     3  the vertical band map. */
.phero.has-photo::after{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    /* EASED OUT, not linear, and that is the seam measurement talking.
       Alpha->luminance is convex (gamma 2.4), so a linear ramp's biggest
       single-row step lands at the OPAQUE end: measured in light theme, a
       straight --lead -> 100% ramp gave 0.033-0.046 per row against
       index's 0.021. Spending most of the ramp's length on the last third
       of the alpha flattens that end out. Dark is unaffected either way —
       the whole ramp only travels L .006 -> .045 there. */
    linear-gradient(180deg,rgba(var(--hero-out),0) var(--lead),
      rgba(var(--hero-out),.40) calc(var(--lead) + (100% - var(--lead)) * .30),
      rgba(var(--hero-out),.78) calc(var(--lead) + (100% - var(--lead)) * .64),
      rgba(var(--hero-out),.93) calc(var(--lead) + (100% - var(--lead)) * .85),
      rgb(var(--hero-out)) 100%),
    linear-gradient(90deg,rgba(var(--hero-shade),var(--wx)) 0%,rgba(var(--hero-shade),var(--wx)) 48%,
      rgba(var(--hero-shade),0) 86%),
    linear-gradient(180deg,
      rgba(var(--hero-shade),var(--p1)) 0%,
      rgba(var(--hero-shade),var(--p2)) 10%,
      rgba(var(--hero-shade),var(--p3)) 22%,
      rgba(var(--hero-shade),var(--p4)) 55%,
      rgba(var(--hero-shade),var(--p5)) 80%,
      rgba(var(--hero-shade),var(--p6)) 93%,
      rgba(var(--hero-shade),var(--p6)) 100%);}
@media(max-width:820px){.phero.has-photo{--wx:0;--p1:.40;--p2:.64;--p3:.70;--p4:.70;--p5:.68;--p6:.62;}}
/* THE TYPE. index's headline is weight 500 at line-height .94; .phero's was
   700/.96, which on a photograph reads as a different voice rather than the
   same one a size down. 500 is also the narrower cut, so it costs nothing
   in fit — measured, no page gains a line at any of the four widths.
   NO TEXT-SHADOW, unlike the shader-mist heroes above: WCAG gives a shadow
   no credit and every number quoted for these heroes is measured with the
   glyph shadow removed, so a shadow here would be decoration standing in
   for a wash that has to work anyway. The wash works. */
.phero.has-photo h1{font-weight:500;line-height:.94;text-shadow:none;}
/* full NOIR foreground, both themes. Same argument index makes for its own
   .sub and base.css already makes for the light-theme shader heroes: a lead
   paragraph is not tertiary text, so it takes the primary foreground and
   lets SIZE carry the hierarchy rather than a .62 alpha on a photograph.
   Measured, .62 cream over these frames lands 3.4-4.1 — under the 4.5 bar
   the owner set for this element — and full cream lands 7.7-11.0. */
.phero.has-photo .sub{color:var(--noir-fg);text-shadow:none;}
.phero.has-photo .tag{text-shadow:none;}

/* the wordmark had no colour declaration at all, so it inherited --white
   from <body> — which is cream in dark (identical to --noir-fg, hence no
   change here) but flips to near-black ink in light, where it then sat on
   the nav's own dark glass plate at 2.41:1. Stated explicitly as NOIR,
   like every other element of this deliberately frozen toolbar. */
.nav .brand{position:relative;font-family:var(--sans);font-weight:600;font-size:22px;letter-spacing:.14em;display:flex;align-items:center;gap:9px;color:var(--noir-fg);}
/* brand mark refresh 2026-08: .dot used to be an empty span painted as a
   filled circle (background-color + box-shadow glow). It's now the real
   muus-mark.svg (two lightning strokes forming an "M"), so the circle
   styling is gone and the charge-glow moves from box-shadow (which only
   hugs the element's box) to drop-shadow (which hugs the mark's actual
   jagged silhouette) -- same --charge custom property, same trigger from
   site-motion.js, just a glow that fits the new shape. --noir-accent was
   unified to the mark's literal #eefb87 (color-token sweep, 2026-08), so
   this could read var(--noir-accent) -- left as a literal since the glow
   is tied to this exact mark asset's fill, not the general accent role. */
.nav .brand .dot{width:18px;height:18px;display:block;object-fit:contain;
  filter:drop-shadow(0 0 calc(8px + var(--charge,0) * 16px) #eefb87);transition:filter .15s ease-out;}
.nav .r{position:relative;display:flex;gap:24px;align-items:center;}
.nav .r a.lnk{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--noir-dim);transition:color var(--dur-2) var(--ease);white-space:nowrap;}
.nav .r a.lnk:hover{color:var(--noir-fg);}
.nav .r a.lnk.on{color:var(--noir-accent);}
/* 8 nav links (Field Notes added to the original 7) no longer fit at the
   default gap/padding/type-size all the way down to the 900px mobile-nav
   cutoff -- measured the real numbers (not guessed) and tightened gap,
   nav padding, and link size together in this band so a genuine laptop
   window still gets the full desktop nav instead of being pushed into
   the hamburger. Placed after the base a.lnk rule above (not before) so
   it actually wins the cascade at equal specificity -- an earlier version
   of this rule silently lost to the unconditional rule below it. */
@media(max-width:1180px){
  .nav{padding-left:20px;padding-right:20px;}
  .nav .r{gap:8px;}
  .nav .r a.lnk{font-size:10.5px;letter-spacing:.05em;}
}
@media(max-width:900px){.nav .r a.lnk{display:none;}}
/* the CTA button is wide (fits comfortably alongside 7 links on desktop)
   and the mobile top bar only has room for brand + theme-toggle + the
   hamburger itself -- three-way space-between with a wide 4th element
   would overflow a real phone width. Moved into the mobile panel below
   instead of just hidden outright, so the action isn't lost. */
@media(max-width:900px){.nav .r > a.btn.sm{display:none;}}

/* ── mobile nav toggle + slide-down panel — injected by site.js, not
   markup, so it's one implementation shared by every page instead of 24
   copies. Panel content is built at runtime from the real .nav .r a.lnk
   anchors above, so it can never list a link the desktop nav doesn't
   actually have. */
.navToggle{display:none;flex:0 0 auto;width:44px;height:44px;border-radius:50%;
  align-items:center;justify-content:center;gap:4px;flex-direction:column;
  background:var(--noir-glass);border:1px solid var(--noir-glass-line);cursor:pointer;
  backdrop-filter:blur(14px) saturate(1.15);-webkit-backdrop-filter:blur(14px) saturate(1.15);}
.navToggle span{display:block;width:17px;height:1.5px;background:var(--noir-fg);border-radius:1px;
  transition:transform .3s var(--ease),opacity .2s var(--ease);}
.nav.navOpen .navToggle span:nth-child(1){transform:translateY(5.5px) rotate(45deg);}
.nav.navOpen .navToggle span:nth-child(2){opacity:0;}
.nav.navOpen .navToggle span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg);}
.navMobilePanel{position:fixed;top:0;left:0;right:0;z-index:99;
  padding:88px var(--gut) 28px;background:rgba(28,26,23,.94);
  backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  border-bottom:1px solid var(--noir-glass-line);
  opacity:0;visibility:hidden;transform:translateY(-8px);pointer-events:none;
  transition:opacity .3s var(--ease),transform .3s var(--ease),visibility 0s .3s;}
.nav.navOpen + .navMobilePanel{opacity:1;visibility:visible;transform:translateY(0);pointer-events:auto;
  transition:opacity .3s var(--ease),transform .3s var(--ease),visibility 0s 0s;}
.navMobilePanel-links{display:flex;flex-direction:column;}
.navMobilePanel-links a{display:flex;align-items:center;min-height:52px;padding:6px 0;
  font-family:var(--sans);font-weight:500;font-size:19px;color:var(--noir-fg);
  border-bottom:1px solid var(--noir-glass-line);}
.navMobilePanel-links a.on{color:var(--noir-accent);}
.navMobilePanel-search{display:flex;align-items:center;min-height:52px;margin-top:10px;width:100%;
  font-family:var(--mono);font-size:13px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--noir-dim);background:none;border:none;cursor:pointer;padding:0;}
.navMobilePanel-cta{display:flex;align-items:center;justify-content:center;min-height:52px;margin-top:16px;
  width:100%;border-radius:999px;font-family:var(--sans);font-weight:600;font-size:14px;
  text-transform:uppercase;letter-spacing:.04em;color:var(--noir-ink);background:var(--noir-accent);}
.navMobilePanel-scrim{position:fixed;inset:0;z-index:98;background:rgba(0,0,0,.4);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .25s var(--ease);}
.nav.navOpen ~ .navMobilePanel-scrim{opacity:1;visibility:visible;pointer-events:auto;}
@media(min-width:901px){.navToggle,.navMobilePanel,.navMobilePanel-scrim{display:none;}}
@media(max-width:900px){.navToggle{display:flex;}}

/* nav chase bar — a shared accent underline that slides + stretches between
   whichever link is hovered, injected/driven entirely from JS (site-motion.js)
   so it's absent whenever GSAP/motion is unavailable or reduced-motion is on. */
.nav .r .chase{position:absolute;left:0;bottom:-9px;height:2px;width:0;
  background:var(--noir-accent);box-shadow:0 0 10px rgba(238,251,135,.7);border-radius:2px;opacity:0;pointer-events:none;}

/* ── theme toggle — the one interactive control in the nav that IS aware
   of the theme, since its whole job is switching it. Same glass-icon-button
   language as the rest of the noir nav (dark shell, cream icon) so it reads
   as part of the same toolbar rather than a bolted-on widget; the actual
   read-localStorage / set-attribute logic lives in a tiny inline script at
   the top of every page's <head> (runs before first paint, no flash), not
   here — this file only owns how the control looks and which icon shows. */
.theme-toggle{position:relative;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;border-radius:50%;color:var(--noir-fg);cursor:pointer;
  background:var(--noir-glass);border:1px solid var(--noir-glass-line);
  backdrop-filter:blur(14px) saturate(1.15);-webkit-backdrop-filter:blur(14px) saturate(1.15);
  transition:border-color var(--dur-2) var(--ease),transform var(--dur-2) var(--ease);}
.theme-toggle:hover{border-color:rgba(238,251,135,.5);transform:translateY(var(--lift-chip));}
/* The one control on the site whose entire job is a state change used to do
   it with `display:none/block` — a hard cut, on a page where everything else
   eases. Both icons now stack in the same 17px box and cross-rotate through
   each other. Absolute positioning (not display) is what makes the swap
   animatable at all; the button already had position:relative. */
.theme-toggle svg{width:17px;height:17px;stroke:currentColor;position:absolute;top:50%;left:50%;
  transition:opacity var(--dur-2) var(--ease),transform 420ms var(--ease);}
.theme-toggle .ic-sun{opacity:1;transform:translate(-50%,-50%) rotate(0deg) scale(1);}
.theme-toggle .ic-moon{opacity:0;transform:translate(-50%,-50%) rotate(-70deg) scale(.5);}
:root[data-theme="light"] .theme-toggle .ic-sun{opacity:0;transform:translate(-50%,-50%) rotate(70deg) scale(.5);}
:root[data-theme="light"] .theme-toggle .ic-moon{opacity:1;transform:translate(-50%,-50%) rotate(0deg) scale(1);}

/* nav-brand spark — the wordmark as a living instrument. Scroll velocity,
   read off the exact same Lenis 'scroll' signal that already drives the
   grain effect (site-motion.js), charges a decaying value written to
   --charge on .brand — the same custom-property language THE BOLT drives
   on index.html — which feeds the dot's glow above and a tiny always-on
   canvas that sparks jagged little arcs off the dot, drawn with the shared
   window.MUUSBolt primitives (site.js). Entirely injected/positioned from
   JS, so it's simply absent whenever motion is unavailable or reduced. */
.brand-spark{position:absolute;pointer-events:none;}

/* ── SITE-WIDE VIEW TRANSITIONS ────────────────────────────────────────
   case-studies.html → case/*.html already opts into a cross-document
   View Transition per pair via <meta name="view-transition"
   content="same-origin"> + a matching view-transition-name on the poster
   image / case-player. That meta tag was an early origin-trial mechanism;
   the current shipped API is this @view-transition at-rule instead — one
   declaration here, in the stylesheet every real page already loads,
   turns the SAME pattern on for every top-level hop (index ⇄ process ⇄
   capabilities ⇄ insights ⇄ about, etc.) without touching each page.
   Leftover meta tags on some pages (capabilities.html, the case pages)
   are now inert but harmless — no need to strip them.

   Browser support (checked against MDN + Chrome/WebKit notes, Aug 2026):
   animates today in Chromium 126+ (Chrome/Edge/Opera). Safari and
   Firefox don't yet run the cross-document animation, and any browser
   that doesn't understand the at-rule just ignores it — every case
   falls back to a plain, un-animated navigation, never a broken one.

   Only two things get a stable view-transition-name: the page itself
   (root, unnamed → browser's default whole-page cross-fade, replacing
   the hard reload flash) and the nav wordmark below. Nothing fakes a
   shared hero photo between index.html and process/capabilities/
   insights.html — their .phero hero is copy-only (no img/video), so
   there is no real matching image to morph between. ─────────────────── */
@view-transition{
  navigation:auto;
}

/* `navigation:auto` on its own buys the UA default: a flat, symmetric
   cross-fade with no direction to it, which reads as "the page blinked".
   Naming the root's own old/new animations turns every hop into a short
   directed exit + entrance on the site's own easing token — the departing
   page lifts and clears, the arriving one settles up from below. Both are
   kept well under 500ms: a page transition you have to WAIT for stops
   feeling expensive very fast. Chromium-only today; every other browser
   ignores the pseudo-elements and gets the same plain navigation it gets
   now. The reduced-motion override lives a few rules down (::view-
   transition-* is a UA overlay tree the global `*` reset can't reach). */
@keyframes vt-old{to{opacity:0;transform:translateY(-14px);}}
@keyframes vt-new{from{opacity:0;transform:translateY(18px);}} /* the literal, not var(--rv-y):
   ::view-transition-* is a UA-generated overlay tree, and a custom property
   that failed to inherit into it would invalidate the whole keyframe and
   silently drop the entrance. Same 18px the .rv system travels. */
::view-transition-old(root){animation:vt-old .26s var(--ease) both;}
::view-transition-new(root){animation:vt-new .42s var(--ease) both;}

/* ONE curve for the whole navigation, the shared element included. A named
   group's morph — the case card travelling to become the case film, see the
   CROSS-DOCUMENT SHARED ELEMENT block in site.js — runs on the UA default of
   250ms `ease` unless you say otherwise, which meant the card and the page it
   was sitting on arrived on two different curves at two different speeds.
   `-group` is only the container that does the transform/size morph; the
   root's own old/new snapshots keep the explicit animations above.

   The two snapshots INSIDE a named pair keep the UA's own 250ms cross-fade,
   deliberately: a fade shorter than the .42s travel is what makes the eye
   read one object moving rather than two images dissolving over each other
   for the whole trip. Only the travel is retimed. */
::view-transition-group(*){animation-duration:.42s;animation-timing-function:var(--ease);}

/* nav wordmark persists across the fade instead of cross-fading with the
   rest of the page — the one element every real page shares exactly.
   Scoped to .brand only, not the whole <nav>: .nav swaps in .scrolled
   (blur, tighter padding) once the user has scrolled, so a departure
   page mid-scroll and a fresh top-of-page arrival would otherwise pair
   two visibly different nav snapshots under one name. .brand itself
   doesn't change with .scrolled, so pinning just the wordmark keeps the
   logo steady with, at most, a small settle if padding shifted. Unique
   per document (one .brand per page) so it can't collide with the
   case-* names case-studies.html/case/*.html already use for photos. */
.nav .brand{view-transition-name:site-nav-brand;}

/* view transitions don't auto-respect prefers-reduced-motion — the spec
   leaves that to the page. The site's existing reduced-motion query
   (near the .rv reveal-animation rules, `*{animation:none!important;
   transition:none!important}`) only reaches real DOM nodes;
   ::view-transition-* lives in a separate UA-generated overlay tree
   that needs its own selector. */
@media(prefers-reduced-motion:reduce){
  ::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){
    animation:none!important;
  }
}

/* ── section scaffold ── */
section{position:relative;}
/* the rhythm anchor: --sp-7 was derived FROM this value, so it doesn't move. */
.zone{padding:var(--sp-7) 0;position:relative;}
.zone.atmos::before{content:"";position:absolute;left:50%;top:16%;transform:translateX(-50%);width:min(1500px,150vw);height:900px;z-index:0;pointer-events:none;
  background:radial-gradient(closest-side,rgba(var(--green-rgb),.18),transparent 70%);filter:blur(80px);}
/* The blob above is deliberately wider than its section — 150vw is what gives
   the glow its soft off-screen falloff — but nothing was stopping that width
   from widening the DOCUMENT. On a phone it made every .atmos page scroll
   sideways: at 390 the blob spans -97..488 against a 390 viewport, and
   documentElement.scrollWidth reported 489. body{overflow-x:hidden} did not
   catch it, because a body overflow is propagated to the viewport rather than
   applied to the body box: it hides the scrollbar and clips the paint, while
   the scrollable overflow region — the thing a touch pan actually follows —
   still measures 489.
   Contain it where it escapes instead. `clip` rather than `hidden` on purpose:
   hidden would force the other axis to `auto`, making every atmos section a
   scroll container (which breaks position:sticky inside it and would clip the
   blob's intended vertical bleed into the next band). `clip` clips without
   creating a scroll container, so overflow-y stays genuinely visible.
   Horizontally neutral by construction at every width: .zone is a full-bleed
   block, so its padding box and the viewport share the same left/right edges —
   this clips exactly where the viewport was already clipping. */
.zone.atmos{overflow-x:clip;}
.zone>*{position:relative;z-index:1;}
.tag{font-family:var(--mono);font-size:var(--fs-eyebrow);letter-spacing:.28em;text-transform:uppercase;color:var(--dim-2);}

/* ── glass card grid — reused for manifesto cards on home, and any
   philosophy/pillar/case-summary grid on subpages ── */
/* --sp-5 is "space under a heading block", used identically by all ten of
   the grids/rows that previously spelled that one relationship ten ways
   (60/64/64/70/80/80/84/88/90/110px). --sp-4 is the card-grid gutter, shared
   with .stats below: they are the same component family in the same
   stylesheet and used to differ by 87%. */
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4);margin-top:var(--sp-5);align-items:start;}
.card{position:relative;overflow:hidden;background:var(--surface);border:1px solid var(--surface-line);border-radius:30px;padding:clamp(26px,2.4vw,40px);
  backdrop-filter:blur(20px) saturate(1.15);-webkit-backdrop-filter:blur(20px) saturate(1.15);min-height:clamp(280px,32vw,420px);display:flex;flex-direction:column;
  transition:transform var(--dur-3) var(--ease),border-color var(--dur-2) var(--ease),background var(--dur-2) var(--ease);}
.card:hover{transform:translateY(var(--lift-card));border-color:rgba(var(--green-rgb),.5);background:var(--surface-hover);}
.card .hd{display:flex;justify-content:space-between;align-items:center;font-family:var(--mono);font-size:var(--fs-eyebrow);letter-spacing:.28em;color:var(--dim);text-transform:uppercase;padding-bottom:20px;border-bottom:1px solid rgba(var(--white-rgb),.12);}
.card .big{margin-top:auto;}
.card .big .s{font-family:var(--sans);font-weight:300;color:var(--accent);font-size:clamp(34px,3.6vw,58px);line-height:1;display:block;}
.card .big .t{font-family:var(--sans);font-weight:700;text-transform:uppercase;font-size:clamp(30px,3.2vw,52px);line-height:1;letter-spacing:-.02em;margin-top:6px;display:block;}
.card p{margin-top:var(--sp-3);color:var(--dim);font-size:var(--fs-body);line-height:1.55;max-width:34ch;}
@media(max-width:820px){.cards{grid-template-columns:1fr;}.card{min-height:auto;}}
/* future-of-ai.html's "Where it actually helps" row has one card with a
   noticeably longer body paragraph than its siblings -- the shared .cards
   grid opts out of the default grid stretch (align-items:start) so other
   pages' card rows can size to their own content, but that leaves this
   row visibly uneven. .card is already flex-column with a min-height, so
   stretching just this row is enough to even the three out. */
.helps .cards{align-items:stretch;}

/* ── glass stat grid — reused for the home numbers section and any
   KPI/proof block on subpages ── */
.stats{position:relative;display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-4);margin-top:var(--sp-5);}
/* halftone screen-print texture, sitting just behind the numerals — same
   inline-SVG-as-background-image technique as .grain, a repeating dot
   pattern instead of turbulence noise, at low enough opacity to read as
   texture on poster stock rather than literal dots. */
.stats::before{content:"";position:absolute;inset:-24px -14px;z-index:-1;opacity:.05;pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9'%3E%3Ccircle cx='2' cy='2' r='1' fill='%23f2ede0'/%3E%3C/svg%3E");
  background-size:9px 9px;mix-blend-mode:overlay;}
.stat{position:relative;overflow:hidden;background:var(--surface);border:1px solid var(--surface-line);border-radius:28px;padding:clamp(24px,2.2vw,40px);backdrop-filter:blur(18px) saturate(1.15);-webkit-backdrop-filter:blur(18px) saturate(1.15);transition:border-color var(--dur-3) var(--ease),transform var(--dur-3) var(--ease);}
.stat:hover{border-color:rgba(var(--green-rgb),.4);transform:translateY(var(--lift-card));}
.stat .n{font-family:var(--sans);font-weight:500;font-size:clamp(48px,6vw,100px);line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums;}
.stat .lab{font-family:var(--mono);font-size:clamp(12px,1vw,16px);letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin-top:var(--sp-2);}
@media(max-width:820px){.stats{grid-template-columns:1fr;}}

/* #numbers sits on a backdrop that's frozen dark in every theme (see the
   section's own .numbers-bg::after scrim) -- .stat/.stat .n/.stat .lab
   above use the theme-flipping --glass/--white/--dim tokens, which invert
   to dark ink in light theme and go near-invisible on that same frozen
   backdrop. Freeze this instance to the NOIR set already used one
   element up in this same section for exactly this reason. */
#numbers .stat{background:var(--noir-glass);border-color:var(--noir-glass-line);}
#numbers .stat .n{color:var(--noir-fg);}
#numbers .stat .lab{color:var(--noir-dim);}

/* ── filmstrip sprocket-hole divider — a quiet, literal "we shoot film"
   detail for section seams that want more than a plain 1px border.
   Real cut-throughs via mask (not a flat dot color), so it reads correctly
   against whatever sits behind it. ── */
.sprocket-divider{display:block;width:100%;height:16px;background-color:var(--void);
  border-top:1px solid var(--glass-line);border-bottom:1px solid var(--glass-line);
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='16'%3E%3Ccircle cx='10' cy='8' r='2.2' fill='%23fff'/%3E%3C/svg%3E");
  mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='16'%3E%3Ccircle cx='10' cy='8' r='2.2' fill='%23fff'/%3E%3C/svg%3E");
  -webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;-webkit-mask-size:20px 16px;mask-size:20px 16px;-webkit-mask-position:center;mask-position:center;}

/* one-shot specular light-sweep — plays once when .rv.in is first added
   (a CSS animation restarts whenever an element newly matches a rule that
   names it, so gating this on .in instead of a bare :hover is what makes
   it a single "the glass just caught light" moment on scroll-reveal
   rather than a repeating hover gimmick). */
.card::after,.stat::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(75deg,transparent 42%,rgba(255,255,255,.16) 50%,transparent 58%);
  transform:translateX(-150%);}
.card.rv.in::after,.stat.rv.in::after{animation:sweep var(--dur-4) var(--ease) .2s 1 both;}
@keyframes sweep{0%{transform:translateX(-150%);opacity:0;}12%{opacity:1;}100%{transform:translateX(150%);opacity:0;}}
@media(prefers-reduced-motion:reduce){.card::after,.stat::after{content:none;}}

/* ── FOOTER ── */
/* was the only non-fluid section padding on the site — a flat 36px that
   didn't scale with anything else on the page. */
footer{border-top:1px solid rgba(var(--white-rgb),.12);padding:var(--sp-5) 0;}
footer .in{display:flex;justify-content:space-between;align-items:center;gap:18px;flex-wrap:wrap;}
footer .brand{font-family:var(--sans);font-weight:600;letter-spacing:.14em;font-size:19px;display:flex;gap:9px;align-items:center;}
footer .brand .dot{width:16px;height:16px;display:block;object-fit:contain;}
footer .m{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim-2);display:flex;gap:22px;flex-wrap:wrap;}
footer .m a{color:inherit;transition:color var(--dur-2) var(--ease);}
footer .m a:hover{color:var(--green);}
/* newsletter capture — a second, lower-friction row than the full
   onboard.html intake form. Its own .wrap sibling to footer .in, not a
   3rd child inside it, so the brand/.m flex(justify-content:space-between)
   pair is never disturbed by a 3rd item. */
footer .news{margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:1px solid rgba(var(--white-rgb),.08);}
.nl-form{display:flex;flex-wrap:wrap;align-items:center;gap:14px 20px;}
.nl-form label{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim-2);white-space:nowrap;}
/* min-width:0 is load-bearing, not decoration. As a flex item .nl-row took the
   default min-width:auto, so its floor was its own min-content width — and that
   min-content is the email field's intrinsic size plus the gap plus the button,
   measured at 329.9px. Inside a 280px form (320px viewport minus two 20px
   gutters) it therefore could not shrink and overflowed by 50px, which is the
   second, independent reason the document scrolled sideways at 320 — the one
   that also hit careers.html, the single page with no .atmos section.
   min-width:0 lets the row shrink to the form; the field inside already carries
   its own min-width:0 and flexes down with it. No effect above ~700px, where
   the row is capped by max-width:380px and never compressed at all. */
.nl-row{display:flex;gap:8px;flex:1 1 280px;max-width:380px;min-width:0;}
.nl-row input{flex:1;min-width:0;background:var(--paper);border:1px solid rgba(var(--white-rgb),.18);border-radius:8px;
  padding:12px 14px;min-height:44px;font-family:var(--mono);font-size:16px;color:var(--white);}
.nl-row input::placeholder{color:var(--dim-2);}
.nl-row input:focus-visible{outline:2px solid var(--green);outline-offset:1px;}
.nl-row button{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink);
  background:var(--green);border:none;border-radius:8px;padding:0 16px;cursor:pointer;transition:opacity var(--dur-1) var(--ease);white-space:nowrap;}
.nl-row button:hover{opacity:.85;}
.nl-row button:disabled{opacity:.5;cursor:default;}
.nl-status{font-family:var(--mono);font-size:11px;color:var(--dim-2);margin:8px 0 0;min-height:1em;}
.nl-status[data-state="ok"]{color:var(--green);}
.nl-status[data-state="err"]{color:var(--error);}

/* ── custom cursor v2 — dot + breathing ring, now contextual: the ring
   swaps into a filled mono-label pill ("Play" over .case-player, "Drag"
   over the home filmstrip) instead of only growing for the generic .hot
   state. html.has-cursor is added by JS (site-motion.js) strictly inside a
   matchMedia(hover:hover)+(pointer:fine) gate, so the cursor:none rule
   below only ever fires on real fine-pointer/hover devices — and never at
   all if that script fails to run, leaving the native cursor untouched. ── */
.cur,.curR{position:fixed;top:0;left:0;z-index:10000;pointer-events:none;border-radius:50%;transform:translate(-50%,-50%);}
.cur{width:8px;height:8px;background:var(--noir-accent);}
.curR{width:34px;height:34px;border:1.5px solid rgba(238,251,135,.5);display:flex;align-items:center;justify-content:center;
  transition:width var(--dur-2) var(--ease),height var(--dur-2) var(--ease),padding var(--dur-2) var(--ease),border-radius var(--dur-2) var(--ease),border-color var(--dur-2) var(--ease),background-color var(--dur-2) var(--ease);}
.curR.hot{width:58px;height:58px;border-color:rgba(238,251,135,.9);}
.curR .cl{opacity:0;font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--noir-ink);white-space:nowrap;transition:opacity .2s var(--ease);}
.curR.ctx{width:auto;height:36px;padding:0 16px;border-radius:999px;background:var(--noir-accent);border-color:var(--noir-accent);}
.curR.ctx .cl{opacity:1;}
@media(hover:none),(pointer:coarse){.cur,.curR{display:none;}}
.cur-trail{position:fixed;inset:0;z-index:9999;pointer-events:none;}
@media(hover:none),(pointer:coarse){.cur-trail{display:none;}}
@media(hover:hover) and (pointer:fine){
  html.has-cursor,html.has-cursor *{cursor:none;}
  /* real form fields (the case-search box, the "propose a time" panel) keep
     their native text-caret/affordance — the dot+ring has no text-entry
     state of its own, so suppressing the native cursor there would leave
     no cursor at all over a field a visitor is about to type into. */
  html.has-cursor input,html.has-cursor textarea,html.has-cursor select,html.has-cursor [contenteditable]{cursor:auto;}
}

/* word-mask reveal — no CSS transform here on purpose, see site.js
   maskWords(): every call site follows with gsap.set(ws,{yPercent:112}),
   which would otherwise fight a static transform rule for control of the
   same property on any reveal gated behind a scroll delay. */
.wm{display:inline-block;overflow:hidden;vertical-align:bottom;}
.wm>span{display:inline-block;will-change:transform;}
.lm{display:block;overflow:hidden;}
.lm>span{display:block;will-change:transform;}

/* reveal-on-scroll */
/* --i is an index-driven stagger, set per child by site.js on the dense
   grids (.lgrid, .casegrid, .capgrid, .cards, .stats, …). The fixed
   .d1/.d2/.d3 classes below only ever gave 4 distinct delays, so a 23-cell
   logo grid revealed as four simultaneous blocks of six — a flash, not a
   cascade. They still win where a page hand-authors them (higher
   specificity), and site.js strips them from the grids it takes over. */
/* The transform runs LONGER than the fade (--rv-rise .8s vs --rv-fade .5s),
   deliberately and everywhere. Matched durations make an element pop into
   place — it becomes legible at the same instant it stops moving. Letting
   the travel outlast the fade means the copy is readable while it is still
   settling the last few pixels, which is the difference between "appeared"
   and "arrived". Both were .55s across the whole site before this. */
.rv{opacity:0;transform:translateY(var(--rv-y));
  transition:opacity var(--rv-fade) var(--ease),transform var(--rv-rise) var(--ease);}
.rv.in{opacity:1;transform:none;}
/* `data-seq="off"` on an ancestor opts a subtree out of site.js's sequencer,
   for the one sequence authored in absolute time rather than by scroll
   position — the home hero, whose whole stack is on screen at load (see the
   HERO OVERTURE block in index.html). No style attaches to it; it is read
   only by JS. */
/* .d1/.d2/.d3 are now named STOPS on the --i scale, not a second, competing
   `transition-delay` declaration. That matters: several component rules
   (.card.rv, .stat.rv, .mcard.rv, .casecard.rv, .lcell) legitimately
   re-declare the `transition` SHORTHAND, which resets transition-delay to 0
   — so any delay declared at plain .rv specificity was being silently
   zeroed. There is now exactly one transition-delay rule in the system, at
   .rv.rv (0,2,0) and placed after every component override, and everything
   else just contributes a value to --i. */
.rv.d1{--i:2;}.rv.d2{--i:4;}.rv.d3{--i:5;}

/* second reveal in the vocabulary: a clip wipe, for full-bleed media only.
   An image that UNCOVERS reads materially more expensive than an image that
   fades, and it gives the two full-bleed breaks a real entrance instead of
   sharing the same 18px rise as every paragraph on the page. transform is
   reset to none because the wipe IS the movement — stacking a rise under it
   just makes the crop drift. */
.rv.rv-wipe{transform:none;clip-path:inset(0 0 88% 0);
  transition:opacity var(--rv-fade) var(--ease),clip-path 1.05s var(--ease);}
.rv.rv-wipe.in{clip-path:inset(0 0 0% 0);}
@media(prefers-reduced-motion:reduce){.rv.rv-wipe{clip-path:none;}}

/* cards/stats get a slight scale-overshoot on top of the base rise —
   settles a hair past 100% before easing back, reads as a more directed,
   "confident" entrance than a flat fade+rise on every single element. */
/* full property list re-declared here (not just opacity/transform) since
   this rule's specificity otherwise wins the whole `transition` shorthand
   away from .card/.stat's own hover-transition properties, not just adds
   to it — border-color/background need to stay listed or hover snaps instead
   of easing once the card has settled in. */
.card.rv{transform:translateY(var(--rv-y)) scale(.94);
  transition:opacity var(--rv-fade) var(--ease),transform var(--rv-rise) var(--ease-back),border-color var(--dur-3) var(--ease),background var(--dur-3) var(--ease);}
.stat.rv{transform:translateY(var(--rv-y)) scale(.94);
  transition:opacity var(--rv-fade) var(--ease),transform var(--rv-rise) var(--ease-back),border-color var(--dur-3) var(--ease);}
/* THE SETTLED REVEAL STATE MUST NOT OUTRANK THE HOVER STATE.
   `.card.rv.in` is (0,3,0); `.card:hover` is (0,2,0). So from the moment a
   card finished revealing — which is to say, always, by the time a visitor
   could point at it — its hover lift could never apply. Measured with a real
   pointer at 1440x900 on both trees: .card, .stat and .captile all travelled
   0.00px on hover, while .capcard, .roof .cell, .lcell and .notecard (whose
   hover rules live in a page's own <style>, which loads after this file and
   so wins the tie) travelled correctly. Three of the site's main card
   components had a dead hover.

   `:not(:hover)` hands `transform` back the instant the pointer arrives.
   The transition is declared only on the :hover branch, deliberately: a CSS
   transition uses the timing of the state being entered, so the reveal
   (entering the :not(:hover) branch) keeps .card.rv's --rv-rise/--ease-back
   entrance untouched, while the lift itself resolves on the hover clock. */
.card.rv.in:not(:hover),.stat.rv.in:not(:hover){transform:none;}
.card.rv.in:hover,.stat.rv.in:hover{transform:translateY(var(--lift-card));
  transition:transform var(--dur-3) var(--ease),border-color var(--dur-2) var(--ease),background var(--dur-2) var(--ease);}

/* The no-JS fallback delay. Deliberately last, and at (0,2,0), so it
   outranks the component rules above that re-declare the `transition`
   shorthand — but NOT, it turns out, a page-level one: every page's inline
   <style> loads after this file and wins at equal specificity, so
   index.html's `.roof .cell{transition:…}` (also 0,2,0) was silently
   zeroing the delay on all six of those cells. Measured before this pass:
   six .roof .cell reveals, three of them carrying .d1/.d2, all resolving
   transition-delay 0s and landing on the same frame.

   That is why the real delay is now written INLINE by site.js's sequencer
   (see "REVEAL SEQUENCER" there) — an inline declaration is the only thing
   in the cascade a later page stylesheet cannot outrank, and reduced
   motion's `transition:none!important` still beats it. This rule remains
   as the path for a visitor whose JS never runs: .d1/.d2/.d3 keep working,
   just without the grouping. */
.rv.rv{transition-delay:calc(var(--i,0) * var(--rv-step));}

@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;}.rv{opacity:1;transform:none;}}

/* ══════════════════════════════════════════════════════════════════════
   CASE DETAIL PAGES — shared across every /case/*.html. Reuses .stats/
   .stat and .btn from above; everything below is new to this page type. */

/* kept deliberately shorter than .phero — a case hero is a different content
   type (back-link + meta rail rather than a statement headline) and reads
   correctly sitting lower. Expressed on the same scale so it tracks. */
.case-hero{position:relative;min-height:40svh;display:flex;flex-direction:column;justify-content:flex-end;overflow:hidden;padding:calc(var(--sp-7) * 1.3) 0 var(--sp-5);}
.case-hero .mist{position:absolute;inset:-15%;z-index:0;pointer-events:none;overflow:hidden;}
.case-hero .mist span{position:absolute;border-radius:50%;filter:blur(70px);}
.case-hero .mist .b1{width:40vw;height:30vw;left:0;top:0;background:radial-gradient(circle,rgba(var(--green-rgb),.16),transparent 70%);animation:mista 32s ease-in-out infinite alternate;}
.case-hero .mist .b2{width:36vw;height:28vw;right:2%;bottom:0;background:radial-gradient(circle,rgba(var(--sage-rgb),.13),transparent 70%);animation:mistb 42s ease-in-out infinite alternate;}
@keyframes mista{from{transform:translateX(-4%) scale(1)}to{transform:translateX(6%) scale(1.12)}}
@keyframes mistb{from{transform:translateX(5%) scale(1)}to{transform:translateX(-7%) scale(1.08)}}
@media(prefers-reduced-motion:reduce){.case-hero .mist span{animation:none;}}
.case-hero .wrap{position:relative;z-index:1;}
.case-hero .back{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin-bottom:var(--sp-3);transition:color var(--dur-2) var(--ease);}
.case-hero .back:hover{color:var(--green);}
.case-hero .tag{color:var(--green);margin-bottom:16px;}
.case-hero h1{font-family:var(--sans);font-weight:700;text-transform:uppercase;line-height:.98;letter-spacing:-.02em;font-size:clamp(32px,5.2vw,76px);max-width:var(--measure-head);}
.case-hero .dek{margin-top:var(--sp-3);max-width:var(--measure-lead);color:var(--dim);font-size:var(--fs-lead);line-height:1.5;}

/* the meta rail is a horizontal row of label/value pairs, not a card grid —
   it legitimately wants the wider --sp-5 gutter, same as .six .st's 4-up. */
.case-meta{display:flex;flex-wrap:wrap;gap:var(--sp-5);margin-top:var(--sp-4);padding-top:var(--sp-3);border-top:1px solid rgba(var(--white-rgb),.14);}
.case-meta .mi .l{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim-2);}
.case-meta .mi .v{margin-top:var(--sp-1);font-family:var(--sans);font-weight:500;font-size:15px;color:var(--white);}

/* ── the playable case film — starts muted/looping like the rest of the
   site's ambient video (site.js's data-src lazy loader), but a click hands
   control to the visitor: unmute, native controls, no more auto-loop.
   That's what makes this "clickable" rather than decorative background. ── */
.case-player{position:relative;border-radius:24px;overflow:hidden;background:var(--void);box-shadow:0 40px 90px -40px rgba(10,9,8,.6);margin:0 auto;cursor:pointer;}
.case-player.wide{aspect-ratio:16/9;max-width:1100px;}
.case-player.tall{aspect-ratio:9/16;max-width:400px;}
.case-player video{width:100%;height:100%;object-fit:cover;}
.case-player .playhint{position:absolute;left:18px;bottom:18px;z-index:2;display:flex;align-items:center;gap:9px;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--noir-fg);background:rgba(10,9,8,.55);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);padding:9px 15px;border-radius:99px;pointer-events:none;opacity:1;transition:opacity var(--dur-2) var(--ease);}
.case-player .playhint::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--noir-accent);}
.case-player.played .playhint{opacity:0;}

/* ── cursor spotlight — soft radial highlight tracking the pointer over
   the case film (--mx/--my set on mousemove in site.js). Only visible on
   hover, soft-light blended so it reads as tactile sheen, not a spotlight
   prop; the mousemove listener itself is gated behind !calm in site.js,
   so this is inert (never gets non-center --mx/--my) under reduced motion. ── */
.case-player{--mx:50%;--my:50%;}
.case-player::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;opacity:0;
  background:radial-gradient(240px circle at var(--mx) var(--my),rgba(242,237,224,.16),transparent 70%);
  mix-blend-mode:soft-light;transition:opacity .5s var(--ease);}
.case-player:hover::before{opacity:1;}
@media(prefers-reduced-motion:reduce){.case-player::before{content:none;}}

.case-body{max-width:var(--measure-body);}
.case-body h2{font-family:var(--sans);font-weight:600;text-transform:uppercase;font-size:clamp(22px,2.8vw,36px);line-height:1.1;letter-spacing:-.01em;max-width:18ch;}
.case-body .lede{margin-top:var(--sp-3);color:var(--white);font-size:var(--fs-lead);line-height:1.5;}
/* editorial drop-cap on the brief's opening line only (.brief .lede) —
   print-magazine convention: the first paragraph of the piece gets it,
   not every lede reused elsewhere (e.g. the results-section lede). */
.brief .case-body .lede::first-letter{font-family:var(--sans);font-weight:700;color:var(--green);
  font-size:3.4em;line-height:.72;float:left;padding:.05em .07em 0 0;}

/* ── inset pull-quote — breaks the case-body text column with a lifted
   line, print-spread style, rather than leaving copy as flat stacked <p>. ── */
.pullquote{margin-top:var(--sp-4);padding-left:24px;border-left:2px solid var(--green);}
.pullquote p{font-family:var(--sans);font-weight:600;color:var(--accent);font-size:clamp(19px,2vw,25px);line-height:1.4;}
.case-body p{margin-top:var(--sp-2);color:var(--dim);font-size:var(--fs-body);line-height:1.6;}
.case-body p+p{margin-top:14px;}

.case-still{border-radius:20px;overflow:hidden;margin-top:var(--sp-4);box-shadow:0 30px 60px -34px rgba(10,9,8,.5);}
.case-still img{width:100%;height:auto;filter:contrast(1.04) saturate(1.08) brightness(.97);}

/* ── oversized stat hero — the single number that matters most on a case
   page, art-directed as its own full-bleed typographic moment instead of
   staying a small tile in the .stats grid (technique studied from doc.cc:
   a giant number/concept standing in for a photo as the section's hero
   graphic). Used once per case page, on the lead result only -- any
   secondary stats stay in the existing small .stats tiles. ── */
/* was clamp(70px,10vw,140px) — vw driving VERTICAL padding, which inverted
   the section rhythm between a 1440x700 laptop (140px band vs a 91px .zone)
   and a 1024x1366 tablet. Now on the same vh-anchored scale as every other
   section band. */
.case-stat-hero{padding:var(--sp-7) var(--gut);text-align:center;background:var(--void);
  border-top:1px solid rgba(var(--white-rgb),.08);border-bottom:1px solid rgba(var(--white-rgb),.08);}
/* the band is background:var(--void) — permanently dark in BOTH themes —
   so its text is NOIR, not the flipping --green/--dim, which inverted to
   near-black ink on near-black ground in light (2.82:1 and 1.03:1). The
   NOIR values are byte-identical to --green/--dim in dark, so this is a
   zero-pixel change there; same pattern as #numbers .stat above. */
.case-stat-hero .n{font-family:var(--sans);font-weight:700;letter-spacing:-.02em;line-height:.88;
  font-size:clamp(72px,15vw,220px);color:var(--noir-accent);}
.case-stat-hero .lab{margin-top:var(--sp-2);font-family:var(--mono);font-size:13px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--noir-dim);}

.case-nav{display:flex;justify-content:space-between;align-items:center;gap:24px;flex-wrap:wrap;padding:var(--sp-4) 0;border-top:1px solid rgba(var(--white-rgb),.12);border-bottom:1px solid rgba(var(--white-rgb),.12);}
.case-nav a{display:flex;align-items:center;gap:14px;font-family:var(--sans);font-weight:600;text-transform:uppercase;font-size:clamp(15px,1.5vw,20px);color:var(--white);transition:opacity var(--dur-2) var(--ease);}
.case-nav a:hover{opacity:.65;}
.case-nav .next{margin-left:auto;text-align:right;flex-direction:row-reverse;}
.case-nav .arr{font-family:var(--mono);color:var(--green);font-size:18px;}
.case-nav .lab{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--dim-2);margin-bottom:4px;text-transform:uppercase;font-weight:400;}

/* ── closing CTA — was duplicated inline on about/case-studies/future-of-ai;
   promoted here once the case-detail pages made it an 11x-over repeat. ── */
.close{text-align:center;}
.close h2{font-family:var(--sans);font-weight:600;text-transform:uppercase;font-size:clamp(34px,6vw,74px);line-height:1;letter-spacing:-.01em;}
.close .sub{margin-top:var(--sp-3);color:var(--dim);font-size:var(--fs-lead);max-width:var(--measure-lead);margin-left:auto;margin-right:auto;}
.close .btn{margin-top:var(--sp-4);}

/* ── mid-scale section heading — smaller than .close h2's closing-CTA
   scale, for section intros like capabilities.html's matrix header ── */
.zone h2.mid{font-family:var(--sans);font-weight:600;text-transform:uppercase;font-size:clamp(28px,4.4vw,52px);line-height:1.05;letter-spacing:-.01em;max-width:var(--measure-head);}

/* ── persistent case-detail CTA bar — slides up once the hero has fully
   scrolled off, hides again once the page's own .close CTA is in view
   (avoids showing two "start a project" prompts on screen at once). ── */
.cta-bar{position:fixed;left:0;right:0;bottom:0;z-index:90;padding:16px 0;
  background:rgba(28,26,23,.86);border-top:1px solid var(--noir-glass-line);
  backdrop-filter:blur(20px) saturate(1.15);-webkit-backdrop-filter:blur(20px) saturate(1.15);
  transform:translateY(100%);transition:transform .5s var(--ease);}
.cta-bar.show{transform:translateY(0);}
.cta-bar .wrap{display:flex;align-items:center;justify-content:center;gap:20px;flex-wrap:wrap;}
.cta-bar p{font-family:var(--sans);font-size:14px;color:var(--noir-dim);}
.cta-bar p strong{color:var(--noir-fg);font-weight:600;}
@media(max-width:640px){.cta-bar p{display:none;}}
@media(prefers-reduced-motion:reduce){.cta-bar{transition:none;}}


/* ══════════════════════════════════════════════════════════════════════
   COMMAND PALETTE (⌘K / Ctrl+K) — site-wide, keyboard-triggered jump menu.
   Injected once at runtime by site.js (same DOM-injection approach as the
   custom cursor and .grain above), so every real page carries it without
   a single markup edit. Lists the real top-level pages plus every live
   case study — the case list is read straight off case-studies.html's own
   DOM (see site.js) rather than a hand-typed duplicate, so it can never
   drift from what's actually in that grid. Background content is marked
   inert while open (see site.js setInert()) since this is the site's
   first true blocking modal — nothing existing to match a convention
   against, so this follows the WAI-ARIA combobox/listbox pattern: only
   the input is ever focusable, arrow keys move a virtual selection via
   aria-activedescendant, Tab is trapped rather than needing a focus ring
   to cycle across unfocusable list rows. ══════════════════════════════ */
.cmdk-scrim{position:fixed;inset:0;z-index:10010;display:flex;align-items:flex-start;justify-content:center;
  padding:12vh 20px 20px;background:rgba(12,11,10,.6);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  opacity:0;visibility:hidden;pointer-events:none;transition:opacity .25s var(--ease),visibility 0s linear .25s;}
.cmdk-scrim.open{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .25s var(--ease);}
.cmdk-panel{width:100%;max-width:560px;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;
  background:rgba(28,26,23,.88);border:1px solid var(--noir-glass-line);border-radius:20px;
  backdrop-filter:blur(24px) saturate(1.15);-webkit-backdrop-filter:blur(24px) saturate(1.15);
  box-shadow:0 50px 100px -40px rgba(0,0,0,.7);
  transform:translateY(10px) scale(.98);opacity:0;transition:transform .35s var(--ease-back),opacity .3s var(--ease);}
.cmdk-scrim.open .cmdk-panel{transform:none;opacity:1;}
.cmdk-inputwrap{display:flex;align-items:center;gap:12px;padding:18px 20px;border-bottom:1px solid rgba(242,237,224,.1);flex:0 0 auto;}
.cmdk-inputwrap svg{width:16px;height:16px;flex-shrink:0;stroke:var(--noir-dim);}
.cmdk-input{flex:1;min-width:0;background:none;border:none;outline:none;font-family:var(--mono);font-size:14px;color:var(--noir-fg);}
.cmdk-input::placeholder{color:var(--noir-dim-2);}
.cmdk-esc{flex-shrink:0;font-family:var(--mono);font-size:10px;letter-spacing:.06em;color:var(--noir-dim-2);
  border:1px solid rgba(242,237,224,.18);border-radius:6px;padding:3px 7px;}
.cmdk-list{list-style:none;overflow-y:auto;padding:8px;flex:1 1 auto;}
.cmdk-item{display:flex;flex-direction:column;gap:4px;padding:12px 14px;border-radius:12px;cursor:pointer;
  border-left:2px solid transparent;transition:background .15s var(--ease),border-color .15s var(--ease);}
.cmdk-item.active{background:rgba(242,237,224,.08);border-left-color:var(--noir-accent);}
.cmdk-row1{display:flex;align-items:center;gap:10px;}
.cmdk-item .cmdk-t{flex:1;min-width:0;font-family:var(--sans);font-weight:500;font-size:14px;color:var(--noir-fg);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.cmdk-item .cmdk-t b{font-weight:700;color:var(--noir-accent);margin-right:6px;}
.cmdk-item .cmdk-kind{flex-shrink:0;font-family:var(--mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--noir-dim-2);}
.cmdk-item .cmdk-sub{font-family:var(--mono);font-size:11px;color:var(--noir-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.cmdk-empty{margin:0;padding:34px 20px;text-align:center;font-family:var(--mono);font-size:12px;color:var(--noir-dim);}
.cmdk-foot{display:flex;gap:18px;padding:12px 20px;border-top:1px solid rgba(242,237,224,.1);flex:0 0 auto;
  font-family:var(--mono);font-size:10px;letter-spacing:.04em;color:var(--noir-dim-2);}
.cmdk-foot kbd{display:inline-block;font-family:var(--mono);border:1px solid rgba(242,237,224,.18);
  border-radius:4px;padding:1px 5px;margin-right:4px;}
@media(max-width:640px){.cmdk-scrim{padding:8vh 12px 12px;}.cmdk-foot span:not(:last-child){display:none;}}
@media(prefers-reduced-motion:reduce){.cmdk-panel{transform:none;}}

/* ══════════════════════════════════════════════════════════════════════
   LIGHT THEME — ART DIRECTION
   ──────────────────────────────────────────────────────────────────────
   Everything above this line is the shared, theme-agnostic system. This
   block is the light theme's own art direction: the parts of the design
   that are not a colour swap but a genuinely different construction.

   The premise it implements (unchanged from the dark theme's): a light
   EDITORIAL GROUND interrupted by dark CINEMATIC BANDS. The bands stay
   dark in light mode on purpose — that rhythm is the design. What was
   missing is that the light half was never art-directed, so three things
   were left broken and are fixed here:

     1. NOIR chrome floating over a BRIGHT surface. The nav, and the hero
        type, are frozen cream by design (they have to survive arbitrary
        video underneath). On a dark page that is correct; on a cream page
        or a bright sky it is invisible. Fixed with real scrims/plates
        built for light, never by unfreezing the NOIR tokens — other
        surfaces depend on those staying constant.
     2. Plates built by DARKENING. --glass tints toward ink, which lifts a
        card off a dark ground and sinks it into a light one. See --surface.
     3. Band edges. In dark, a --void band against the --bg page is a Δ0.03
        luminance step — invisible, which is why nothing marked it. In
        light the same edge is Δ0.83: a 1px cut from paper to near-black,
        which reads as a rendering fault rather than an edit. The bands are
        kept; the CUT is what gets built. --band-lead ramps the paper into
        the band, and .band-rule sets a hairline on the seam.

   Scoped entirely to :root[data-theme="light"]. The inline <head> script
   on every page sets data-theme before first paint (localStorage, else
   the OS preference), so this attribute — not the media query — is the
   authoritative signal; the @media block up top stays a token-only no-JS
   fallback. Dark renders byte-identically to before this block existed.
   ══════════════════════════════════════════════════════════════════════ */

/* ── NAV — always a real toolbar in light ──────────────────────────────
   The bar's contents are NOIR (see the NAV comment above), so in light
   they need a dark backing on EVERY page and at EVERY scroll position,
   not just on .scrolled / body.text-hero. Without it the links sit
   cream-on-cream: measured 1.01:1 on the 16 case pages, which have a
   plain <body> and a mist-only hero, and 1.33:1 over the home hero's sky.
   Deeper than the dark theme's .82 plate because the same alpha
   composites ~20 luminance points lighter over cream than over --bg. */
/* A PAPER toolbar, not the dark one with a plate under it. The first pass
   here gave .nav a dark plate in light — which fixed legibility (the bar's
   contents are NOIR) but left a near-black band cutting across a cream page
   on every route: measured as the single hardest edge on the site at
   Δ0.75 luminance, worse than any of the section seams this pass set out to
   remove. Dark chrome on a light page is the "token swap" tell.

   So the nav gets real light-mode counterparts instead. Per the NOIR note
   in :root, the frozen tokens themselves are NOT touched — other surfaces
   (the command palette, the case cta-bar, the mobile panel's own scrim)
   depend on them staying constant. These are additive overrides for this
   one component, in this one theme.

   The bar still floats over the home hero's video, which is why it keeps a
   real backdrop-filter and a translucent fill rather than a flat colour:
   over the dark hero it reads as a lit paper rail, over the cream pages it
   nearly dissolves into the ground. */
:root[data-theme="light"] .nav{
  background:rgba(var(--paper-rgb),.82);
  backdrop-filter:blur(22px) saturate(1.25);-webkit-backdrop-filter:blur(22px) saturate(1.25);
  border-bottom-color:rgba(var(--white-rgb),.12);
}
/* ONE ADDITION, and it is the paper toolbar keeping its own promise rather
   than a second design. The alphas above were set against the ground the
   bar usually floats on — the cream page — where .82 paper over cream
   composites to rgb(249,246,236) and the numbers in the comments hold. The
   seven .phero.has-photo pages put a DARK PHOTOGRAPH under it instead, and
   .82 over that composites to about rgb(208,205,195): the plate loses ~40
   luminance points and everything on it loses contrast with it. Measured,
   worst glyph pixel, light theme, over the hero:

     on `main` (the shader hero, whose light scrim is .66-.70 near-black,
     i.e. the same problem one stop shallower)   nav .lnk 4.40-4.53
     with the photographs and .82 unchanged                    4.28-4.36
     with .94                                                  see report

   In both cases the worst element is the ACTIVE link, gold #77560f, which
   the comment above measures at 6.1:1 over the cream page. So this is not
   a new failure, it is an existing one this pass makes about 0.1 worse and
   then fixes, and it is fixed by restoring the plate rather than by
   swapping the toolbar's colours: a near-opaque paper rail over the
   photograph is still a paper rail, where a dark bar would be exactly the
   "token swap tell" the note above rejects.

   :not(.scrolled) is the whole scope. Past 24px the bar is over the page's
   own cream ground again, where .82 was measured and is correct. */
:root[data-theme="light"] body.photo-hero .nav:not(.scrolled){background:rgba(var(--paper-rgb),.94);}
:root[data-theme="light"] .nav .brand{color:var(--white);}
:root[data-theme="light"] .nav .r a.lnk{color:var(--dim-2);}        /* 5.6:1 */
:root[data-theme="light"] .nav .r a.lnk:hover{color:var(--white);}
:root[data-theme="light"] .nav .r a.lnk.on{color:var(--accent);}    /* 6.1:1 */
:root[data-theme="light"] .theme-toggle,
:root[data-theme="light"] .navToggle{
  color:var(--white);background:rgba(var(--white-rgb),.06);border-color:rgba(var(--white-rgb),.16);
}
:root[data-theme="light"] .navToggle span{background:var(--white);}
:root[data-theme="light"] .nav .r .chase{
  background:var(--accent);box-shadow:0 0 10px rgba(var(--accent-rgb),.45);
}
/* the slide-down mobile panel is the same toolbar continued downward, so it
   follows it onto paper rather than staying a dark sheet hanging off a
   light bar. Its scrim stays black — that is a shadow, not a surface. */
:root[data-theme="light"] .navMobilePanel{
  background:rgba(var(--paper-rgb),.97);border-bottom-color:rgba(var(--white-rgb),.12);
}
:root[data-theme="light"] .navMobilePanel-links a{
  color:var(--white);border-bottom-color:rgba(var(--white-rgb),.12);
}
:root[data-theme="light"] .navMobilePanel-links a.on{color:var(--accent);}
:root[data-theme="light"] .navMobilePanel-search{color:var(--dim-2);}

/* ── BUTTON — ink on the lime pill ────────────────────────────────────
   .btn is cream (--noir-fg) sitting on its own ::before lime fill. Cream
   on #eefb87 is 1.05-2.69:1 — the single most repeated contrast failure
   on the site ("Work with MUUS", "Book a call", every .cta-bar). The
   filled variant takes the ink token instead: #1f1e1b on the lime pill
   measures 15.5:1, and because the pill is a NOIR constant the fix holds
   over cream ground and --void band alike. Any variant whose ::before is
   transparent keeps cream — there is no lime under it to sit on. */
:root[data-theme="light"] .btn{color:var(--noir-ink);}
/* .ghost suppresses the lime fill (its ::before is transparent), so there
   is no pill under the label — it keeps cream, and the glass shell behind
   it is already NOIR because .ghost only ever appears inside the --void
   .contact band, which the NOIR-BAND rule below repoints wholesale. */
:root[data-theme="light"] .btn.ghost{color:var(--noir-fg);}
/* the pill itself was only 55% lime, so on a dark band it composited to a
   dull olive (#8f9554) and even ink text only reached 5.2:1. In light it
   becomes a real accent pill — ink on near-solid lime, 14.9:1 — which is
   both the accessible answer and the more deliberate-looking one. :not(
   .ghost) is load-bearing: .ghost's whole definition is a transparent
   ::before, and this selector would otherwise out-specify it. */
:root[data-theme="light"] .btn:not(.ghost)::before{background:rgba(238,251,135,.92);}
:root[data-theme="light"] .btn:not(.ghost):hover::before{background:rgba(238,251,135,.98);}

/* stat captions and film labels sit on the dark bands' own glass over a
   photograph; --noir-dim (.62) lands at 3.9-4.5:1 there. At 10-14px they
   are body-grade text, so they take the full NOIR foreground in light. */
/* #numbers .stat .lab further up carries an ID (specificity 1,1,1), so the
   class-only form of this rule lost to it and the home page's captions
   stayed at --noir-dim. Named explicitly rather than relying on order. */
:root[data-theme="light"] .noir-band .stat .lab,
:root[data-theme="light"] #numbers .stat .lab{color:var(--noir-fg);}
:root[data-theme="light"] .vfl i{color:var(--noir-fg);}
/* the diptych's bottom-up scrim cleared by 58%, leaving its labels on bare
   video, and its top edge butted the paper section above it (Δ0.53). One
   rule for both pages that use the component. */
:root[data-theme="light"] .vbleed-img::after{
  background:linear-gradient(0deg,
    rgba(28,26,23,.86) 0%,rgba(28,26,23,.10) 58%,
    rgba(var(--paper-rgb),.42) 92%,var(--paper) 100%);
}

/* ── NOIR BANDS — the mechanism, instead of 25 hand-swapped colours ────
   A --void band is a dark SURFACE that happens to sit on a light PAGE.
   Its contents were written against the theme tokens (--white/--dim/
   --green/--glass), which flip to ink in light and then paint near-black
   on near-black: measured 1.00-1.06:1 across whole sections — the process
   stage list, both proof blocks, the "four calls that stay human" list,
   the capabilities marquee, the case hero stat. Previously this was
   patched one declaration at a time (see #numbers .stat above), which is
   why it kept regressing as sections were added.

   Repointing the tokens themselves at their NOIR twins for the subtree is
   the actual fix: every descendant, including ones not written yet, gets
   the right palette. And it is provably free in dark, because there the
   two sets are byte-identical — --white #f2ede0 IS --noir-fg, --green
   #eefb87 IS --noir-accent, --glass .12 IS --noir-glass, and so on down
   the list. Verified as a zero-property computed-style diff across all 12
   page templates.

   --bg / --paper are deliberately NOT repointed: the band-edge ramps below
   are painted on these same elements and need the PAGE's ground colour to
   ramp from. */
/* Marked in the markup, NOT inferred from section names: .proof is a void
   band on process/about but sits on the cream ground on capabilities, and
   .work is dark on the home page but cream on all 16 case pages. A name
   list would have silently inverted both. .case-stat-hero is named
   directly because it is a base.css component with one meaning. */
/* .phero.has-shader-mist is the third named case, and the reason it is a
   CLASS test rather than markup: the class is added by site.js only once
   the WebGL mist has actually compiled and been inserted. When it hasn't
   (no WebGL, reduced-motion, compile failure) the hero falls back to the
   CSS blob mist over the plain page ground — cream in light — where NOIR
   type would be exactly wrong. Tying the repoint to the same signal that
   puts the dark canvas on screen means the two can never disagree. */
/* .phero.has-photo joins for the same reason .phero.has-shader-mist did,
   and on the same kind of signal: the class is on the header only where a
   photograph is actually in the markup, and those photographs are dark in
   both themes, so cream/lime is the right answer in both. */
.noir-band,.case-stat-hero,.phero.has-shader-mist,.phero.has-photo{
  /* Repointing tokens only reaches elements that actually say var(--white).
     Anything with no colour of its own — .stat .n, most h2s — inherits the
     value <body> already resolved, which is ink. So the band states its own
     inherited colour too. In dark this computes to the identical #f2ede0
     body already passed down, hence still a zero-diff. */
  color:var(--noir-fg);
  --white:var(--noir-fg);
  --white-rgb:242,237,224;
  --dim:var(--noir-dim);
  --dim-2:var(--noir-dim-2);
  --green:var(--noir-accent);
  --green-rgb:238,251,135;
  --accent:var(--noir-accent);
  --accent-rgb:238,251,135;
  --ink:var(--noir-ink);
  --glass:var(--noir-glass);
  --glass-line:var(--noir-glass-line);
  --surface:var(--noir-glass);
  --surface-line:var(--noir-glass-line);
  --surface-hover:rgba(242,237,224,.22);
}

/* ── BAND EDGES ───────────────────────────────────────────────────────
   Applied to the flat --void bands (no full-bleed photo child of their
   own, so the section's own background-image is free to paint the ramp).
   Photo-backed bands carry the same treatment inside their existing
   ::after scrim, on their own page, because there the ramp has to
   composite with the photograph rather than replace it.

   --lead-top / --lead-bot are per-section so a band that meets paper on
   one edge and another dark band on the other only ramps where there is
   actually something to ramp from. rgba(var(--bg-rgb),0) — not the
   `transparent` keyword, which is rgba(0,0,0,0) and would drag the ramp
   grey through its midpoint. */
/* .band-cut is the subset of .noir-band that meets the paper ground
   DIRECTLY, i.e. flat --void sections with no full-bleed photo child of
   their own — so the section's own background-image is free to paint the
   ramp. Photo-backed bands (the home page's) carry the same treatment
   inside their existing ::after scrim instead, because there the ramp has
   to composite with the photograph rather than replace it. */
/* THE 1PX CREAM HAIRLINE. This block also sets a border on both edges (see
   below), and a border changes where a gradient lives: background-origin
   defaults to padding-box, so the ramp's 0% and 100% sit INSIDE the border,
   while background-clip:border-box paints it across the border too. The
   row the border occupies is therefore outside the gradient's own box, and
   background-repeat defaults to `repeat` — so that row tiles the gradient's
   opposite edge. The bottom stop is solid --bg. Result: a full-width 1px
   line of PAPER laid across the top of every dark band, in the one block
   whose entire purpose is to stop dark bands being cut into a light page.
   Measured in light across the #bolt -> #contact seam, mean row colour:
   rgb(10,15,12) above, rgb(211,208,196) on the seam row, rgb(29,27,24)
   below. Isolated by elimination — the line survived setting the border
   transparent (it went brighter, 241,237,225, the untinted paper) and
   vanished only with background-image:none.

   background-origin:border-box puts the ramp's own box where it is
   actually painted, which both removes the tiling and makes the stated
   lead lengths true — they were previously measured from inside the
   border. no-repeat is belt and braces.

   --lead-c-top / --lead-c-bot are the second half of the fix: with the box
   corrected, .contact's top row now renders the ramp's FIRST stop, and
   "no lead on this edge" was expressed as --lead-top:0px, which collapses
   the first two stops onto 0% and leaves a solid --bg first stop painting
   that row. Suppressing an edge has to mean naming its colour transparent,
   not zeroing its length. */
:root[data-theme="light"] :is(.band-cut,.case-stat-hero){
  background-image:linear-gradient(180deg,
    var(--lead-c-top,var(--bg)) 0,
    rgba(var(--bg-rgb),0) min(var(--lead-top,0px),22%),
    rgba(var(--bg-rgb),0) calc(100% - min(var(--lead-bot,0px),22%)),
    var(--lead-c-bot,var(--bg)) 100%);
  background-origin:border-box;background-repeat:no-repeat;
  --lead-top:var(--band-lead);--lead-bot:var(--band-lead);
}
/* .contact is preceded by another dark band and followed only by the cream
   footer — it has one edge to resolve, not two. */
:root[data-theme="light"] .contact{--lead-top:0px;--lead-c-top:rgba(var(--bg-rgb),0);--lead-bot:var(--band-lead);}
/* the hairline that says the cut is deliberate. Ink-side, sitting on the
   paper half of the seam, so it reads as an editorial rule not a glow. */
:root[data-theme="light"] :is(.band-cut,.case-stat-hero){
  border-top:1px solid rgba(33,30,23,.14);
  border-bottom:1px solid rgba(33,30,23,.14);
}

/* ══════════════════════════════════════════════════════════════════════
   THE BAND RHYTHM
   ══════════════════════════════════════════════════════════════════════
   The home page opens on a photograph and then CHANGES GROUND on its way
   down: measured, 1440x900, median rendered pixel luminance per top-level
   band, dark theme —

     hero .536 · manifesto .199 · logos .027 · numbers .298 · bleed .055 ·
     work .047 · chair .074 · ribbon .046 · process .054 · method .007 ·
     light .061 · bolt .051 · contact .012 · footer .045

   and in light — .534 · .858 · .028 · .106 · .021 · .042 · .122 · .863 ·
   .019 · .008 · .733 · .053 · .013 · .863. Bright open, paper read, dark
   run, paper, dark run, paper, dark close. The seven interior pages had
   none of it: measured on the same rig, about's `why` -> `philosophy`
   stepped 0.0000, capabilities' `proof` -> matrix 0.0000, insights and
   future-of-ai the same. Four grounds' worth of vocabulary existed and
   the interiors used one of them for thousands of pixels at a stretch.

   FOUR GROUNDS, brightest first. Measured by painting each token and
   reading the rendered pixel, dark / light:

     photo   whatever the frame is        — OPEN, SHOW, CUT
     plate   var(--paper)   .061 / .922   — READ: the page in its own voice
     page    var(--bg)      .045 / .863   — the ground between
     void    var(--void)    .010 / .010   — SHOW / structural bands

   ONE RULE, and it is the whole of the rhythm: no two consecutive bands
   sit on the same ground. That is what the interiors were breaking.

   AND THE HONEST LIMIT. base.css already states it above: in dark a --void
   band against the --bg page is a 0.03 step and reads as nothing. So the
   plate/page/void alternation is a LIGHT-theme instrument, and dark's
   rhythm is carried by the photographs — which is exactly how index does
   it (six photographic beds, and the three brightest bands in the list
   above are all photographs). Hence: change of ground in light, change of
   TEXTURE in dark, from the same markup.

   Three components below, and each one resolves BOTH its edges into the
   ground of its neighbour — --gin above, --gout below — because a band
   with an authored top and a cut bottom is not authored. */

/* ── .band-cut, in dark as well ────────────────────────────────────────
   The light-theme block above ramps paper into a --void band over
   --band-lead. Dark never got the same ramp, so every dark band on the
   interiors still arrived in ONE row: measured at 1440x900 on
   process.html, --bg .045 -> --void .010 across a single pixel row at
   every one of its four band edges (0.0347 / 0.0412 / 0.0415 / 0.0510).
   The gradient is written entirely in --bg / --bg-rgb, both of which
   flip, so it is the same declaration — it was only ever scoped away.
   .case-stat-hero is deliberately NOT in this selector: it renders on
   case/ pages, which are out of scope for this pass, and excluding it
   keeps them byte-identical. */
:root[data-theme="dark"] .band-cut{
  background-image:linear-gradient(180deg,
    var(--lead-c-top,var(--bg)) 0,
    rgba(var(--bg-rgb),0) min(var(--lead-top,0px),22%),
    rgba(var(--bg-rgb),0) calc(100% - min(var(--lead-bot,0px),22%)),
    var(--lead-c-bot,var(--bg)) 100%);
  background-origin:border-box;background-repeat:no-repeat;
  --lead-top:var(--band-lead);--lead-bot:var(--band-lead);
}
:root[data-theme="dark"] .contact{--lead-top:0px;--lead-c-top:rgba(var(--bg-rgb),0);--lead-bot:var(--band-lead);}

/* ── PLATE — the reading ground ────────────────────────────────────────
   var(--paper) is the site's existing lifted plate; index's `.light`
   section is built from it and measures .061 dark / .733 light against
   .007 and .053 neighbours. Nothing new is invented here — the token is
   given band edges so it can be used as a band instead of only as a
   card ground. Both edges ramp to --gin / --gout, defaulting to the page.
   `background-color` and `background-image` are set separately on
   purpose: a shorthand would drop the colour every time a page restates
   only the ramp. */
/* ONE OWNER PER SEAM. Two bands that both ramp into each other put an
   opaque row of each colour on either side of the boundary and make the
   seam WORSE than no ramp at all. The rule this file follows: the dark or
   photographic band owns both of its edges (that is what .band-cut has
   always done), and a plate suppresses whichever of its own edges meets
   one — --gin-a / --gout-a to 0. Suppressing an edge means naming its
   colour transparent, never zeroing its length; see --lead-c-top above. */
.band-plate{position:relative;
  background-color:var(--paper);
  background-image:linear-gradient(180deg,
    rgba(var(--gin,var(--bg-rgb)),var(--gin-a,1)) 0,
    rgba(var(--gin,var(--bg-rgb)),0) min(var(--plead-top,var(--band-lead)),22%),
    rgba(var(--gout,var(--bg-rgb)),0) calc(100% - min(var(--plead-bot,var(--band-lead)),22%)),
    rgba(var(--gout,var(--bg-rgb)),var(--gout-a,1)) 100%);
  background-origin:border-box;background-repeat:no-repeat;}
/* .zone.atmos's lime blob is painted by ::before at z 0 and the plate is
   the element's own background, so the blob still reads on top of it —
   which is what keeps a plated .zone.atmos from looking like a flat swatch. */

/* ── PHOTO BED — a photograph as a band's ground ───────────────────────
   The same four parts as .phero.has-photo, in the same order and the same
   expression (frame / grade / wash / lead), with one difference the
   position forces: a header resolves DOWNWARD only, a mid-page band has a
   neighbour on both sides, so there are two leads instead of one.
   Markup: <section class="… noir-band"><div class="bandphoto"><img
   loading="lazy" …></div><div class="wrap">…</div></section>. */
.bandphoto{position:absolute;inset:0;z-index:0;overflow:hidden;pointer-events:none;}
.bandphoto img{position:absolute;left:0;top:var(--frame-top,0%);width:100%;height:var(--frame-h,100%);
  object-fit:cover;object-position:var(--frame-x,50%) var(--frame-y,50%);
  /* the site's one filmic grade, shared with the hero and every other
     full-bleed photo, rather than showing raw source flat and desaturated */
  filter:contrast(1.04) saturate(1.08) brightness(.99);}
/* Painted top-down as written, so the two leads sit OVER the wash and the
   band hands over to the real page colour rather than to a scrimmed
   version of it — index's argument on .manifesto-bg::after, unchanged.
   The lead-out is EASED for the reason stated on .phero.has-photo::after:
   alpha->luminance is convex, so a linear ramp puts its biggest single-row
   step at the opaque end. --w1..--w4 are the wash; every page that uses
   this sets its own from the type map it measured. */
.bandphoto::after{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(180deg,rgba(var(--gin,var(--bg-rgb)),var(--gin-a,1)) 0,
      rgba(var(--gin,var(--bg-rgb)),calc(.93 * var(--gin-a,1))) calc(var(--band-lead) * .15),
      rgba(var(--gin,var(--bg-rgb)),calc(.78 * var(--gin-a,1))) calc(var(--band-lead) * .36),
      rgba(var(--gin,var(--bg-rgb)),calc(.40 * var(--gin-a,1))) calc(var(--band-lead) * .57),
      rgba(var(--gin,var(--bg-rgb)),0) var(--band-lead)),
    linear-gradient(0deg,rgba(var(--gout,var(--bg-rgb)),var(--gout-a,1)) 0,
      rgba(var(--gout,var(--bg-rgb)),calc(.93 * var(--gout-a,1))) calc(var(--band-lead) * .15),
      rgba(var(--gout,var(--bg-rgb)),calc(.78 * var(--gout-a,1))) calc(var(--band-lead) * .36),
      rgba(var(--gout,var(--bg-rgb)),calc(.40 * var(--gout-a,1))) calc(var(--band-lead) * .57),
      rgba(var(--gout,var(--bg-rgb)),0) var(--band-lead)),
    linear-gradient(180deg,
      rgba(var(--band-shade,16,15,13),var(--w1,.72)) 0%,
      rgba(var(--band-shade,16,15,13),var(--w2,.80)) 34%,
      rgba(var(--band-shade,16,15,13),var(--w3,.86)) 72%,
      rgba(var(--band-shade,16,15,13),var(--w4,.90)) 100%);}

/* THE SAME DECISION .phero.has-photo .sub MAKES. A --void band is flat
   near-black (L .010) and .62 cream on it is comfortable; a PHOTOGRAPH
   under the same band is not flat, and the label under a statistic is not
   tertiary text. Measured on process's .proof at 1440x900, worst glyph
   pixel, shadow given no credit: .62 cream landed 4.13 against the 4.5
   bar, on rgb(74,72,70). Full cream lands 7.76 — which is exactly the
   value the light theme ALREADY repoints this element to for every
   .noir-band, so this only brings dark into line with light rather than
   inventing a third answer. Scoped to bands that actually have a bed. */
.bandphoto ~ .wrap .stat .lab{color:var(--noir-fg);}

/* ── CUT — a wordless full-bleed strip ────────────────────────────────
   Punctuation, not decoration: it carries no type, so the photograph runs
   at full strength and only the two edges are authored. index's `.bleed`
   is the same idea; this is the version that can sit between any two
   grounds because it names them. */
.bandstrip{position:relative;overflow:hidden;background:var(--void);
  height:var(--strip-h,clamp(220px,30vh,380px));}
.bandstrip .bandstrip-imgs{display:flex;width:100%;height:100%;}
.bandstrip .bandstrip-img{flex:1;position:relative;height:100%;overflow:hidden;}
.bandstrip img{width:100%;height:100%;object-fit:cover;display:block;
  object-position:var(--sx,50%) var(--sy,50%);
  filter:contrast(1.04) saturate(1.08) brightness(.99);
  transform:scale(1.001);transition:transform 1.6s cubic-bezier(.22,1,.36,1);}
.bandstrip .bandstrip-img:hover img{transform:scale(var(--zoom-media));}
@media(prefers-reduced-motion:reduce){.bandstrip .bandstrip-img:hover img{transform:scale(1.001);}}
@media(max-width:640px){.bandstrip{--strip-h:clamp(180px,26vh,260px);}}

/* ── EDGES ON A BAND THAT ALREADY OWNS ITS OWN BACKGROUND ─────────────
   .band-cut works by painting the ramp INTO the section's background, so
   it only serves a flat band. A band whose ground is a photograph, a
   video wall, or a row of media children has that background covered by
   its own content, and the ramp has to be an overlay above them instead.
   Same two leads, same --gin / --gout, expressed as ::after. z 2 clears
   the per-cell scrims .vbleed/.bleed already paint at z 1.
   The lead is EASED for the reason stated on .phero.has-photo::after, and
   with that hero's own stop distribution rather than a guessed one: read
   from the opaque end, alpha 1 -> .93 over the first 15% of the run, ->
   .78 by 36%, -> .40 by 57%, -> 0 at 100%. Alpha->luminance is convex, so
   the biggest single-row step lands where alpha is HIGHEST, and spending
   a sixth of the length on the first .07 of alpha is what flattens it.
   Measured on process's .proof -> .close in light, a .62-at-28% shape gave
   0.0391 per row at its steepest; this one gives 0.0243. */
.band-edges::after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
  background:
    linear-gradient(180deg,rgba(var(--gin,var(--bg-rgb)),var(--gin-a,1)) 0,
      rgba(var(--gin,var(--bg-rgb)),calc(.93 * var(--gin-a,1))) calc(var(--band-lead) * .15),
      rgba(var(--gin,var(--bg-rgb)),calc(.78 * var(--gin-a,1))) calc(var(--band-lead) * .36),
      rgba(var(--gin,var(--bg-rgb)),calc(.40 * var(--gin-a,1))) calc(var(--band-lead) * .57),
      rgba(var(--gin,var(--bg-rgb)),0) var(--band-lead)),
    linear-gradient(0deg,rgba(var(--gout,var(--bg-rgb)),var(--gout-a,1)) 0,
      rgba(var(--gout,var(--bg-rgb)),calc(.93 * var(--gout-a,1))) calc(var(--band-lead) * .15),
      rgba(var(--gout,var(--bg-rgb)),calc(.78 * var(--gout-a,1))) calc(var(--band-lead) * .36),
      rgba(var(--gout,var(--bg-rgb)),calc(.40 * var(--gout-a,1))) calc(var(--band-lead) * .57),
      rgba(var(--gout,var(--bg-rgb)),0) var(--band-lead));}

/* ── THE SHADER-MIST HERO (about / process / capabilities / future-of-ai)
   ──────────────────────────────────────────────────────────────────────
   These four heroes had the token conflict this whole pass exists to kill,
   in its purest form: `.phero .tag` was authored `color:var(--green)` on
   each page and `.phero.has-shader-mist .sub` was authored
   `color:var(--noir-dim)` in this file. In dark those two are the same
   family so nobody noticed; in light the first flips to gold ink and the
   second stays cream — two opposite answers, six inches apart, on ONE
   backdrop. Whichever way the backdrop went, one of them was wrong.

   The backdrop is not a photograph, it is the WebGL mist in site.js, and
   it is theme-independent: one fragment shader mixing --void -> --taupe ->
   --sage -> lime through domain-warped fbm. --sage is #d6e2dc. So the
   "dark" hero is in fact a field that runs from near-black to very nearly
   WHITE, and its bright passes travel straight through the type. Measured
   over 6 shader timestamps at 1440x900, sampling every rendered pixel
   under each text box and compositing the text colour (alpha included)
   onto it, the brightest pixel found under the copy was rgb(217,227,212).

   Resolution, in two halves:

   1. TOKENS. The hero joins the .noir-band list above, so tag / h1 / sub
      all resolve to the frozen NOIR set and stop disagreeing. Free in
      dark: the NOIR values are byte-identical to the themed ones there.
   2. A REAL SCRIM. Tokens alone cannot fix this — cream on #d6e2dc is
      1.13:1 no matter which cream you pick. The mist needs something
      between it and the type. .70 of near-black held flat through the
      type zone (the copy occupies 20%-91% of the hero box on all four
      pages) takes the worst-case backdrop under the copy from
      rgb(217,227,212) to rgb(77,80,74), and the field still reads as a
      field: its dark end only moves from rgb(28,26,23) to rgb(24,23,20),
      so the mist keeps roughly its full internal range, just seated a
      stop and a half down. The last 7% resolves to the paper ground, the
      same lead-out .band-cut gives every other dark band in this theme —
      previously this hero cut to cream on a 1px edge.

   Measured on the real rendered pixels, WORST glyph pixel, all four pages,
   before (main) -> after (here). Method: three screenshots per element —
   as-rendered, glyphs removed with the shadow kept, glyphs removed with the
   shadow removed — so only true glyph interiors are sampled and the
   text-shadow gets no credit.
     .tag  needs 4.5 (13px mono)   1.15-1.18 -> 10.85-11.66
     .sub  needs 3.0 (28px large)  1.09-1.16 ->  7.19- 7.42
     h1    needs 3.0 (95px+)       2.56-2.76 ->  7.10- 7.10
   Same treatment carries the six other shader-mist heroes (insights,
   case-studies, onboard, privacy, careers, 404): 18 probes, 18 pass.

   LIGHT ONLY, and that is a deliberate stop, not an oversight. FLAG FOR
   REVIEW: the same measurement run against DARK on `main` fails on all
   twelve probes, and fails harder — .tag 3.14-3.67 (needs 4.5), h1 1.13
   on every page, .sub 1.09-1.13 (both need 3). Cream and lime sitting on
   a shader pass that reaches rgb(217,227,212) is a contrast failure in
   whatever theme you are in; light was simply the half that got looked at.
   Dark is the live default and this pass is under a hard instruction that
   it must render identically to main, so unilaterally repainting it is not
   this change's call. Deleting the `:root[data-theme="light"]` guard on the
   rule below is the entire fix, and it is a deliberate, visible change to
   the shipped dark hero on ten pages — a human decision, not a merge one. */
:root[data-theme="light"] .phero.has-shader-mist::after{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(16,15,13,.66) 0%,
    rgba(16,15,13,.70) 16%,
    rgba(16,15,13,.70) 88%,
    rgba(16,15,13,.62) 93%,
    rgba(var(--bg-rgb),.34) 97.5%,
    var(--bg) 100%);
}
/* .sub is --noir-dim, the TERTIARY token, and on seven of the ten heroes it
   is 28px — large text, 3:1, and it clears that on the scrim above (3.89-
   3.95). On privacy / careers / 404 the page authors it at 15px instead,
   which makes it body copy at a 4.5:1 bar, and .62 cream only reaches
   3.92-4.33 there. Same call Builder 1 already made for the home hero's own
   .sub and for .noir-band .stat .lab: a paragraph is not tertiary text, so
   in light it takes the full NOIR foreground. 15px subs go to 7.3:1, the
   28px ones to ~7.3 as well — the hierarchy is still carried by size, which
   is a 2x difference, rather than by a 38% alpha on a photograph. Light
   only; dark keeps --noir-dim exactly as shipped. */
:root[data-theme="light"] .phero.has-shader-mist .sub{color:var(--noir-fg);}

/* ── card interior on the new plate ───────────────────────────────────
   .hd's rule and the .card::before conic ring were both drawn for a
   cream-on-dark card; on paper they need the ink side of the same idea. */
:root[data-theme="light"] .card .hd{border-bottom-color:rgba(var(--white-rgb),.16);}

/* ── TYPE RIBBON ──────────────────────────────────────────────────────
   Decorative (aria-hidden), so it is exempt from a text threshold — but
   at .16 ink on cream it measured 1.37:1, which reads as a dead band
   where the dark theme reads it as texture. .30 restores the texture
   without promoting it to content. */
:root[data-theme="light"] .ribbon .rin{color:rgba(var(--white-rgb),.30);}

/* ── TOUCH TARGETS ──────────────────────────────────────────────────────────
   Measured on real device emulation (isMobile+hasTouch, iPhone UA, DPR 2/3),
   not a narrow desktop window. Everything below is gated at max-width:820px —
   the breakpoint this stylesheet already uses for .cards and .stats — so the
   desktop that shipped last night is untouched by construction, not by
   inspection.

   Two techniques, chosen per case:
   - real padding where the element sits in normal flow and a little more
     mobile breathing room costs nothing. Padding is layout, so neighbouring
     hit areas can never overlap.
   - a transparent pseudo-element where padding would move something that must
     not move (the fixed nav bar's height, the footer's brand/menu row). The
     visible glyphs stay exactly where they were; only the hit area grows.
   Type size, colour and position are unchanged in every case. ── */
@media(max-width:820px){

  /* footer utility menu — 6 links x 8 pages, the largest single group of
     undersized targets on the site. Measured 58.5 x 16.5. 14px block padding
     takes them to 44.5 tall; row-gap drops to 0 because the padding now
     supplies that separation itself, so the rows do not double-space. */
  footer .m{gap:0 22px;}
  footer .m a{display:inline-block;padding:14px 0;}

  /* nav wordmark — measured 110.4 x 33 inside a fixed toolbar whose height is
     an art-directed constant. Padding would grow the bar, so the hit area is
     extended by a transparent ::after instead: same 22px font, same baseline,
     44px of finger. .nav .brand is already position:relative (the .brand-spark
     charge effect needs it) and has no other ::after. */
  .nav .brand::after{content:"";position:absolute;left:0;right:0;top:50%;height:44px;transform:translateY(-50%);}

  /* footer wordmark — measured 97.1 x 28.5. Same reasoning: footer .in is a
     centre-aligned flex row, so padding here would push the whole row taller. */
  footer .brand{position:relative;}
  footer .brand::after{content:"";position:absolute;left:0;right:0;top:50%;height:44px;transform:translateY(-50%);}

  /* small buttons — "Propose a time" measured 169.8 x 41, three pixels short. */
  .btn.sm{padding:14px 22px;}
}
