/* ==========================================================================
   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;

  /* Hyjc Night — cinematic cobalt. Token names (--red, --orange) stay so
     the motion/CSS system does not need a rewrite; the paints are blue. */
  --red:        #3E6BFF;   /* brand cobalt — CTA, highlight, giant type */
  --red-hot:    #6378EF;   /* original Hyjc periwinkle, hover only */
  --red-ink:    #3E6BFF;   /* brand used as TYPE; overridden on light grounds */
  --focus:      #3E6BFF;
  --orange:     #3EC8FF;   /* ice cyan analog */
  --yellow:     #9ECFFF;   /* pale sky */
  --hot-white:  #F6F8FC;

  /* Night navy grounds + steel midtones */
  --coal:       #050A16;
  --charcoal:   #0C1832;
  --burnt-red:  #1A3A78;
  --terracotta: #4A72B5;
  --hazy-pink:  #B8C4FF;
  --pink-dust:  #E4E8FF;
  --blue:       #1D4ED8;
  --ice:        #D6E8FF;
  --grey:       #6B7A8F;
  --clay:       #8A9BB5;
  --dry-clay:   #B8C5D6;
  --blush:      #DCE4F0;

  /* Semantic surfaces */
  --bg:          var(--coal);
  --surface:     var(--charcoal);
  --surface-2:   #081224;
  --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 — cobalt as the root */
  --g-sunset: linear-gradient(180deg, #9ECFFF 0%, #3E6BFF 100%);
  --g-flare:  linear-gradient(180deg, #3EC8FF 0%, #3E6BFF 100%);
  --g-scorch: linear-gradient(180deg, #1A3A78 0%, #3E6BFF 100%);
  --g-ember:  linear-gradient(180deg, #050A16 0%, #3E6BFF 100%);

  /* Extended gradients — §3.4 (use sparingly, for maximum impact) */
  --g-inferno:   linear-gradient(180deg, #F6F8FC 0%, #9ECFFF 34%, #3EC8FF 62%, #3E6BFF 100%);
  --g-plasma:    linear-gradient(180deg, #D6E8FF 0%, #B8C4FF 42%, #1D4ED8 100%);
  --g-nebula:    linear-gradient(180deg, #B8C4FF 0%, #1D4ED8 55%, #050A16 100%);
  --g-duskburn:  linear-gradient(180deg, #B8C4FF 0%, #E4E8FF 30%, #3E6BFF 100%);
  --g-peachblow: linear-gradient(180deg, #B8C4FF 0%, #9ECFFF 55%, #3EC8FF 100%);

  /* Type — §4.1 BDO Grotesk; §4.3 sanctions Geist or Inter as the substitute */
  --font-sans: "BDO Grotesk", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC",
               "Microsoft YaHei", -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;

  /* CJK needs open tracking; Latin -3% would crush 汉字 */
  --tr-display: 0.04em;
  --tr-head:    0.02em;
  --tr-body:    0.02em;
  --tr-micro:   0.01em;

  /* 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 brand-colored ring on a cobalt ground
   would otherwise disappear. */
: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: var(--tr-body);
  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 — paper ground with near-navy type */
/* On light grounds cobalt is a fill, never type — ink uses deep navy #1A3A78. */
.section--light {
  background: var(--hot-white);
  color: var(--coal);
  --red-ink: #1A3A78;
  --ink: #07101C;
  --ink-dim: color-mix(in srgb, #07101C 66%, transparent);
  --ink-faint: color-mix(in srgb, #07101C 62%, transparent);
  --hairline: color-mix(in srgb, #07101C 14%, transparent);
  --hairline-2: color-mix(in srgb, #07101C 8%, transparent);
  --focus: #07101C;
}
.section--charcoal { background: var(--charcoal); }
.section--red {
  background: var(--red);
  color: var(--coal);
  --ink: #07122A;
  /* Paper on cobalt is shy of AA at small sizes, so ink on a brand ground
     stays navy and hierarchy is carried by size and weight, not opacity. */
  --ink-dim: color-mix(in srgb, #07122A 88%, transparent);
  --ink-faint: #07122A;
  --hairline: color-mix(in srgb, #07122A 22%, transparent);
  --focus: #07122A;
}

.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: var(--tr-body);
  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: #07101C; }
.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: #07101C; --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;
}
/* The bar also takes its lifted material while it is carrying the panel,
   even at scrollY 0: chrome densifies when it holds something. Tint and
   opacity only, both already transitioned, so §Navigation's "changes
   tint, never its box" still holds. */
.nav:is([data-lifted="true"], [data-menu="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:is([data-lifted="true"], [data-menu="true"]) {
  background: color-mix(in srgb, var(--coal) 82%, transparent);
}
.nav:is([data-lifted="true"], [data-menu="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: #07101C;
  --ink: #07101C;
  --ink-dim: color-mix(in srgb, #07101C 62%, transparent);
  /* These four were missing, which is a bug older than the products panel and
     dormant only because no .section--light exists yet: .nav__burger's
     `border: 1px solid var(--hairline)` and the bright top edge both resolve
     from Hot White, so they would go white-on-white the day a light section
     passes under the bar. Values mirror .section--light so chrome and content
     agree, and --red-ink follows the Don't-list rule — #3E6BFF is never type
     on Hot White, Burnt Red is. */
  --ink-faint: color-mix(in srgb, #07101C 62%, transparent);
  --hairline: color-mix(in srgb, #07101C 14%, transparent);
  --hairline-2: color-mix(in srgb, #07101C 8%, transparent);
  --red-ink: var(--burnt-red);
  color: #07101C;
}
.nav[data-over="light"]:is([data-lifted="true"], [data-menu="true"]) {
  background: color-mix(in srgb, var(--hot-white) 88%, transparent);
}
.nav[data-over="light"] .nav__logo { fill: #07101C; }
.nav[data-over="light"] .nav__link:hover { background: color-mix(in srgb, #07101C 7%, transparent); }
.nav[data-over="light"]::after {
  background: linear-gradient(90deg, transparent, color-mix(in srgb, #07101C 10%, transparent) 30%, color-mix(in srgb, #07101C 10%, transparent) 70%, transparent);
}

.nav[data-over="red"] {
  background: color-mix(in srgb, var(--coal) 44%, transparent);
}
.nav[data-over="red"]:is([data-lifted="true"], [data-menu="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; }
/* align-self: stretch because .nav is align-items:center, which sizes this
   row to its content rather than to the bar. Without it the products item
   stops 12px short of the bar's bottom edge and the pointer crosses dead
   ground on its way to the panel. Nothing moves: align-items stays centre. */
.nav__links { display: flex; align-items: center; gap: 4px; align-self: stretch; }
.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__cta { margin-left: 0; padding: 9px 11px; font-size: var(--fs-ui-sm); }
}

/* --------------------------------------------------------------------------
   6b. Products panel — a drawer of raised material, hung off the bar's column
   -------------------------------------------------------------------------- */
/* The .nav__brand trick again: padding plus a negative margin grows the box
   without moving what is in it, so the item's border box is exactly the bar's
   and there is no strip of bar padding between the trigger and the panel. That,
   plus the panel being a DOM child (pointerleave does not fire when the pointer
   enters a descendant), removes the diagonal-drift bug structurally instead of
   patching it with a timer. The item is deliberately NOT position:relative:
   the panel's containing block is .nav, which is what lets it take the shell's
   own column edge rather than a pointer position. */
.nav__item {
  display: flex; align-items: center;
  align-self: stretch; margin-block: -12px; padding-block: 12px;
}
.nav__trigger {
  display: flex; align-items: center;
  border-radius: var(--r-btn);
  transition: background-color 160ms ease;
}
.nav__item:hover .nav__trigger,
.nav__item[data-open="true"] .nav__trigger {
  background: color-mix(in srgb, var(--hot-white) 8%, transparent);
}
/* The link and the caret share one hover wash, so they read as one control. */
.nav__item--products .nav__link { padding: 9px 4px 9px 13px; }
.nav__item--products .nav__link:hover { background: none; }
.nav[data-over="light"] .nav__item:hover .nav__trigger,
.nav[data-over="light"] .nav__item[data-open="true"] .nav__trigger {
  background: color-mix(in srgb, #07101C 7%, transparent);
}

/* 26x34 clears WCAG 2.2 Target Size (Minimum). */
.nav__disc {
  width: 26px; height: 34px; padding: 0;
  background: none; border: 0; display: grid; place-items: center;
  cursor: pointer; color: var(--ink-dim);
  transition: color 160ms ease;
}
.nav__item:hover .nav__disc,
.nav__item[data-open="true"] .nav__disc { color: var(--ink); }
.nav__disc svg { transition: transform 220ms cubic-bezier(0.2, 0, 0, 1); }
.nav__item[data-open="true"] .nav__disc svg { transform: rotate(180deg); }

.nav__panel {
  position: absolute; z-index: 2;   /* over the bar's own ::before scrim */
  top: 100%;
  /* The bar's padding-inline, byte for byte: fixed chrome is capped the way
     the content shell is, so the panel's edge is a column edge. */
  right: max(var(--margin), calc((100% - var(--maxw)) / 2));
  width: 384px; max-width: calc(100vw - 2 * var(--margin));
  max-height: calc(100dvh - 96px);
  overflow-y: auto; overscroll-behavior: contain;
  padding: 4px 0;
  /* Square, and joined to the bar: 14px belongs to content cards that sit in
     the layout, and a line across the seam would cut one object in two. */
  border: 1px solid var(--hairline);
  border-top: 0;
  /* Solid Surface Raised, one tonal step off the Coal page — depth is tonal
     here, never a shadow. No second blur: the app-card badge is the precedent
     for stacked chrome, and the bar's backdrop-filter is clipped to its own
     border box anyway, so this hangs over unfiltered content and has to carry
     its own ground. */
  background: var(--surface-2);
  /* The panel keeps one identity on every ground. An opaque sheet has no
     "beneath", so the material-follows-the-surface rule — which is about
     translucent chrome — does not reach it, exactly as the badge stays Coal
     over any poster. --focus is the load-bearing one: a light bar sets it to
     #07101C, which is invisible inside #081224. */
  --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);
  --red-ink:    var(--red);
  --focus:      var(--red);
  color: var(--hot-white);
  /* A clip wipe, not a slide. The panel paints above the bar's background, so
     a translateY start state would cover the bar's own bottom edge for the
     length of the transition; the wipe also reads as what this is — material
     drawn out from behind the bar, not a card arriving in front of the page.
     visibility is the third transitioned property, and is what takes the
     closed rows out of the tab order and the accessibility tree. */
  opacity: 0; visibility: hidden;
  clip-path: inset(0 0 100% 0);
  transition: opacity 140ms ease,
              clip-path 140ms cubic-bezier(0.2, 0, 0, 1),
              visibility 0s linear 140ms;
}
.nav__item[data-open="true"] .nav__panel {
  opacity: 1; visibility: visible;
  clip-path: inset(0 0 0 0);
  transition: opacity 120ms ease,
              clip-path 220ms cubic-bezier(0.2, 0, 0, 1),
              visibility 0s;
}

.nav-panel__list { list-style: none; margin: 0; padding: 0; }
.nav-panel__list li + li { border-top: 1px solid var(--hairline-2); }

.nav-panel__row {
  position: relative;
  display: grid; grid-template-columns: 32px 1fr 11px;
  align-items: center; column-gap: 14px;
  min-height: 56px; padding: 12px 18px;
  transition: background-color 160ms ease, color 160ms ease;
}
/* The rail indicator's idiom: 2px of Catalyst Red that scales rather than
   resizes, which is the Don't-list rule taken literally rather than by
   analogy. */
.nav-panel__row::before,
.nav-panel__all::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--red); border-radius: var(--r-hairline);
  transform: scaleY(0);
  transition: transform 200ms cubic-bezier(0.2, 0, 0, 1);
}
/* One more tonal step: Coal page, Surface Raised panel, Charcoal row. A row
   lighting to the app cards' own fill is that card compressed to a line. */
.nav-panel__row:hover,
.nav-panel__row:focus-visible { background: var(--charcoal); }
.nav-panel__row:hover::before,
.nav-panel__row:focus-visible::before { transform: scaleY(1); }
.nav-panel__row:focus-visible { outline-offset: -3px; }

/* 8px on 32px is the corner .app-card__icon already gives this exact PNG.
   No frame: a hairline box around a baked squircle leaves four corner gaps. */
.nav-panel__icon {
  width: 32px; height: 32px; border-radius: 8px;
  -webkit-user-drag: none; user-select: none; -webkit-user-select: none;
}
.nav-panel__text { display: grid; row-gap: 3px; min-width: 0; }
.nav-panel__name {
  font-size: var(--fs-ui); font-weight: 600; line-height: 1;
  letter-spacing: var(--tr-head); color: var(--ink);
}
.nav-panel__tag {
  font-size: var(--fs-ui-sm); font-weight: 500; line-height: 1.2;
  letter-spacing: var(--tr-body); color: var(--ink-dim);
}
/* The tagline's closing beat, coloured not slanted — the same move as
   .app-card__tag em, so the rail and the menu say the product's name the
   same way. */
.nav-panel__tag em { font-style: normal; color: var(--red-ink); }
.nav-panel__arrow {
  color: var(--ink-faint);
  transition: transform 240ms cubic-bezier(0.2, 0, 0, 1), color 160ms ease;
}
.nav-panel__row:hover .nav-panel__arrow,
.nav-panel__all:hover .nav-panel__arrow { color: var(--ink-dim); }
.nav-panel__row:hover .nav-panel__arrow { transform: translate(2px, -2px); }
.nav-panel__all:hover .nav-panel__arrow { transform: translateY(2px); }

/* The foot reads as a foot on hairline weight alone: 12% against the rows' 7%,
   no new colour and no label. */
.nav-panel__all {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  padding: 13px 18px; border-top: 1px solid var(--hairline);
  font-size: var(--fs-ui); font-weight: 500; color: var(--ink-dim);
  transition: background-color 160ms ease, color 160ms ease;
}
.nav-panel__all:hover, .nav-panel__all:focus-visible {
  color: var(--ink); background: var(--charcoal);
}
.nav-panel__all:hover::before,
.nav-panel__all:focus-visible::before { transform: scaleY(1); }
.nav-panel__all:focus-visible { outline-offset: -3px; }

/* With scripting off there is no dead control and the disclosure still works.
   The `transition: visibility 0s` is not decoration: without it the open state
   inherits the closed state's 140ms delay and the rows are unfocusable for
   that long, so a quick Tab from Products lands on LinkedIn instead. */
html:not(.js) .nav__disc { display: none; }
html:not(.js) .nav__item--products:hover .nav__panel,
html:not(.js) .nav__item--products:focus-within .nav__panel {
  opacity: 1; visibility: visible; clip-path: inset(0 0 0 0);
  transition: opacity 120ms ease, clip-path 220ms cubic-bezier(0.2, 0, 0, 1), visibility 0s;
}
/* Below 820px .nav__links is already display:none, which takes this whole
   subtree with it — absolutely positioned descendants included — so the panel
   needs no breakpoint of its own. */

/* The same five rows at sheet scale. */
.nav-sheet__apps { list-style: none; margin: 0; padding: 0; display: grid; }
.nav-sheet__apps a {
  display: grid; grid-template-columns: 32px 1fr auto;
  align-items: center; column-gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--hairline-2);
  color: var(--ink);
}
.nav-sheet__apps img {
  width: 32px; height: 32px; border-radius: 8px;
  -webkit-user-drag: none; user-select: none; -webkit-user-select: none;
}
.nav-sheet__text { display: grid; row-gap: 2px; min-width: 0; }
/* One step below the sheet's 1.5rem top-level links, which is what subordinates
   the apps to the three primary destinations. The 44px indent does the rest —
   no heading, no third type style. */
.nav-sheet__name {
  font-size: var(--fs-subtitle); font-weight: 600; line-height: 1.15;
  letter-spacing: var(--tr-head);
}
.nav-sheet__tag {
  font-size: var(--fs-ui-sm); font-weight: 500; line-height: 1.2;
  letter-spacing: var(--tr-body); color: var(--ink-dim);
}
.nav-sheet__tag em { font-style: normal; color: var(--red-ink); }
.nav-sheet__apps svg { color: var(--ink-faint); }

/* Mirrors the sheet's own degradation: the panel still appears and disappears,
   it just stops travelling. A hard pop under the pointer is worse than a
   120ms fade, so the opacity stays. */
@media (prefers-reduced-motion: reduce) {
  .nav__panel {
    clip-path: none;
    transition: opacity 120ms ease, visibility 0s linear 120ms;
  }
  .nav__item[data-open="true"] .nav__panel,
  html:not(.js) .nav__item--products:hover .nav__panel,
  html:not(.js) .nav__item--products:focus-within .nav__panel {
    clip-path: none;
    transition: opacity 120ms ease, visibility 0s;
  }
  .nav__disc svg,
  .nav-panel__row::before,
  .nav-panel__all::before,
  .nav-panel__arrow { transition: none; }
  .nav-panel__row:hover .nav-panel__arrow,
  .nav-panel__all:hover .nav-panel__arrow { transform: none; }
}

/* 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;
  /* This used to refuse the pan outright, on the premise that portrait
     content never scrolls. Five app rows retire that premise: 84px of top
     padding, three ~57px links, five ~61px rows and 48px of foot is about
     608px, which overflows a 375x667 phone — and `none` would make the sheet
     unscrollable with no symptom except links cut off below the fold. What
     actually holds the page still behind it is overscroll-behavior: contain
     above, plus the documentElement overflow lock in setOpen(). */
  touch-action: pan-y;
}
@media (max-height: 520px) {
  .nav-sheet { padding-top: 68px; padding-bottom: var(--sp-5); }
  .nav-sheet > a { padding: 10px 0; font-size: var(--fs-subtitle); }
  .nav-sheet__apps a { padding: 7px 0; }
  .nav-sheet__name { font-size: var(--fs-ui); }
}
.nav-sheet[data-open="true"] { opacity: 1; pointer-events: auto; transform: none; }
/* `>` rather than a higher-specificity override: this rule sets five
   properties, and an override only wins for the ones it names — letter-spacing
   and the border-bottom would leak into the nested app rows. */
.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: var(--tr-head);
  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: var(--tr-display);
  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: #07101C; }

/* --------------------------------------------------------------------------
   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: var(--tr-display);
  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);
  /* Shorter than the old clamp: the controls row now sits between this and
     the cards, and carries part of the separation the margin used to hold. */
  margin-bottom: clamp(28px, 3.2vw, 52px);
}
.portfolio__head .lead { max-width: 54ch; }

.rail {
  position: relative;
  margin-inline: calc(var(--margin) * -1);
  padding-inline: var(--margin);
  /* The clip is what lets the track run off both page edges. The margin is
     for the focus ring: the controls now sit flush against the top of this
     box, and a 2px outline at 3px offset would otherwise be shaved off. The
     extra 8px sideways stays outside the viewport, where the body's own
     overflow-x clip already hides it. */
  overflow: clip;
  overflow-clip-margin: 8px;
}
/* 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: var(--tr-head);
  white-space: normal;
  text-align: left;
  line-height: 1.2;
  max-width: calc(100% - 28px);
  /* 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 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: var(--tr-head);
  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; }
.app-card__platforms {
  margin: 0;
  color: var(--ink-dim);
  font-size: var(--fs-ui-sm);
  font-weight: 500;
  letter-spacing: var(--tr-body);
  line-height: 1.35;
}

/* The controls read as two objects on one line above the cards — position on
   the left margin, travel on the right — so both are framed to the same 42px
   height and the row balances instead of trailing off. Square, per §Shapes:
   the only curves in the system belong to the catalysts. */
.rail__controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-5); margin-bottom: var(--sp-5);
}
.rail__nav { display: flex; align-items: center; gap: 8px; }
.rail__dots {
  display: flex; align-items: center; gap: 2px;
  height: 42px; padding-inline: 12px;
  border: 1px solid var(--hairline); border-radius: var(--r-btn);
}
/* A styled display beats the UA sheet, so the JS `hidden` needs saying here. */
.rail__dots[hidden] { display: none; }
/* The bar is 3px; the target is not. */
.rail__dot {
  width: 26px; height: 40px; border: 0; padding: 0; background: none;
  display: grid; place-items: center; cursor: pointer;
}
.rail__dot::before {
  content: ""; display: block;
  width: 22px; height: 3px; border-radius: var(--r-hairline);
  background: color-mix(in srgb, var(--ink) 38%, transparent);
  transform: scaleX(0.38);
  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;
  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 cobalt on
   a ground that deepens from night navy into mixed brand blue, 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: cool 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 night navy so it reads as the
     same surface as the momentum section, then deepens into cobalt. */
  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, #050A16, #1A3A78 16%, #3E6BFF 40%,
    #3EC8FF 58%, #9ECFFF 72%, #B8C4FF 88%, #1D4ED8);
}
.footer__strip svg { width: 100%; height: 100%; }

/* The giant type. Full shell width, solid cobalt — 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); }
.js .footer__wordmark[data-ink] { overflow-clip-margin: 0.4em; }
.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__legal {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.footer__legal a {
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.footer__legal a:hover {
  color: var(--f-ink);
  text-decoration: underline;
  text-decoration-color: currentColor;
}
.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__legal { align-items: center; }
  .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) {
  /* The menu state has to be listed here or it out-specifies this block and
     hands the translucent bar back exactly while a panel hangs off it. The
     data-over variants are widened at the same time: at (0,3,0) they already
     beat this block today, a hole that has been dormant only because no
     .section--light or .section--red exists yet. */
  .nav,
  .nav:is([data-lifted="true"], [data-menu="true"]),
  .nav[data-over="red"]:is([data-lifted="true"], [data-menu="true"]) {
    background: var(--coal); backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .nav[data-over="light"],
  .nav[data-over="light"]:is([data-lifted="true"], [data-menu="true"]) {
    background: var(--hot-white); 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: #07101C; --ink-faint: #07101C;
    --hairline: color-mix(in srgb, #07101C 40%, transparent);
    --hairline-2: color-mix(in srgb, #07101C 24%, transparent); }
  .section--red { --ink-dim: #07122A; --ink-faint: #07122A;
    --hairline: color-mix(in srgb, #07122A 46%, transparent); }
  /* The menu state has to be named here too. .nav:is([data-lifted],[data-menu])
     is (0,2,0) and would otherwise out-specify a bare .nav, handing the bar its
     translucent tint back exactly while a panel hangs off it. */
  .nav,
  .nav:is([data-lifted="true"], [data-menu="true"]) { background: var(--coal); }
  .nav[data-over="light"],
  .nav[data-over="light"]:is([data-lifted="true"], [data-menu="true"]) { background: var(--hot-white); }
  /* The panel pins its own ink and hairline tokens so it can keep one identity
     on every ground, which also means the :root boost above cannot reach inside
     it. Re-state the boost at the panel's own level or its five separators stay
     at 7% while the rest of the page moves to 24%. */
  .nav__panel {
    --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);
  }
  .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: var(--tr-head);
}

/* --------------------------------------------------------------------------
   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, #3EC8FF, #3E6BFF);
  will-change: transform;
}

/* --------------------------------------------------------------------------
   21. Two-tone secondary CTA. A cobalt brand needs a defined non-cobalt
   button: a deep navy bar with the arrow broken out into a brand 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: #16356A; }
.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); }

/* Hyjc wordmark — same lockup slot, company name instead of Catalyst lettering */
.nav__wordmark {
  display: flex;
  align-items: center;
  color: var(--hot-white);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: var(--tr-display);
  line-height: 1;
}
.nav[data-over="light"] .nav__wordmark { color: #07101C; }
.footer__hyjc {
  display: block;
  font-size: clamp(3.2rem, 13vw, 11rem);
  font-weight: 700;
  line-height: 1.18;
  letter-spacing: var(--tr-display);
  color: var(--red);
}

/* Chatwoot: hide stock bubble; crop "Powered by"; keep composer bottom gap. */
.woot-widget-bubble,
.woot--bubble-holder {
  display: none !important;
  visibility: hidden !important;
  pointer-events: none !important;
}
.woot-widget-holder:not(.woot--hide) {
  overflow: hidden !important;
  background: #fff !important;
}
.woot-widget-holder:not(.woot--hide) iframe {
  /* Clip ~36px branding; +16px height → ~20px white gap under composer. */
  height: calc(100% + 16px) !important;
  max-height: none !important;
  clip-path: inset(0 0 36px 0);
}

.hyjc-cw-launcher {
  position: fixed;
  right: 20px;
  bottom: 32px;
  z-index: 2147483000;
  box-sizing: border-box;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  margin: 0;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--hot-white) 18%, transparent);
  border-radius: 50%;
  background: var(--red);
  color: var(--hot-white);
  cursor: pointer;
  box-shadow: 0 10px 28px rgba(5, 10, 22, 0.35);
  transition: background-color 160ms ease, transform 160ms ease, border-color 160ms ease;
}
.hyjc-cw-launcher:hover {
  background: var(--red-hot);
  border-color: color-mix(in srgb, var(--hot-white) 28%, transparent);
}
.hyjc-cw-launcher:focus-visible {
  outline: 2px solid var(--hot-white);
  outline-offset: 3px;
}
.hyjc-cw-launcher:active {
  transform: translateY(1px);
}
.hyjc-cw-launcher__chat {
  display: block;
  width: 22px;
  height: 22px;
  pointer-events: none;
}
.hyjc-cw-launcher__close {
  display: none;
}
.hyjc-cw-launcher[data-open="true"] .hyjc-cw-launcher__chat {
  display: none;
}
.hyjc-cw-launcher[data-open="true"] .hyjc-cw-launcher__close {
  display: block;
}
/* Keep the panel input clear; widget header already has close. */
.hyjc-cw-launcher[data-open="true"] {
  opacity: 0;
  pointer-events: none;
}
@media (max-width: 640px) {
  .hyjc-cw-launcher {
    right: 16px;
    bottom: 28px;
    width: 48px;
    height: 48px;
  }
}
