/* ==========================================================================
   Catalyst Apps — landing page
   Design tokens derived from "Catalyst Brand Guidelines" (59pp).
   Colour §3.1–3.6 · Typography §4.1–4.3 · Identity system §6 · Grids §8
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. The brand face — §4.1
   BDO Grotesk, self-hosted from assets/fonts/ (SIL Open Font License 1.1; see
   assets/fonts/OFL.txt). Four faces, because four is what the page asks for —
   the family also ships Light, ExtraBold and Black if a later surface needs
   them. There is no second typeface anywhere in this system: metadata is
   separated from body copy by weight, size and colour, never by family or by
   setting it in capitals.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "BDO Grotesk";
  src: url("assets/fonts/BDOGrotesk-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "BDO Grotesk";
  src: url("assets/fonts/BDOGrotesk-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "BDO Grotesk";
  src: url("assets/fonts/BDOGrotesk-DemiBold.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "BDO Grotesk";
  src: url("assets/fonts/BDOGrotesk-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
  color-scheme: dark;

  /* Core colours — §3.1 */
  --red:        #FF230A;   /* Catalyst Red */
  --red-hot:    #FF3A22;   /* Catalyst Red, one step hotter: hover only */
  --red-ink:    #FF230A;   /* red used as TYPE; overridden on light grounds */
  --focus:      #FF230A;   /* focus ring; overridden per ground */
  --orange:     #FF7F0F;
  --yellow:     #FEC062;
  --hot-white:  #FCFAF8;

  /* Extended colours — §3.2 */
  --coal:       #190402;
  --charcoal:   #2D0F0B;
  --burnt-red:  #682E27;
  --terracotta: #935352;
  --hazy-pink:  #E7B1F1;
  --pink-dust:  #F4DCF9;
  --blue:       #055DE2;
  --ice:        #D9EEF2;
  --grey:       #7B6665;
  --clay:       #B3978F;
  --dry-clay:   #CEABA0;
  --blush:      #ECDCDC;

  /* Semantic surfaces */
  --bg:          var(--coal);
  --surface:     var(--charcoal);
  --surface-2:   #200806;
  --ink:         var(--hot-white);
  --ink-dim:     color-mix(in srgb, var(--hot-white) 62%, transparent);
  --ink-faint:   color-mix(in srgb, var(--hot-white) 50%, transparent);
  --hairline:    color-mix(in srgb, var(--hot-white) 12%, transparent);
  --hairline-2:  color-mix(in srgb, var(--hot-white) 7%, transparent);

  /* Core gradients — §3.3 (2-colour, Catalyst Red as the root) */
  --g-sunset: linear-gradient(180deg, #FEC062 0%, #FF230A 100%);
  --g-flare:  linear-gradient(180deg, #FF7F0F 0%, #FF230A 100%);
  --g-scorch: linear-gradient(180deg, #682E27 0%, #FF230A 100%);
  --g-ember:  linear-gradient(180deg, #190402 0%, #FF230A 100%);

  /* Extended gradients — §3.4 (use sparingly, for maximum impact) */
  --g-inferno:   linear-gradient(180deg, #FCFAF8 0%, #FEC062 34%, #FF7F0F 62%, #FF230A 100%);
  --g-plasma:    linear-gradient(180deg, #D9EEF2 0%, #E7B1F1 42%, #055DE2 100%);
  --g-nebula:    linear-gradient(180deg, #E7B1F1 0%, #055DE2 55%, #190402 100%);
  --g-duskburn:  linear-gradient(180deg, #E7B1F1 0%, #F4DCF9 30%, #FF230A 100%);
  --g-peachblow: linear-gradient(180deg, #E7B1F1 0%, #FEC062 55%, #FF7F0F 100%);

  /* Type — §4.1 BDO Grotesk; §4.3 sanctions Geist or Inter as the substitute */
  --font-sans: "BDO Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI",
               "Helvetica Neue", Arial, sans-serif;

  /* §4.3 scale: H1 700% · H2 450% · H3 350% · H4 250% · P1 100%, all at -3% tracking */
  --fs-display: clamp(2.75rem, 7vw, 7rem);
  --fs-h1:      clamp(2.5rem, 6.6vw, 7rem);
  --fs-h2:      clamp(2rem, 4.4vw, 4.5rem);
  --fs-h3:      clamp(1.625rem, 3.1vw, 3.5rem);
  --fs-h4:      clamp(1.3125rem, 2vw, 2.5rem);
  --fs-lead:    clamp(1.0625rem, 1.45vw, 1.5rem);
  --fs-body:    clamp(0.9375rem, 1vw, 1.0625rem);
  --fs-subtitle: clamp(1.125rem, 1.6vw, 1.5rem);   /* component headings, card titles */
  --fs-stat:     clamp(2.75rem, 6.4vw, 6rem);      /* the one big numeral */
  --fs-stat-mid: clamp(1.75rem, 2.7vw, 2.625rem);  /* numerals in the rail */
  --fs-stat-sm:  clamp(1.375rem, 2.1vw, 2rem);     /* numerals inside a ring */
  --fs-ui:      0.9375rem;                          /* controls and small body */
  --fs-ui-sm:   0.875rem;                           /* chips, dense controls */
  --fs-micro:   0.8125rem;   /* metadata; sentence case needs more than caps did */
  --fs-micro-sm: 0.75rem;

  /* Tracking is size-specific — never one value for every size */
  --tr-display: -0.035em;
  --tr-head:    -0.03em;
  --tr-body:    -0.011em;
  --tr-micro:   -0.005em;

  /* Grid — §8.2 (12 columns, 120px wide margin at 1920) */
  --margin: clamp(20px, 6.25vw, 120px);
  --maxw: 1680px;
  --gutter: clamp(16px, 1.6vw, 28px);

  /* Rhythm */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
  --sp-9: 96px; --sp-10: 144px;
  --section-y: clamp(80px, 10vw, 176px);

  /* Radius policy read off the deck: the primary CTA is a square-cornered
     Catalyst Red block — never a pill. Panels and cards take a small radius;
     brand artwork is never rounded. */
  --r-btn: 0px;
  --r-hairline: 2px;   /* focus ring, rail bar, slider rail */
  --r-art: 8px;        /* generated-artwork panels inside a layout */
  --r-card: 14px;
  --r-tile: 16px;
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  /* Set on the root, not on body, so the whole document is one family. */
  -webkit-tap-highlight-color: transparent;
  font-family: var(--font-sans);
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The scrollbar is part of the page, not the browser's. */
  scrollbar-color: var(--burnt-red) var(--coal);
  scrollbar-width: thin;
}
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--coal); }
::-webkit-scrollbar-thumb {
  background: var(--burnt-red); border: 3px solid var(--coal);
  border-radius: var(--r-hairline);
}
::-webkit-scrollbar-thumb:hover { background: var(--burnt-red); }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  line-height: 1.5;
  letter-spacing: var(--tr-body);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

img, svg { display: block; max-width: 100%; }
button { font: inherit; letter-spacing: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }

/* The ring is a token, because a red ring on a Catalyst Red ground is a 1:1
   invisible focus indicator. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--r-hairline);
}

::selection { background: var(--red); color: var(--hot-white); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.skip-link {
  position: fixed; top: 8px; left: 8px; z-index: 200;
  padding: 10px 16px; background: var(--red); color: var(--hot-white);
  border-radius: var(--r-btn); font-weight: 600;
  transform: translateY(-200%);
}
.skip-link:focus-visible { transform: none; }

/* --------------------------------------------------------------------------
   3. Type primitives
   -------------------------------------------------------------------------- */
h1, h2, h3, h4 {
  margin: 0;
  font-weight: 700;
  line-height: 1.0;
  letter-spacing: var(--tr-head);
  text-wrap: balance;
}
p { margin: 0; }

.display {
  font-size: var(--fs-display);
  line-height: 0.93;
  letter-spacing: var(--tr-display);
  font-weight: 700;
}
.h1 { font-size: var(--fs-h1); line-height: 0.96; }
.h2 { font-size: var(--fs-h2); line-height: 0.98; }
.h3 { font-size: var(--fs-h3); line-height: 1.02; }
.h4 { font-size: var(--fs-h4); line-height: 1.06; font-weight: 600; }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.34;
  letter-spacing: -0.015em;
  color: var(--ink-dim);
  text-wrap: pretty;
}
.body { max-width: 62ch; color: var(--ink-dim); text-wrap: pretty; }

/* Metadata. One family for the whole page, so this voice is built from the
   weight axis instead of a second typeface: DemiBold against Regular body
   copy, two sizes smaller, and dimmed. Small + heavy + quiet is how a printed
   caption or a table head reads, and it separates cleanly from body text
   without setting anything in capitals.
   §4.3 escalates weight for importance — a label is not less important than
   body copy, it is structural, so it goes up the axis rather than down. */
.label {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: var(--tr-micro);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
/* The subordinate tier: micro-copy that is prose rather than a name — a hint,
   a disclosure, a seed readout. Regular, so it never competes with a label
   that is actually labelling something. */
.label--quiet { font-weight: 400; }
.label--red { color: var(--red-ink); font-weight: 600; }

.hl { color: var(--red-ink); }

/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */
.shell {
  width: 100%;
  max-width: calc(var(--maxw) + var(--margin) * 2);
  margin-inline: auto;
  padding-inline: var(--margin);
}

.section { position: relative; padding-block: var(--section-y); }
.section--tight { padding-block: clamp(56px, 6vw, 104px); }
.section--flush { padding-block: 0; }

/* Light section — §3.6 pairs Hot White surfaces with near-black type */
/* On light grounds Catalyst Red is a fill, never type — the deck sets
   red-family copy in Burnt Red #682E27 instead, which also clears AA. */
.section--light {
  background: var(--hot-white);
  color: var(--coal);
  --red-ink: #682E27;
  --ink: #150402;
  --ink-dim: color-mix(in srgb, #150402 66%, transparent);
  --ink-faint: color-mix(in srgb, #150402 62%, transparent);
  --hairline: color-mix(in srgb, #150402 14%, transparent);
  --hairline-2: color-mix(in srgb, #150402 8%, transparent);
  --focus: #150402;
}
.section--charcoal { background: var(--charcoal); }
.section--red {
  background: var(--red);
  color: var(--coal);
  --ink: #1A0603;
  /* Hot White on Catalyst Red is only 3.7:1, so on a red ground the ink runs
     dark and the hierarchy is carried by size and weight, not by opacity. */
  --ink-dim: color-mix(in srgb, #1A0603 88%, transparent);
  --ink-faint: #1A0603;
  --hairline: color-mix(in srgb, #1A0603 22%, transparent);
  --focus: #1A0603;
}

.grid12 {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--gutter);
}
.grid12 > * { min-width: 0; }

/* --------------------------------------------------------------------------
   5. Buttons — §3.6
   -------------------------------------------------------------------------- */
.btn {
  --btn-bg: var(--red);
  --btn-fg: var(--hot-white);
  position: relative;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 13px 18px;
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: 1px solid transparent;
  border-radius: var(--r-btn);
  font-weight: 600;
  font-size: var(--fs-ui);
  letter-spacing: -0.01em;
  white-space: nowrap;
  cursor: pointer;
  transform: translateZ(0);
  transition: transform 110ms cubic-bezier(0.2, 0, 0, 1),
              background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}
.btn:hover { --btn-bg: var(--red-hot); }
/* Feedback lives on the press, and it is instant */
.btn:active { transform: scale(0.972); }

.btn--light { --btn-bg: var(--hot-white); --btn-fg: #150402; }
.btn--light:hover { --btn-bg: #FFFFFF; }

.btn--ghost {
  --btn-bg: transparent; --btn-fg: var(--ink);
  border-color: color-mix(in srgb, currentColor 34%, transparent);
}
.btn--ghost:hover { --btn-bg: color-mix(in srgb, currentColor 10%, transparent); }

.btn--dark { --btn-bg: #150402; --btn-fg: var(--hot-white); }

/* BDO Grotesk carries 4:1 ascent:descent metrics, so a centred line box sets
   the label's cap ink ~0.09em above the button's optical centre. */
.btn__label { transform: translateY(0.09em); }

.btn__arrow { transition: transform 240ms cubic-bezier(0.2, 0, 0, 1); }
.btn:hover .btn__arrow { transform: translate(2px, -2px); }

@media (prefers-reduced-motion: reduce) {
  .btn, .btn__arrow { transition: background-color 160ms ease, color 160ms ease; }
  .btn:active { transform: none; }
  .btn:hover .btn__arrow { transform: none; }
}

/* --------------------------------------------------------------------------
   6. Nav — translucent chrome, content scrolls under (§12 materials)
   -------------------------------------------------------------------------- */
.nav {
  position: fixed; inset: 0 0 auto; z-index: 100;
  display: flex; align-items: center; gap: var(--sp-5);
  /* Same cap as .shell, so the lockup lines up with the headline below it. */
  padding-block: 12px;
  padding-inline: max(var(--margin), calc((100% - var(--maxw)) / 2));
  background: color-mix(in srgb, var(--coal) 62%, transparent);
  backdrop-filter: blur(22px) saturate(160%);
  -webkit-backdrop-filter: blur(22px) saturate(160%);
  /* Material only. Animating the box of a fixed, backdrop-filtered bar
     re-blurs and re-lays-out the whole strip every frame. */
  transition: background-color 300ms ease;
}
/* Where the chrome overlaps content, fade the edge — never rule it. */
.nav::before {
  content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 22px;
  background: linear-gradient(180deg, color-mix(in srgb, var(--coal) 34%, transparent), transparent);
  opacity: 0; transition: opacity 300ms ease; pointer-events: none;
}
.nav[data-lifted="true"]::before { opacity: 1; }
.nav[data-over="light"]::before {
  background: linear-gradient(180deg, color-mix(in srgb, var(--hot-white) 48%, transparent), transparent);
}
/* Bright top edge = light catching the material */
.nav::after {
  content: ""; position: absolute; inset: 0 0 auto; height: 1px;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--hot-white) 16%, transparent) 30%, color-mix(in srgb, var(--hot-white) 16%, transparent) 70%, transparent);
  opacity: 0; transition: opacity 300ms ease;
}
.nav[data-lifted="true"] {
  background: color-mix(in srgb, var(--coal) 82%, transparent);
}
.nav[data-lifted="true"]::after { opacity: 1; }

/* Material weight follows what is underneath it, so the chrome never fights
   the section it floats over. */
.nav[data-over="light"] {
  background: color-mix(in srgb, var(--hot-white) 66%, transparent);
  --focus: #150402;
  --ink: #150402;
  --ink-dim: color-mix(in srgb, #150402 62%, transparent);
  color: #150402;
}
.nav[data-over="light"][data-lifted="true"] {
  background: color-mix(in srgb, var(--hot-white) 88%, transparent);
}
.nav[data-over="light"] .nav__logo { fill: #150402; }
.nav[data-over="light"] .nav__link:hover { background: color-mix(in srgb, #150402 7%, transparent); }
.nav[data-over="light"]::after {
  background: linear-gradient(90deg, transparent, color-mix(in srgb, #150402 10%, transparent) 30%, color-mix(in srgb, #150402 10%, transparent) 70%, transparent);
}

.nav[data-over="red"] {
  background: color-mix(in srgb, var(--coal) 44%, transparent);
}
.nav[data-over="red"][data-lifted="true"] {
  background: color-mix(in srgb, var(--coal) 72%, transparent);
}

/* The padding+negative-margin pair grows the tap target without moving the
   lockup off the shell's left edge. */
.nav__brand { display: flex; align-items: center; flex: 0 0 auto; padding: 8px; margin: -8px; }
.nav__logo { height: 27px; width: auto; fill: var(--hot-white); }
.nav__logo .mark { fill: var(--red); }

.nav__spacer { flex: 1 1 auto; }
.nav__links { display: flex; align-items: center; gap: 4px; }
.nav__link {
  padding: 9px 13px; border-radius: var(--r-btn);
  font-size: var(--fs-ui); font-weight: 500; color: var(--ink-dim);
  transition: color 160ms ease, background-color 160ms ease;
}
.nav__link:hover { color: var(--ink); background: color-mix(in srgb, var(--hot-white) 8%, transparent); }
.nav__cta { margin-left: 6px; padding: 10px 14px; }

.nav__burger {
  display: none; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  background: none; border: 1px solid var(--hairline); border-radius: var(--r-btn);
  cursor: pointer;
}
.nav__burger span {
  position: relative;
  display: block; width: 16px; height: 1.5px; background: currentColor;
  box-shadow: 0 5px 0 currentColor, 0 -5px 0 currentColor;
  transition: transform 240ms cubic-bezier(0.2, 0, 0, 1), box-shadow 180ms ease;
}
/* The second stroke of the ✕, carried by the span's own rotation. */
.nav__burger span::after {
  content: ""; position: absolute; inset: 0; background: currentColor;
  transform: rotate(90deg); opacity: 0; transition: opacity 120ms ease;
}
.nav[data-open="true"] .nav__burger span {
  transform: rotate(45deg);
  box-shadow: 0 0 0 currentColor, 0 0 0 currentColor;
}
.nav[data-open="true"] .nav__burger span::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .nav__burger span, .nav__burger span::after { transition: none; }
}

@media (max-width: 820px) {
  .nav__links { display: none; }
  .nav__burger { display: inline-flex; }
  .nav[data-open="true"] + .nav-sheet { pointer-events: auto; }
}
@media (max-width: 520px) {
  .nav { gap: 10px; }
  .nav__logo--lockup { height: 25px; }
  .nav__cta { margin-left: 0; padding: 9px 11px; font-size: var(--fs-ui-sm); }
}
.nav__logo--mark { display: none; height: 26px; }
@media (max-width: 400px) {
  .nav__logo--lockup { display: none; }
  .nav__logo--mark { display: block; }
}

/* Mobile sheet — enters and exits along the same path (§7) */
.nav-sheet {
  position: fixed; inset: 0; z-index: 99;
  display: grid; align-content: start; gap: 2px;
  padding: 84px max(var(--margin), calc((100% - var(--maxw)) / 2)) var(--sp-7);
  background: color-mix(in srgb, var(--coal) 92%, transparent);
  backdrop-filter: blur(28px);
  -webkit-backdrop-filter: blur(28px);
  opacity: 0; pointer-events: none;
  transform: translateY(-8px);
  transition: opacity 260ms ease, transform 320ms cubic-bezier(0.2, 0, 0, 1);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* html{overflow:hidden} does not stop iOS touch scroll behind the sheet;
     refusing the pan here does. Portrait content never scrolls, and the short
     landscape case below restores pan-y for its genuinely scrollable list. */
  touch-action: none;
}
@media (max-height: 520px) {
  .nav-sheet { padding-top: 68px; padding-bottom: var(--sp-5); touch-action: pan-y; }
  .nav-sheet a { padding: 10px 0; font-size: var(--fs-subtitle); }
}
.nav-sheet[data-open="true"] { opacity: 1; pointer-events: auto; transform: none; }
.nav-sheet a {
  padding: 14px 0; font-size: 1.5rem; font-weight: 600;
  letter-spacing: var(--tr-head); border-bottom: 1px solid var(--hairline-2);
}
@media (prefers-reduced-motion: reduce) {
  .nav-sheet { transition: opacity 200ms ease; transform: none !important; }
}

/* --------------------------------------------------------------------------
   7. Hero — the landing panel. The export's arc bands fill the ground, the
   copy sits quiet in the bottom-left the way the panel design sets it, and
   the paper chip is the one light surface on the red.
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  min-height: clamp(560px, 100svh, 1080px);
  display: flex;
  background: var(--red);
  color: var(--hot-white);
  isolation: isolate;
  overflow: clip;
}
.hero__art { position: absolute; inset: 0; z-index: -1; }
.hero__art svg { display: block; width: 100%; height: 100%; }

/* The drift. The bands rotate INSIDE the svg canvas about their own fitted
   centre — transform-box makes the pivot a viewBox point, so no script and
   no bleed: the canvas never moves, only the bands slide along their own
   arcs and their end-seams glide across the frame. Sectors, not full rings
   (union coverage measured at ±15deg headroom), so it breathes ±3deg.
   The glow is the gradients' share: a broad warm light crossing the bands.
   Both pause off screen and hold still under reduced motion. */
.hero__bands {
  transform-box: view-box;
  transform-origin: 2px 1733px;   /* the bands' fitted convergence centre */
}
.js .hero__bands {
  animation: hero-drift 20s cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
  animation-play-state: paused;
}
.hero__glow {
  position: absolute;
  width: 140vmax; aspect-ratio: 1; border-radius: 50%;
  left: -40vmax; top: -70vmax;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--yellow) 68%, transparent) 0%,
    color-mix(in srgb, var(--hazy-pink) 24%, transparent) 42%,
    transparent 64%);
  mix-blend-mode: overlay;
  animation: hero-glow 24s cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
  animation-play-state: paused;
}
.js .hero.is-live .hero__bands,
.js .hero.is-live .hero__glow { animation-play-state: running; }
@keyframes hero-drift { from { transform: rotate(-3deg); } to { transform: rotate(3deg); } }
@keyframes hero-glow {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(32vmax, 18vmax) scale(1.12); }
}
/* --------------------------------------------------------------------------
   7c. Hero motion variants. ?motion=gradient trades the rotation for colour:
   the bands hold absolutely still — geometry frozen, every line exactly where
   the export draws it — and three broad brand lights cross the field instead,
   on periods that share no common multiple (24s / 29s / 37s), so the colour
   never visibly repeats. Blended into the bands rather than laid over them,
   so what moves reads as the gradients themselves shifting. Compositor-only:
   transform on three elements, no repaint of the 33 paths.
   -------------------------------------------------------------------------- */
.hero__light { display: none; }
html[data-hero-motion="gradient"] .hero__bands { animation: none; }
html[data-hero-motion="gradient"] .hero__light {
  display: block;
  position: absolute; border-radius: 50%;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
  animation-play-state: paused;
}
html[data-hero-motion="gradient"] .hero__light--pink {
  width: 125vmax; aspect-ratio: 1; right: -48vmax; top: -34vmax;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--hazy-pink) 50%, transparent) 0%,
    color-mix(in srgb, var(--pink-dust) 18%, transparent) 38%, transparent 62%);
  mix-blend-mode: soft-light;
  animation-name: hero-light-pink; animation-duration: 29s;
}
html[data-hero-motion="gradient"] .hero__light--blue {
  width: 155vmax; aspect-ratio: 1; left: -30vmax; bottom: -88vmax;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--blue) 38%, transparent) 0%,
    color-mix(in srgb, var(--ice) 16%, transparent) 40%, transparent 60%);
  mix-blend-mode: soft-light;
  animation-name: hero-light-blue; animation-duration: 37s;
}
html[data-hero-motion="gradient"] .hero.is-live .hero__light { animation-play-state: running; }

@keyframes hero-light-pink {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(-28vmax, 16vmax) scale(1.16); }
}
@keyframes hero-light-blue {
  from { transform: translate(0, 0) scale(1.1); }
  to   { transform: translate(24vmax, -20vmax) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .hero__bands, .hero__glow, .hero__light { animation: none; }
}

/* --------------------------------------------------------------------------
   7d. Tempo — principle 02 at section scale, and the scroll seam that joins
   it to the hero. Both scrub transforms are written by main.js from scroll
   position; everything here is layout plus the pivots. The pivots are each
   field's own fitted convergence centre, so the morph slides every band
   along its own arc — the same rule the drift and the splash cut follow.
   -------------------------------------------------------------------------- */
.tempo {
  position: relative;
  min-height: clamp(560px, 100svh, 1080px);
  display: flex; align-items: center;
  background: var(--coal);
  isolation: isolate;
  overflow: clip;
}
/* The art owns the right side as its own box — the export is panoramic, and
   a full-bleed slice at ordinary aspects buries the copy in stripes. Anchored
   xMax, the box always shows the stripe field's right edge; the copy keeps
   the design's bare-coal left. On narrow screens the art becomes the bottom
   band instead, the copy above it. */
.tempo__art { position: absolute; inset: 0 0 0 auto; width: 58%; z-index: -1; }
.tempo__art svg { display: block; width: 100%; height: 100%; }
/* Narrow screens get their own export, and it carries its own composition —
   coal on the left, stripes on the right, full height — so the art goes full
   bleed and the copy sits on the dark side of the asset itself rather than in
   a box beside it. Sliced from the middle: the crop keeps the field's
   dark/stripe boundary about three-fifths across at phone aspects. */
@media (max-width: 719px) {
  .tempo { align-items: center; }
  .tempo__art { inset: 0; width: auto; }
}
.tempo__stripes {
  /* the pivot is each export's own fitted convergence centre, planted by
     main.js after injection because the two exports differ */
  transform-box: view-box;
}
.tempo__panel { width: 100%; }
/* Wide enough for the current two-line title (the measure was 40vw when the
   title was two short words per line) but still inside the export's dark
   edge, which the crop plants at 68% of the frame. */
.tempo__copy { display: grid; gap: 14px; max-width: min(830px, 56vw); }

/* The composed break exists for the wide layout's rhythm; on a phone each
   half is wider than the screen, so the break yields to natural wrapping. */
@media (max-width: 719px) { .br-wide { display: none; } }

.tempo__title {
  /* stepped down from the 4.25rem of the original two-word lines: the
     current title carries ~31 characters a line and has to hold TWO lines
     inside the copy measure */
  font-size: clamp(2.25rem, 3.2vw, 3.25rem);
  line-height: 1.06;
  letter-spacing: -0.015em;
  font-weight: 700;
}
.tempo__tag { color: var(--ink-dim); font-size: var(--fs-ui); max-width: 40ch; }

/* Narrow screens: the heading carries the section the way the tall export
   sets it — big, left, vertically centred on the asset's own dark side.
   Must follow the base type rules to win at equal specificity. */
@media (max-width: 719px) {
  /* The heading takes its natural width — the export's dark field widens
     toward the vertical middle, which is exactly where the copy sits, so the
     second line may reach past the stripe edge and still land on coal. Only
     the tag is held narrow, clear of the colour. */
  .tempo__copy { max-width: none; gap: 18px; }
  /* sized to clear the export's dark edge (planted at 68% by main.js). The
     old nowrap protected a short two-line break; the current title is longer
     than any phone is wide, so the break hides (.br-wide) and the title
     wraps naturally instead. */
  .tempo__title { font-size: min(9.4vw, 3rem); }
  .tempo__tag { max-width: 56vw; }
}

/* --------------------------------------------------------------------------
   7e. The intro stage — hero and tempo as one continuous asset.
   Base layout (no JS, reduced motion, narrow screens): the two views stack
   as plain sections, which is the previous, fully legible design. With
   html[data-morph] (set by main.js on capable viewports) the stage pins for
   a 260svh runway and scroll drives the cross-morph: arcs sweep out, ground
   crossfades red to coal, stripes rotate in — no section edge ever crosses
   the viewport, so there is no seam to see.
   -------------------------------------------------------------------------- */
.intro__ground { display: none; }

html[data-morph] .intro { height: 260svh; }
@media (max-width: 719px) { html[data-morph] .intro { height: 215svh; } }
html[data-morph] .intro__stage {
  position: sticky; top: 0;
  /* The DYNAMIC viewport: a 100svh stage leaves a coal band under the red
     ground the moment the mobile toolbar retracts; dvh tracks the glass so
     the artwork always reaches the bottom edge. main.js derives the scrub
     runway from this same box, so pin and progress stay in step. */
  height: 100svh;
  height: 100dvh;
  overflow: clip;
  background: var(--coal);           /* what the red ground fades away to */
  isolation: isolate;
}
html[data-morph] .intro__ground {
  display: block;
  position: absolute; inset: 0;
  background: var(--red);
}
html[data-morph] .hero,
html[data-morph] .tempo {
  position: absolute; inset: 0;
  min-height: 0;
  background: transparent;
}
/* stripes ride above the arcs during the cross; the layer itself never eats
   the hero chip's clicks */
html[data-morph] .tempo { z-index: 2; pointer-events: none; }
html[data-morph] .tempo__art { inset: 0; width: auto; opacity: 0; }
html[data-morph] .tempo__copy { opacity: 0; }

/* the hero's scrub twin lives inside the drift group, so scroll and the
   ambient breathe compose about the same centre */
.hero__scrub {
  transform-box: view-box;
  transform-origin: 2px 1733px;
}

/* The copy enters on the same beat: held by the gate, released with the
   bloom. */
.hero__foot {
  transition: opacity 620ms cubic-bezier(0.2, 0, 0, 1) 340ms,
              transform 760ms cubic-bezier(0.16, 1, 0.3, 1) 340ms;
}
html[data-splash] .hero__foot { opacity: 0; transform: translate3d(0, 18px, 0); }

.hero__panel {
  position: relative;
  display: flex; flex-direction: column; justify-content: flex-end;
  width: 100%;
  padding-top: clamp(96px, 12vh, 150px);
  padding-bottom: clamp(30px, 5vh, 60px);
}
.hero__foot { display: flex; align-items: flex-end; }
.hero__copy { display: grid; gap: clamp(14px, 1.2vw, 22px); max-width: none; }
.hero__title {
  /* Display scale, per the reference, which sets the headline near 4.2vw with
     its longest line about 40% of the width. The ceiling has to clear a 27-inch
     display or the cap re-creates the very problem it guards against: at 2560
     a 5rem cap fell back to 3.1vw and 30% of the width. */
  font-size: clamp(2.75rem, 4.2vw, 6.75rem);
  line-height: 1.02;
  letter-spacing: -0.022em;
  font-weight: 700;
  text-wrap: balance;
}
.hero__tag {
  color: color-mix(in srgb, var(--hot-white) 78%, transparent);
  font-size: clamp(0.9375rem, 1.15vw, 1.25rem);
  line-height: 1.45;
  max-width: 46ch;
}
.btn--paper { background: var(--hot-white); color: #150402; }

/* --------------------------------------------------------------------------
   7b. Splash — the progress wheel. A conic mask sweeps the tick ring from
   0 to 100 in step with real load progress, then the finished circle expands
   into the hero: transform and opacity only, so both actors composite.
   -------------------------------------------------------------------------- */
.splash { display: none; }
.js .splash {
  position: fixed; inset: 0; z-index: 300;
  display: grid; justify-items: center; align-content: center;
  gap: clamp(18px, 4vh, 32px);
}
.splash__bg { position: absolute; inset: 0; z-index: -1; background: var(--charcoal); }
.splash__wheel {
  width: clamp(230px, 40vmin, 420px);
  --p: 6%;
  -webkit-mask-image: conic-gradient(#000 var(--p), transparent 0);
  mask-image: conic-gradient(#000 var(--p), transparent 0);
}
.splash__wheel svg { display: block; width: 100%; height: auto; }
.splash__pct {
  font-weight: 600;
  font-size: clamp(1.25rem, 3vh, 1.75rem);
  color: var(--hot-white);
  font-variant-numeric: tabular-nums;
  transition: opacity 220ms ease;
}
/* the hand-off: the ground fades first, unveiling the hero already in place;
   the wheel outlives it, carried by the match-cut animation in main.js onto
   the arcs' own centre, and dissolves as its rings land on the bands. The
   wheel's opacity transition here is the reduced-motion and no-WAAPI exit —
   the animation overrides it whenever the full cut runs. */
.splash--go { pointer-events: none; }
.splash--go .splash__bg { opacity: 0;
  transition: opacity 460ms cubic-bezier(0.23, 1, 0.32, 1) 100ms; }
.splash--go .splash__wheel { opacity: 0;
  transition: opacity 620ms cubic-bezier(0.23, 1, 0.32, 1) 240ms; }
.splash--go .splash__pct { opacity: 0; }
html[data-splash] { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  html[data-splash] .hero__foot { transform: none; }
}
/* --------------------------------------------------------------------------
   8. Mission statement — §1.1. The statement is the whole section: no panel,
   no artwork, no metadata. It is sized to the viewport rather than to a
   measure, so the sentence fills the screen at any size, and its highlight is
   scroll-driven — words start at a dim ink and take Catalyst Red as the
   reader arrives. The dim colour is the red itself, mixed toward the ground,
   so nothing new enters the palette.
   -------------------------------------------------------------------------- */
.mission { background: var(--coal); }
.mission__stage {
  min-height: 100svh;
  display: grid; place-items: center;
  padding: clamp(80px, 12vh, 160px) var(--gutter);
}
/* Pinned while the sweep runs: main.js sets data-mission when it can drive
   this (script present, motion allowed), and then the section is a runway —
   the page holds still and the scroll goes into the highlight instead. The
   base state below is an ordinary screen-tall section, which is what a
   no-script or reduced-motion reader gets. */
html[data-mission] .mission { height: 200svh; }
html[data-mission] .mission__stage {
  position: sticky; top: 0;
  height: 100svh; min-height: 0;
}
.mission__statement {
  --mission-dim: color-mix(in srgb, var(--red) 26%, var(--coal));
  max-width: 22ch;
  text-align: center;
  text-wrap: balance;
  color: var(--red);
  font-size: clamp(2rem, 5.2vw, 6rem);
  line-height: 1.04;
  letter-spacing: -0.02em;
  font-weight: 500;
}
/* Each word is its own span so the sweep can follow reading order. Its lit
   fraction is a plain number on --t and the browser resolves the mix, so the
   two paints stay defined here and JS never parses a colour — computed
   color-mix comes back as color(srgb 0..1), which is easy to misread as
   0..255 and silently produces black. Spans only exist when the script has
   run, so the unlit default cannot strand a no-JS reader. */
.mission__word {
  --t: 0;
  color: color-mix(in srgb, var(--red) calc(var(--t) * 100%), var(--mission-dim));
  transition: color 400ms ease;
}
.js .mission__statement[data-lit] .mission__word { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .mission__word { color: var(--red); }
}

/* --------------------------------------------------------------------------
   9. Apps rail
   -------------------------------------------------------------------------- */
/* The section's own header rather than the shared .sec-head: centred, and
   with no rule under the title — the rail's card edges below already draw
   that line. The measure stays at 54ch and centres as a block, so the
   paragraph keeps its reading width instead of stretching to the shell. */
.portfolio__head {
  display: grid; justify-items: center;
  text-align: center;
  gap: var(--sp-5);
  margin-bottom: clamp(32px, 4vw, 72px);
}
.portfolio__head .lead { max-width: 54ch; }

.rail {
  position: relative;
  margin-inline: calc(var(--margin) * -1);
  padding-inline: var(--margin);
  overflow: clip;
}
/* pinch-zoom stays allowed: the drag only ever claims single-pointer pans */
.rail__viewport { overflow: visible; touch-action: pan-y pinch-zoom; }
.rail__track {
  display: flex; gap: var(--gutter);
  will-change: transform;
  cursor: grab;
}
.rail__track:active { cursor: grabbing; }

.app-card {
  /* The 300px floor keeps every value proposition to two lines, so the row
     of cards shares one text rhythm (see .app-card__news). */
  flex: 0 0 clamp(300px, 30vw, 420px);
  display: grid;
  grid-template-rows: auto 1fr;
  border-radius: var(--r-card);
  overflow: clip;
  background: var(--charcoal);
  border: 1px solid var(--hairline-2);
  transform: translateZ(0);
  will-change: transform;
}
.app-card__art {
  position: relative;
  /* The poster exports' own proportions (921x1372) — the art is shown whole,
     never cropped, so the card takes the artwork's ratio rather than a
     square's. */
  aspect-ratio: 921 / 1372;
  overflow: clip;
  background: var(--coal);
}
/* The poster is decoration the pointer should never catch: without this a
   mouse-drag across the art starts the browser's own image drag — a ghost of
   the file following the cursor — and the rail's own drag never begins. The
   markup also carries draggable="false"; this is the belt to that brace, and
   pointer-events keeps the gesture on the track underneath. */
.app-card__bg {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none; -webkit-user-select: none;
}
.app-card__badge {
  position: absolute; z-index: 2; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 11px;
  padding: 10px 16px 10px 12px;
  border-radius: var(--r-card);
  /* Deliberately not a backdrop-filter: three blurred surfaces inside a track
     that translates every drag frame is the one place the blur would cost
     something, and a near-solid chip carries the label better anyway. */
  background: color-mix(in srgb, var(--coal) 88%, transparent);
  border: 1px solid color-mix(in srgb, var(--hot-white) 22%, transparent);
  color: var(--hot-white);
  font-weight: 600;
  font-size: var(--fs-subtitle);
  letter-spacing: -0.02em;
  white-space: nowrap;
  /* The label sits exactly where the rail is grabbed, so dragging must not
     highlight it, and a long-press grab must not open iOS's link preview.
     The meta copy below stays selectable — that is real text someone may
     want to copy. */
  user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* The store icon, straight from the artwork: the radius matches the 8px chip
   the old glyph tiles used, close enough to the store's own ~24% corner. */
.app-card__icon {
  width: 30px; height: 30px; border-radius: 8px; flex: 0 0 auto;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none; -webkit-user-select: none;
}

/* A quiet orange wash between the poster and the badge. Hovering (or
   keyboard-focusing) the badge lifts it, so the artwork flares to full heat
   under the pointer — the card's "lit" state. The scrim sits after the badge
   in the DOM (for the ~ combinator) but under it in the stack. */
.app-card__scrim {
  position: absolute; inset: 0; z-index: 1;
  background: var(--orange);
  opacity: 0.22;
  pointer-events: none;
  transition: opacity 260ms ease;
}
@media (hover: hover) and (pointer: fine) {
  .app-card__badge:hover ~ .app-card__scrim { opacity: 0; }
}
.app-card__badge:focus-visible ~ .app-card__scrim { opacity: 0; }

/* One text rhythm across the row: the tagline leads at the system's
   card-title step with its closing beat in Catalyst Red, and the value
   proposition always occupies a two-line block (a one-liner keeps the empty
   second line) so five cards share one baseline. */
.app-card__meta {
  display: grid; gap: 10px;
  grid-template-rows: auto 1fr;
  justify-items: start;
  padding: 20px 20px 20px;
}
.app-card__tag {
  font-size: var(--fs-subtitle);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
/* One step down at phone width: the longest tagline ("Free up your phone in
   seconds") must hold one line in a 300px card, or the row loses the shared
   text rhythm the cards are built on. */
@media (max-width: 1099px) { .app-card__tag { font-size: var(--fs-ui); } }
/* Phones: one card to a screen. At a 300px card a ~36px sliver of the next
   one showed past the screen edge — enough to slice its heading and body copy
   mid-word, which reads as broken text rather than as a peek. The dots and
   arrows directly below already say there is more to swipe. */
@media (max-width: 480px) {
  .app-card { flex-basis: 100%; }
}
/* Emphasis, literally: the tagline's payoff is <em>, coloured not slanted.
   Red-ink on charcoal passes AA for large type, which this size and weight is. */
.app-card__tag em { font-style: normal; color: var(--red-ink); }
.app-card__news { color: var(--ink); font-size: var(--fs-ui); min-height: 3em; }

.rail__controls { display: flex; align-items: center; gap: 10px; margin-top: var(--sp-6); }
.rail__dots { display: flex; gap: 4px; margin-left: auto; }
/* The bar is 3px; the target is not. */
.rail__dot {
  width: 40px; height: 40px; border: 0; padding: 0; background: none;
  display: grid; place-items: center; cursor: pointer;
}
.rail__dot::before {
  content: ""; display: block;
  width: 34px; height: 3px; border-radius: var(--r-hairline);
  background: color-mix(in srgb, var(--ink) 38%, transparent);
  transform: scaleX(0.62);
  transition: background-color 220ms ease,
              transform 320ms cubic-bezier(0.2, 0, 0, 1);
}
.rail__dot:hover::before { background: color-mix(in srgb, var(--ink) 62%, transparent); }
.rail__dot[aria-current="true"]::before { background: var(--red); transform: scaleX(1); }

.icon-btn {
  width: 42px; height: 42px; display: grid; place-items: center;
  background: none; border: 1px solid var(--hairline); border-radius: var(--r-btn);
  cursor: pointer; color: var(--ink-dim);
  transition: color 160ms ease, border-color 160ms ease, transform 110ms cubic-bezier(0.2,0,0,1);
}
.icon-btn:hover:not(:disabled) { color: var(--ink); border-color: color-mix(in srgb, currentColor 40%, transparent); }
.icon-btn:active:not(:disabled) { transform: scale(0.94); }
.icon-btn:disabled { opacity: 0.32; cursor: default; }

/* --------------------------------------------------------------------------
   10b. Signal poster — the data manifesto
   The §7.3 poster at section scale: the export's ray field revolving slowly
   around live type. Two stacked fields turn at different paces and breathe a
   few percent — transform-only, so the loop composites off the main thread —
   and everything holds still under reduced motion or off screen.
   -------------------------------------------------------------------------- */
/* The poster arrives like a page. The portfolio holds still at its last
   screenful (top measured by main.js, since sticky cannot know its own
   height) while this section slides up over it; the hairline and the thrown
   shadow are what make the incoming edge read as a sheet rather than a seam.
   The stage is exactly one viewport, so once the page has landed the poster
   IS the screen — its crop is the full-bleed edge of a page, not a slice
   against a neighbour. */
.section-anchor { height: 0; }
#portfolio-sec {
  position: sticky;
  /* Pinned at its last screenful: top = viewport minus own height (min()
     clamps a section shorter than a screen to 0). The viewport term is
     100lvh — the LARGE viewport, which holds still while a mobile toolbar
     shows and hides — so the frozen sheet cannot jump when innerHeight
     does; and it matches the poster page's own lvh height, so the pinned
     travel and the cover distance agree on every platform. JS supplies only
     --under-h, the section's own measured height. */
  top: min(0px, calc(100vh - var(--under-h, 200vh)));
  top: min(0px, calc(100lvh - var(--under-h, 200lvh)));
  z-index: 0;
}
/* A covered control must never hold an invisible focus ring: while keyboard
   focus is inside the pinned sheet, it surfaces above the poster.
   :focus-visible inside :has() keeps mouse clicks from flipping the deck. */
#portfolio-sec:has(:focus-visible) { z-index: 2; }
.signal {
  position: relative; z-index: 1;
  background: var(--coal); overflow: clip;
  border-top: 1px solid var(--hairline);
  box-shadow: 0 -30px 70px color-mix(in srgb, #000 55%, transparent);
}
/* The deck order continues past the poster: the footer needs to paint above
   the pinned sheet too, or the portfolio would show through behind it. */
.footer { position: relative; z-index: 1; }
.signal__stage {
  position: relative;
  /* The LARGE viewport on mobile (toolbar retracted — the state the page is
     actually in while scrolling down into this section), so the sheet's
     height equals the sticky travel above and the landing really fills the
     screen. With the toolbar re-shown the page is a toolbar's-worth taller
     than the glass, the standard 100vh trade. */
  height: 100vh;
  height: 100lvh;
  min-height: 640px;
  display: grid; place-items: center;
}
.signal__rays { position: absolute; inset: 0; }
.signal__layer { position: absolute; top: 50%; left: 50%; /* sized by main.js */ }
.signal__layer svg { display: block; width: 100%; height: 100%; }

/* The revolutions. Linear, because ambient motion has no destination; paused
   by default and released by .is-live so an off-screen poster costs nothing.
   The halo runs faster than the field, so the dash chains slowly shear the
   way the printed sequence staggers them — that shear IS the cycling. */
.js .signal__layer {
  animation: signal-turn 130s linear infinite;
  animation-play-state: paused;
}
.js .signal__layer--halo { animation-duration: 88s; }
.js .signal__layer svg {
  animation: signal-breath 17s cubic-bezier(0.77, 0, 0.175, 1) infinite alternate;
  animation-play-state: paused;
}
.js .signal__layer--halo svg { animation-duration: 13s; animation-delay: -6.5s; }
.js .signal.is-live .signal__layer,
.js .signal.is-live .signal__layer svg { animation-play-state: running; }

@keyframes signal-turn { to { transform: rotate(1turn); } }
@keyframes signal-breath { from { transform: scale(1); } to { transform: scale(1.035); } }

.signal__copy {
  position: relative;
  display: grid; justify-items: center; gap: clamp(26px, 4vw, 46px);
  text-align: center;
  padding: clamp(96px, 15vh, 180px) var(--sp-5);
}
.signal__h {
  /* On landscape the ray field is scaled by the viewport's height, so the
     type follows the height there too — the vh term reproduces the poster's
     line-to-frame ratio — while portrait keeps the width-driven size. */
  font-size: clamp(3.25rem, min(8.6vw, 7.6vh), 7.75rem);
  line-height: 0.96;
  letter-spacing: var(--tr-display);
  font-weight: 700;
}


@media (prefers-reduced-motion: reduce) {
  .signal__layer, .signal__layer svg { animation: none; }
}

/* --------------------------------------------------------------------------
   11. Numbers — the portfolio in seven figures, §7.2/§7.3
   A full-bleed marquee between the statement and the portfolio: each item is
   one infographic export beside its numeral and caption, the whole band
   drifting right to left on a seamless two-copy track. The drift is CSS and
   transform-only (compositing off the main thread, like the signal poster);
   it runs only while the band is on screen (.is-live) and rests under a
   pointer so the figures can be read. Items are JS-built (main.js §8).
   -------------------------------------------------------------------------- */
.numbers {
  overflow: clip;
  /* A strip, not a chapter: tighter block rhythm than a full section. */
  padding-block: clamp(48px, 6vw, 104px);
}
.numbers__viewport { overflow: clip; }
.numbers__track { display: flex; width: max-content; will-change: transform; }

.js .numbers__track {
  animation: numbers-drift var(--marquee-dur, 48s) linear infinite;
  animation-play-state: paused;
}
.js .numbers.is-live .numbers__track { animation-play-state: running; }
/* Hover-capable pointers only: on touch, iOS/Android latch :hover on tap,
   which froze the drift until the next tap somewhere else. */
@media (hover: hover) and (pointer: fine) {
  .js .numbers.is-live .numbers__viewport:hover .numbers__track { animation-play-state: paused; }
}
/* Touch has no hover, so a held finger is its pause: main.js sets is-held
   from pointerdown/up, which never latches the way :hover does. */
.js .numbers.is-live .numbers__viewport.is-held .numbers__track { animation-play-state: paused; }

@keyframes numbers-drift { to { transform: translate3d(-50%, 0, 0); } }

/* Trailing padding equals the item gap, so the seam between the two copies
   reads as one more ordinary gap and the -50% wrap is invisible. */
.numbers__group {
  display: flex; align-items: center;
  gap: clamp(52px, 6.5vw, 116px);
  padding-right: clamp(52px, 6.5vw, 116px);
  padding-left: 0;
}

.number { flex: none; display: flex; align-items: center; gap: clamp(14px, 1.7vw, 26px); }
.number__fig { width: clamp(72px, 7.4vw, 112px); aspect-ratio: 1 / 1; flex: none; }
.number__fig svg { width: 100%; height: 100%; }
.number__txt { display: grid; gap: 5px; }
.number__v {
  font-size: var(--fs-stat-mid);
  font-weight: 400; line-height: 0.9; letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.number__v .plus { color: var(--red); }
/* The rating's star: drawn, not typed — BDO Grotesk has no star glyph, and a
   fallback-font character would break the face rule. Red, like the plus. */
.number__star {
  display: inline-block;
  width: 0.62em; height: 0.62em;
  fill: var(--red);
  vertical-align: baseline;
}
.number__l { color: var(--ink-dim); white-space: nowrap; }

/* Reduced motion: no drift, no duplicate — the one group stands still and
   overflows into an ordinary horizontal scroll. */
@media (prefers-reduced-motion: reduce) {
  .js .numbers__track { animation: none; }
  .numbers__viewport { overflow-x: auto; }
  .numbers__group[aria-hidden="true"] { display: none; }
}

/* --------------------------------------------------------------------------
   12. Careers (red flood)
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   13. Footer — the sign-off. The lockup at page scale in solid Catalyst Red on
   a ground that deepens from Coal into a crimson mixed from the page's own
   red, with four quiet link stacks under the baseline. Every colour here is a
   mix of two existing paints; the footer earns its heat without a new hex.
   -------------------------------------------------------------------------- */
.footer {
  /* Local ink family: warm coral tints of the brand pair, made for the deep
     ground below rather than borrowed from the coal sections above. */
  --f-ground: color-mix(in srgb, var(--red) 26%, var(--coal));
  --f-ink:    color-mix(in srgb, var(--red) 58%, var(--hot-white));
  --f-faint:  color-mix(in srgb, var(--red) 42%, color-mix(in srgb, var(--hot-white) 62%, transparent));
  --f-line:   color-mix(in srgb, var(--red) 34%, transparent);
  /* No seam above: the footer opens as bare Coal so it reads as the same
     surface as the momentum section, and only then deepens toward crimson. */
  background: linear-gradient(180deg, var(--coal) 0%, var(--coal) 18%,
    color-mix(in srgb, var(--red) 12%, var(--coal)) 52%, var(--f-ground) 100%);
  overflow: clip;
}

/* Footer catalyst — the cool rim of the object, where the extended palette
   finally gets its share. Generated, not a gradient. The page's last band,
   full-bleed below the colophon. */
.footer__strip {
  height: clamp(56px, 7vw, 96px); overflow: clip;
  margin-top: clamp(34px, 5vw, 64px);
  background: linear-gradient(90deg, #190402, #682E27 16%, #FF230A 40%,
    #FF7F0F 58%, #FEC062 72%, #E7B1F1 88%, #055DE2);
}
.footer__strip svg { width: 100%; height: 100%; }

/* The giant type. Full shell width, solid Catalyst Red — display INK, so it
   arrives by the same clip-and-rise every headline uses, never a fade. The
   descender margin below it is trimmed by the rule sitting close under the
   baseline, the way the reference crops its logo against the hairline. */
.footer__wordmark { margin-top: clamp(24px, 4vw, 56px); }
.footer__wordmark svg {
  display: block; width: 100%; height: auto;
  fill: var(--red);
}

.footer__cols {
  display: grid; gap: var(--sp-6) var(--gutter);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: clamp(20px, 3vw, 38px);
  padding-top: clamp(20px, 3vw, 34px);
  border-top: 1px solid var(--f-line);
}
@media (min-width: 860px) {
  .footer__cols { grid-template-columns: 1.6fr 1.6fr minmax(0, 1fr) minmax(0, 1fr); }
}
.footer__col { display: grid; align-content: start; gap: 9px; }
/* Sentence case, separated by weight — never tracked capitals (§4). */
.footer__colTitle { color: var(--hot-white); font-weight: 600; font-size: var(--fs-ui); margin-bottom: 2px; }
.footer__col--powered { justify-items: start; }
.footer__powered-mark { width: clamp(150px, 15vw, 210px); height: auto; }
@media (min-width: 860px) { .footer__col--powered { justify-items: end; } }
.footer__col a, .footer__col span { color: var(--f-ink); font-size: var(--fs-ui); }
.footer__col a {
  justify-self: start;
  /* the visible line is ~22px; the touch target should not be */
  padding-block: 8px; margin-block: -8px;
  text-decoration-color: transparent;
  text-underline-offset: 3px; text-decoration-thickness: 2px;
  transition: color 160ms ease, text-decoration-color 160ms ease;
}
.footer__col a:hover { color: var(--hot-white); text-decoration: underline; text-decoration-color: currentColor; }

.footer__bottom {
  display: flex; flex-wrap: wrap; gap: 12px 28px; align-items: center;
  padding-top: clamp(26px, 4vw, 46px);
}
.footer__bottom .label { color: var(--f-faint); }
.footer__seed {
  background: none; border: 0; cursor: pointer;
  margin-left: auto; text-align: right;
  /* the visible line is ~14px; the touch target should not be */
  padding: 12px 0; margin-block: -12px;
}
.footer__seed:hover { color: var(--f-ink); }

/* Narrow screens — below the four-up column grid. The Codeway mark is not a
   nav column: in the two-up grid it landed beside "Socials" and read as a
   fourth, unlabelled list. It takes the full row instead, and the sign-off
   stacks under it, so the mark, the copyright and the seed readout share one
   centred axis rather than three competing alignments. */
@media (max-width: 859.98px) {
  .footer__col--powered {
    grid-column: 1 / -1;
    justify-items: center;
    padding-top: clamp(12px, 4vw, 24px);
  }
  .footer__bottom {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 14px;
  }
  .footer__seed { margin-left: 0; text-align: center; }
}

/* The warm tints are mood, not information — with more contrast requested,
   the footer's text returns to the page's ink. */
@media (prefers-contrast: more) {
  .footer { --f-ink: var(--hot-white); --f-faint: var(--hot-white);
    --f-line: color-mix(in srgb, var(--hot-white) 40%, transparent); }
}

/* --------------------------------------------------------------------------
   14. Reveal on scroll — opacity + a short lift, cross-fade under reduced motion
   -------------------------------------------------------------------------- */
.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition: opacity 620ms cubic-bezier(0.2, 0, 0, 1),
              transform 760ms cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { transform: none; transition: opacity 300ms ease; }
  .rail__dot::before { transition: background-color 200ms ease; }
}

/* --------------------------------------------------------------------------
   15. Accessibility preference overrides — §14
   -------------------------------------------------------------------------- */
@media (prefers-reduced-transparency: reduce) {
  .nav, .nav[data-lifted="true"] { background: var(--coal); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .nav-sheet { background: var(--coal); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

@media (prefers-contrast: more) {
  :root {
    --ink-dim: var(--hot-white);
    --ink-faint: var(--hot-white);
    --hairline: color-mix(in srgb, var(--hot-white) 40%, transparent);
    --hairline-2: color-mix(in srgb, var(--hot-white) 24%, transparent);
  }
  .section--light { --ink-dim: #150402; --ink-faint: #150402;
    --hairline: color-mix(in srgb, #150402 40%, transparent);
    --hairline-2: color-mix(in srgb, #150402 24%, transparent); }
  .section--red { --ink-dim: #1A0603; --ink-faint: #1A0603;
    --hairline: color-mix(in srgb, #1A0603 46%, transparent); }
  .nav { background: var(--coal); }
  .nav[data-over="light"] { background: var(--hot-white); }
  .app-card { border-color: color-mix(in srgb, var(--hot-white) 40%, transparent); }
  .app-card__badge { background: var(--coal); }
  .rail__dot::before { background: color-mix(in srgb, var(--ink) 70%, transparent); }
}

/* --------------------------------------------------------------------------
   17. Material fallbacks — a translucent surface must never be a transparent
   one where the effect is unavailable or unwanted.
   -------------------------------------------------------------------------- */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav { background: color-mix(in srgb, var(--coal) 96%, transparent); }
  .nav[data-over="light"] { background: color-mix(in srgb, var(--hot-white) 96%, transparent); }
  .nav-sheet { background: var(--coal); }
}

/* --------------------------------------------------------------------------
   18. Ink clips, chrome fades.
   Display type is INK: it is revealed by a clip window and a translate, never
   by an opacity fade — a hard-edged brand does not dissolve its headlines.
   Mono labels, body copy and controls are CHROME: they cross-fade.
   -------------------------------------------------------------------------- */
/* A hair of clip margin so descenders survive the resting state, far less
   than the distance the ink travels, so nothing peeks in before its cue. */
.js [data-ink] { overflow: clip; overflow-clip-margin: 0.14em; }
[data-ink] > .ink { display: block; }
.js [data-ink] > .ink {
  transform: translate3d(0, 125%, 0);
  transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--reveal-delay, 0ms);
}
.js [data-ink].is-in > .ink { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js [data-ink] { overflow: visible; }
  .js [data-ink] > .ink { transform: none; transition: opacity 300ms ease; }
}

/* --------------------------------------------------------------------------
   19. Font-loading guard — the whole look rests on tight leading, so the
   fallback stack gets looser metrics until the brand substitute has loaded.
   -------------------------------------------------------------------------- */
:root:not(.fonts-ready) .display,
:root:not(.fonts-ready) .h1,
:root:not(.fonts-ready) .h2,
:root:not(.fonts-ready) .h3,
:root:not(.fonts-ready) .h4 {
  line-height: 1.08;
  letter-spacing: -0.018em;
}

/* --------------------------------------------------------------------------
   20. Scroll progress — a hairline in Flare, replacing a numbered rail that
   would have to be renumbered every time the page gains a section.
   -------------------------------------------------------------------------- */
.progress {
  position: fixed; z-index: 101; inset: 0 0 auto;
  height: 3px; pointer-events: none;
}
.progress__bar {
  height: 100%; width: 100%;
  transform: scaleX(0); transform-origin: 0 50%;
  background: linear-gradient(90deg, #FF7F0F, #FF230A);
  will-change: transform;
}

/* --------------------------------------------------------------------------
   21. Two-tone secondary CTA — §3.6. A red brand needs a defined non-red
   button: a Burnt Red bar with the arrow broken out into a red badge.
   -------------------------------------------------------------------------- */
.btn--duo {
  --btn-bg: var(--burnt-red);
  --btn-fg: var(--hot-white);
  padding-right: 0;
  gap: 14px;
  overflow: clip;
}
.btn--duo:hover { --btn-bg: #7A362E; }
.btn--duo .btn__badge {
  align-self: stretch;
  display: grid; place-items: center;
  width: 42px;
  background: var(--red);
  color: var(--hot-white);
  transition: background-color 160ms ease;
}
.btn--duo:hover .btn__badge { background: var(--red-hot); }
.section--light .btn--duo { --btn-fg: var(--hot-white); }
