/* ============================================================================
   CEIN — Citywide Emergency Integration Network — design system
   Spec of record: brand_assets/brand.md, transcribed from CIEN Brand Guidelines.pdf
   Dark surfaces (the palette fails on light), azure structure, orange signal,
   hairline rules, grain. Tailwind (CDN) handles layout; this file owns the system.
   ========================================================================== */

/* ---------------------------------------------------------------- 1. Tokens */

:root {
  /* Surfaces — three planes, never one flat field.
     ink-900 is sampled from the guidelines deck background; 800/700 extend it. */
  --ink-900: #1e1e1e;
  --ink-800: #262628;
  --ink-700: #2f2f32;

  --hairline: rgba(255, 255, 255, 0.09);
  --hairline-hi: rgba(255, 255, 255, 0.18);

  /* Brand White */
  --text-hi: #fefefe;
  /* Body copy, every dark surface. 12.4 / 11.7 / 9.9 : 1 on ink-900 / 800 / 700,
     and 9.0 on the accent wash. Raised from #b3b5b8, which cleared AA at 8.1 but
     read as grey rather than as text — the whole point of a dark ground is that
     copy can sit high on it. Two steps below --text-hi so headings keep a
     visible colour step and do not rely on size and weight alone. */
  --text-mid: #dcdee1;
  /* 8.7 / 8.2 / 7.0 : 1 on ink-900 / 800 / 700, and 6.4 on the accent wash.
     Must clear AA normal text on --ink-700 and on the wash-tinted table column —
     the two lightest backgrounds — not merely on the canvas. This token drives
     ten small-text roles, so it is a text colour, not a decorative one. Held one
     step under --text-mid so quiet text still reads as quiet. */
  --text-lo: #b8bcc2;

  /* Brand: Azure + Pastel Orange. Orange is the louder signal, so it carries
     primary actions; azure carries structure, links, and data emphasis.
     Note orange is unusable as text on white (1.71:1) — dark surfaces only. */
  --accent: #feb903;
  --azure: #1b92a7;
  --azure-hi: #3fb3c7;
  --accent-wash: rgba(254, 185, 3, 0.10);
  --azure-wash: rgba(27, 146, 167, 0.12);
  --signal: #ff5e3a;

  /* Paper band only — the two light sections ported from the light build (§18b).
     Azure shaded down twice so a light ground still has somewhere dark to go;
     both are derived from --azure, no new hues. Amber stays off paper as text
     (1.71:1) and appears there only as a decorative bullet or rule. */
  --paper: #fafbfb;
  --teal-deep: #0e4e5a;
  --teal-ink: #072f38;
  /* Body copy on paper, 10.9:1. Solid rather than the previous
     rgba(30,30,30,0.68), which composited to ≈#646465 — 5.8:1, and the light
     band's copy read noticeably weaker than the dark bands'. Opaque also means
     the value holds if a role ever lands on a tinted plate inside the band. */
  --ink-on-paper: #3a3b3d;
  /* Azure-tinted elevation. The dark system's shadows are black-based and vanish
     on paper, so this band needs its own. */
  --lift-2:
    0 1px 2px rgba(27, 146, 167, 0.06),
    0 8px 24px -8px rgba(27, 146, 167, 0.18),
    0 24px 48px -24px rgba(14, 78, 90, 0.24);
  --spring: cubic-bezier(0.22, 1, 0.36, 1);

  /* Type — Garet is licensed and self-hosted; League Spartan is the brand's own
     secondary face and covers until Garet is installed. See brand_assets/brand.md. */
  --font-display: "Garet", "League Spartan", "Trebuchet MS", sans-serif;
  --font-body: "Garet", "League Spartan", "Trebuchet MS", sans-serif;
  /* Not a brand font. Code and tabular data only. */
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;

  /* One ladder, eleven rungs, nothing off it. Every rung below names the role it
     serves — a size with no role is how the literals crept back in last time:
     eleven card titles reached 1.125rem through inline styles because there was
     no rung for a card title, and three lists disagreed at 14/15/16px. */
  --t-display: clamp(2.5rem, 6.5vw, 5.5rem);
  --t-h2: clamp(2.5rem, 5vw, 4.25rem);
  --t-h2-sm: clamp(2rem, 3vw, 2.75rem); /* sub-heading, under a section opener */
  --t-h3: 1.5rem; /* 24px — section sub-head */
  --t-h3-sm: 1.25rem; /* 20px — inset title: note, pull, tier */
  --t-h4: 1.125rem; /* 18px — card title */
  --t-lede: 1.1875rem; /* 19px — section lede */
  --t-body: 1.0625rem; /* 17px — prose */
  --t-small: 0.9375rem; /* 15px — captions, component lists, footer meta */
  --t-code: 0.8125rem; /* 13px — mono and tabular */
  --t-micro: 0.75rem; /* 12px — buttons, nav, tabs, link arrows */
  --t-label: 0.6875rem; /* 11px — eyebrow */

  /* Spacing ladder — nothing off-ladder */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --s-8: 64px;
  --s-9: 96px;
  --s-10: 128px;
  --s-11: 192px;

  /* Depth */
  --shadow-elev:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    0 12px 32px -12px rgba(0, 0, 0, 0.8),
    0 0 0 1px rgba(255, 255, 255, 0.06);
  --shadow-float:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 12px 32px -12px rgba(0, 0, 0, 0.8),
    0 32px 64px -24px rgba(0, 0, 0, 0.9),
    0 0 0 1px rgba(255, 255, 255, 0.08);
  --glow-accent: 0 0 40px -10px rgba(254, 185, 3, 0.35);

  /* Motion */
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  --dur: 420ms;

  --radius: 4px;
  --radius-lg: 10px;

  --shell: 1240px;
  --gutter: var(--s-5);
}

@media (min-width: 768px) {
  :root { --gutter: var(--s-7); }
}

/* ------------------------------------------------------------------ 2. Base */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  color-scheme: dark;
  scroll-behavior: smooth;
  /* Sticky nav is 68px; keep in-page anchors clear of it. */
  scroll-padding-top: 88px;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--ink-900);
  color: var(--text-mid);
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "kern" 1, "liga" 1;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Kept independent of Tailwind's preflight so the system holds on its own. */
h1, h2, h3, h4, p, figure, blockquote, dl, dd, pre { margin: 0; }

img, svg { display: block; max-width: 100%; }
svg { height: auto; }

::selection { background: var(--accent); color: var(--ink-900); }

/* ------------------------------------------------------- 3. Atmosphere */

/* Layered radial mesh — cool top-left, faint accent bottom-right, vignette. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(120vw 90vh at 8% -10%, rgba(28, 46, 54, 0.55), transparent 62%),
    radial-gradient(90vw 80vh at 104% 108%, rgba(27, 146, 167, 0.16), transparent 58%),
    radial-gradient(110vw 100vh at 50% 50%, transparent 42%, rgba(0, 0, 0, 0.55) 100%);
}

/* Grain. Fixed so it reads as film on the glass, not texture on the content. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Hairline grid — the entry and exit (hero, closing CTA), plus the backdrop the
   vision topology reads against. Not a general-purpose section texture. */
.grid-field {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(80% 70% at 50% 40%, #000 20%, transparent 78%);
}

.rule { border: 0; border-top: 1px solid var(--hairline); margin: 0; }

/* -------------------------------------------------------------- 4. Layout */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--s-8); position: relative; }
.section-lg { padding-block: var(--s-9); position: relative; }

@media (min-width: 1024px) {
  .section { padding-block: var(--s-9); }
  .section-lg { padding-block: var(--s-10); }
}

/* Consecutive sections stack their padding, and the sum leaves the rhythm — two
   128px sections read as one 256px void rather than a section break. Trim the
   trailing section's top so a boundary lands at 192px, not 256. */
section + .section,
section + .section-lg { padding-top: var(--s-7); }

@media (min-width: 1024px) {
  section + .section,
  section + .section-lg { padding-top: var(--s-8); }
}

/* ---------------------------------------------------------- 5. Typography */

.display,
.h2,
.h3,
.h4 {
  font-family: var(--font-display);
  color: var(--text-hi);
  text-wrap: balance;
  margin: 0;
}

.display {
  font-size: var(--t-display);
  font-weight: 800;
  line-height: 0.92;
  letter-spacing: -0.045em;
  font-variation-settings: "opsz" 96;
}

.h2 {
  font-size: var(--t-h2);
  font-weight: 800;
  line-height: 0.98;
  letter-spacing: -0.035em;
  font-variation-settings: "opsz" 64;
}

/* A heading that sits under a section opener rather than being one — the five
   layer headings in about.html. Modifier on .h2, not a size of its own, because
   everything but the scale is shared. */
.h2-sm { font-size: var(--t-h2-sm); }

.h3 {
  font-size: var(--t-h3);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
}

/* Card title — one rung under .h3. Every flow step and phase card was reaching
   this size through an inline font-size, eleven of them, because the ladder had
   no rung between 17px body and 24px .h3. */
.h4 {
  font-size: var(--t-h4);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
}

/* Size and measure only — no colour. This role appears on the hero video as well
   as on ink, and a colour declared here would beat the `text-white` inherited
   from .vex-hero, which is exactly how the hero lede came to render grey. */
.lede {
  font-size: var(--t-lede);
  line-height: 1.65;
  max-width: 34em;
  margin: 0;
}

.copy { max-width: 32em; margin: 0; }
.small { font-size: var(--t-small); line-height: 1.6; }

/* Mono section label — always numbered: "003 / PLATFORM" */
.label {
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  margin: 0;
}

.label::before {
  content: "";
  width: 24px;
  height: 1px;
  background: currentColor;
  opacity: 0.5;
}

.label-quiet { color: var(--text-lo); }
.label-quiet::before { display: none; }

/* Phase index inside a deployment card. Same metrics as .label, azure like
   .fund-tier-price — amber is reserved for the section eyebrow, and six amber
   labels in one grid would compete with the label that introduces them. The
   dash goes too: it reads as a section marker, not a card marker. */
.phase-label { color: var(--azure-hi); }
.phase-label::before { display: none; }

/* The unnumbered eyebrow. .label's metrics, but no leading rule and no colour of
   its own: this one sits inside the hero pill and on any ground the page cares to
   put it on, so it inherits and lets the element decide. .tag-paper (§18b) is the
   same idea with the paper band's teal pinned on. Colour left to the caller is
   deliberate — see the .lede note above for what happens otherwise. */
.tag {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin: 0;
}

.mono {
  font-family: var(--font-mono);
  font-size: var(--t-code);
  letter-spacing: 0.02em;
}

.accent { color: var(--accent); }

/* Editorial pull-quote */
.quote {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  line-height: 1.16;
  letter-spacing: -0.03em;
  color: var(--text-hi);
  margin: 0;
  text-wrap: pretty;
}

/* A pull-quote inside a column rather than across a full band. Modifier, same
   reasoning as .h2-sm. */
.quote-sm { font-size: clamp(1.5rem, 2.6vw, 2rem); }

/* -------------------------------------------------------------- 6. Surfaces */

.surface {
  background: var(--ink-800);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-elev);
}

.surface-float {
  background: var(--ink-700);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-float);
}

.surface-quiet {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.032), transparent 65%);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
}

/* Interactive surface: hairline lifts, card rises 2px. */
.surface-link {
  transition:
    transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}
.surface-link:hover { transform: translateY(-2px); border-color: var(--hairline-hi); }
.surface-link:active { transform: translateY(0); }

/* Every card that lifts also reveals on scroll, and `[data-reveal].is-in` sets
   `transform: none` at the same specificity from further down the file (§9) —
   which silently ate the lift. Out-specify it rather than reordering: the
   reveal's end state must keep winning over the pending translate. */
.surface-link[data-reveal].is-in:hover { transform: translateY(-2px); }
.surface-link[data-reveal].is-in:active { transform: translateY(0); }

/* Emphasis surface — the top funding tier, the final deployment phase. Amber
   marks a card without needing a badge: a warm border and a wash that falls
   off before the body copy, so the text stays on a neutral field.
   Owns the whole `border` shorthand rather than recolouring a `border-line`
   utility — the Tailwind CDN injects its sheet at runtime and can land after
   this file, which would take the border-color back to the hairline. For the
   same reason, do not pair this class with `border border-line` in markup. */
.surface-featured {
  border: 1px solid rgba(254, 185, 3, 0.42);
  background:
    linear-gradient(180deg, rgba(254, 185, 3, 0.05), transparent 32%),
    var(--ink-800);
}
.surface-featured:hover { border-color: rgba(254, 185, 3, 0.7); }

/* -------------------------------------------------------------- 7. Controls */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text-hi);
  --btn-bd: var(--hairline-hi);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  min-height: 44px;
  border: 1px solid var(--btn-bd);
  border-radius: var(--radius);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform 200ms var(--ease),
    background-color 200ms var(--ease),
    border-color 200ms var(--ease),
    color 200ms var(--ease),
    box-shadow 200ms var(--ease),
    opacity 200ms var(--ease);
}

.btn-primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--ink-900);
  --btn-bd: var(--accent);
  font-weight: 500;
}
.btn-primary:hover { box-shadow: var(--glow-accent); transform: translateY(-1px); }
.btn-primary:active { transform: translateY(1px); box-shadow: none; }

.btn-ghost { --btn-bg: rgba(255, 255, 255, 0.02); }
.btn-ghost:hover { --btn-bd: var(--text-lo); --btn-bg: rgba(255, 255, 255, 0.05); transform: translateY(-1px); }
.btn-ghost:active { transform: translateY(1px); }

.btn-quiet {
  --btn-bd: transparent;
  padding-inline: var(--s-2);
  color: var(--text-mid);
}
.btn-quiet:hover { color: var(--text-hi); }
.btn-quiet:active { transform: translateY(1px); }

/* Text link with an accent underline that grows from the left. */
.link {
  color: var(--text-hi);
  text-decoration: none;
  position: relative;
  transition: color 200ms var(--ease);
}
.link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -3px;
  width: 100%;
  height: 1px;
  background: var(--azure-hi);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 320ms var(--ease);
}
.link:hover { color: var(--azure-hi); }
.link:hover::after { transform: scaleX(1); }

/* Arrow links nudge on hover. */
/* Standalone call-to-action link, not inline prose — needs a real hit area. */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding-block: var(--s-2);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
  transition: color 200ms var(--ease);
}
.link-arrow svg { transition: transform 260ms var(--ease); }
.link-arrow:hover svg { transform: translateX(4px); }
.link-arrow:active svg { transform: translateX(2px); }

/* One focus treatment everywhere. */
:where(a, button, summary, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 6px var(--s-3);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.02);
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 500;
  /* Without this the pill inherits body 1.7 and stands 33px tall, which drags
     any row it shares (see .tier-head) out of alignment. */
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-mid);
}

.pill-dot {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--azure-hi);
  box-shadow: 0 0 0 3px rgba(27, 146, 167, 0.22);
  animation: pill-pulse 2.6s var(--ease) infinite;
}

@keyframes pill-pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(27, 146, 167, 0.22); }
  50% { box-shadow: 0 0 0 6px rgba(27, 146, 167, 0.06); }
}

/* ------------------------------------------------------------------ 8. Nav */

.skip-link {
  position: absolute;
  left: var(--s-4);
  top: var(--s-4);
  z-index: 100;
  padding: var(--s-3) var(--s-4);
  background: var(--accent);
  color: var(--ink-900);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--radius);
  transform: translateY(-200%);
  transition: transform 240ms var(--ease);
}
.skip-link:focus-visible { transform: translateY(0); }

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: 1px solid transparent;
  transition: background-color 300ms var(--ease), border-color 300ms var(--ease);
}
.nav[data-stuck="true"] {
  background: rgba(10, 11, 13, 0.82);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: var(--hairline);
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-6);
  height: 68px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--text-hi);
  text-decoration: none;
  transition: opacity 200ms var(--ease);
}
.brand:hover { opacity: 0.75; }

/* Real lockup, aspect 2.946:1. Height-driven so the nav bar controls the size. */
.brand-logo {
  height: 30px;
  width: auto;
}
footer .brand-logo { height: 38px; }
.nav-sheet .brand-logo { height: 34px; }

.brand-word {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.25rem;
  letter-spacing: -0.02em;
  line-height: 1;
}

.nav-link {
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-mid);
  text-decoration: none;
  padding: var(--s-2) 0;
  position: relative;
  transition: color 200ms var(--ease);
}
.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 280ms var(--ease);
}
.nav-link:hover { color: var(--text-hi); }
.nav-link:hover::after { transform: scaleX(0.6); }
.nav-link[aria-current="page"] { color: var(--text-hi); }
.nav-link[aria-current="page"]::after { transform: scaleX(1); }

/* Mobile sheet */
.nav-toggle {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  padding: 0 10px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: transparent;
  cursor: pointer;
  transition: border-color 200ms var(--ease);
}
.nav-toggle:hover { border-color: var(--hairline-hi); }
.nav-toggle span {
  display: block;
  height: 1.5px;
  background: var(--text-hi);
  transition: transform 300ms var(--ease), opacity 200ms var(--ease);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.nav-sheet {
  position: fixed;
  inset: 68px 0 0;
  z-index: 45;
  background: rgba(10, 11, 13, 0.97);
  backdrop-filter: blur(18px);
  border-top: 1px solid var(--hairline);
  padding: var(--s-7) var(--gutter);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  opacity: 0;
  transform: translateY(-8px);
  visibility: hidden;
  transition: opacity 260ms var(--ease), transform 260ms var(--ease), visibility 260ms;
}
.nav-sheet[data-open="true"] { opacity: 1; transform: translateY(0); visibility: visible; }

.nav-sheet a:not(.btn) {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 2rem;
  letter-spacing: -0.03em;
  color: var(--text-hi);
  text-decoration: none;
  padding-block: var(--s-3);
  border-bottom: 1px solid var(--hairline);
  transition: color 200ms var(--ease), padding-left 240ms var(--ease);
}
.nav-sheet a:not(.btn):hover { color: var(--accent); padding-left: var(--s-3); }

body[data-nav-open="true"] { overflow: hidden; }

@media (min-width: 1024px) {
  .nav-toggle, .nav-sheet { display: none; }
}

/* ----------------------------------------------------------- 9. Reveals */

[data-reveal] {
  opacity: 0;
  transition:
    opacity 760ms var(--ease),
    transform 760ms var(--ease),
    -webkit-mask-size 900ms var(--ease),
    mask-size 900ms var(--ease);
  transition-delay: var(--d, 0ms);
  will-change: opacity, transform;
}
[data-reveal="up"] { transform: translateY(26px); }
[data-reveal="down"] { transform: translateY(-20px); }
[data-reveal="left"] { transform: translateX(-32px); }
[data-reveal="right"] { transform: translateX(32px); }
[data-reveal="scale"] { transform: scale(0.94); }

/* Bottom-up wipe, done with a mask rather than the obvious `clip-path:
   inset(0 0 100% 0)`. clip-path collapses the element's box to zero height, and
   Chrome computes IntersectionObserver geometry *after* clipping — so a
   clip-hidden element reports isIntersecting: false even when it is parked dead
   centre in the viewport, and the observer that exists to reveal it can never
   fire. The three `clip` elements stayed invisible forever. A mask is paint-only
   and leaves the observed box intact, for an identical visual result.
   Do not swap this back to clip-path. */
[data-reveal="clip"] {
  transform: translateY(12px);
  -webkit-mask-image: linear-gradient(#000, #000);
  mask-image: linear-gradient(#000, #000);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  /* Anchored at the bottom edge, so growing the height reveals upward. */
  -webkit-mask-position: 0 100%;
  mask-position: 0 100%;
  -webkit-mask-size: 100% 0%;
  mask-size: 100% 0%;
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
  will-change: auto;
}
[data-reveal="clip"].is-in {
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

/* Below lg the 12-col composition collapses to one column, so a pending
   translateX pushes content past the right edge and the page gains a sideways
   scroll before the element reveals. Slide vertically instead. */
@media (max-width: 1023px) {
  [data-reveal="left"],
  [data-reveal="right"] { transform: translateY(24px); }
}

/* --------------------------------------------------------------- 10. Stats */

.stat-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border-top: 1px solid var(--hairline);
  border-left: 1px solid var(--hairline);
}
.stat {
  padding: var(--s-6) var(--s-5);
  border-right: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}
@media (min-width: 900px) {
  .stat-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .stat { padding: var(--s-7) var(--s-6); }
}

.stat-value {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.75rem, 5.2vw, 4.25rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--text-hi);
  display: flex;
  align-items: baseline;
  gap: 2px;
}
.stat-unit {
  font-family: var(--font-display);
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--accent);
  transform: translateY(-0.35em);
}
.stat-label {
  margin: var(--s-4) 0 0;
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-lo);
}

/* ------------------------------------------------------------ 11. Logo wall */

.logo-cell {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-5) var(--s-4);
  color: var(--text-hi);
  opacity: 0.45;
  transition: opacity 320ms var(--ease), transform 320ms var(--ease);
}
.logo-cell:hover { opacity: 1; transform: translateY(-2px); }
.logo-cell span {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-body);
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.logo-cell img { width: 26px; height: 26px; flex: none; }

/* -------------------------------------------------------------- 12. Marquee */

.marquee {
  overflow: hidden;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  padding-block: var(--s-6);
  mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}
.marquee-track {
  display: flex;
  /* No `gap` here — em on the track resolves against the inherited body size
     (~17px), not the 6.5vw spans, so the phrases collide. Spacing lives on the
     spans, where em means what it should — which also keeps the gap across the
     copy seam identical to the gaps within a copy. */
  width: max-content;
  /* The track carries two identical copies of the phrase set, so -50% of its
     own max-content width is exactly one copy — the loop restarts on a frame
     identical to the one it started on, with no visible seam. Keep the copies
     in sync with the markup: change the phrases and you change both.

     One copy measures ~1.05 viewport widths at every breakpoint (the type is
     sized in vw), so a single duration reads at the same relative pace on
     mobile and desktop — 22s puts a phrase on screen for roughly 4.5s. */
  animation: marquee-slide var(--marquee-dur, 22s) linear infinite;
  will-change: transform;
}
@keyframes marquee-slide {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}
/* Duration is time-per-copy, not speed, so a longer phrase set races unless it
   gets proportionally longer to travel. Scale --marquee-dur with the set's
   width — response.html's sentence-length phrases run ~2.3 viewport widths per
   copy against ~1.05 for the one-word tagline. Both land near 70px/s. */
.marquee-wide { --marquee-dur: 48s; }

/* Let a reader stop the band to actually finish reading it. */
.marquee:hover .marquee-track { animation-play-state: paused; }
/* Solid first, so the leading phrase reads even where the edge mask fades it. */
.marquee-track span {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 6.5vw;
  line-height: 1.08; /* clears descenders at this size; 0.95 clipped them */
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--text-hi);
  opacity: 0.92;
  white-space: nowrap;
  padding-right: 0.3em;
}
.marquee-track span:nth-child(even) {
  color: transparent;
  -webkit-text-stroke: 1px rgba(237, 239, 242, 0.26);
  opacity: 1;
}
.marquee-track span em {
  font-style: normal;
  color: var(--accent);
  -webkit-text-stroke: 0;
}

/* ----------------------------------------------------- 13. Framework layers */

/* Five steps on one flow line. Each step draws its own segment — from its dot
   to the next step's — rather than one absolute rule across the row: the line
   then stops at 05 instead of running on to the shell edge, and the geometry
   survives any column count without magic offsets. */
.flow-step { position: relative; }

.flow-dot {
  position: relative;
  z-index: 1; /* over the segment */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  background: var(--ink-800);
  border: 1px solid var(--hairline-hi);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: 700;
  line-height: 1;
  color: var(--text-lo);
  transition:
    transform var(--dur) var(--ease),
    background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

/* End of the flow takes the accent, like the final deployment phase. */
.flow-step:last-child .flow-dot {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink-900);
}

/* .flow-meta carries no base declaration at all — it is body text, so it takes
   the body size and colour like every other paragraph, and exists here only as
   the hover hook below. It used to set --text-lo, which made a component list
   read as a footnote to the copy above it when it is the same kind of content. */

/* The lift goes on the dot, not on the [data-reveal] step: `[data-reveal].is-in`
   sets `transform: none`, which is the same fight .surface-link documents in §6.
   Transforming the child sidesteps it entirely. */
.flow-step:hover .flow-dot {
  transform: scale(1.1);
  border-color: var(--accent);
  box-shadow: 0 0 0 6px var(--accent-wash);
}
.flow-step:hover .flow-meta { color: var(--azure-hi); }

/* A line only where the steps actually sit in a row. */
@media (min-width: 1024px) {
  /* Five columns at this width are ~203px, wide enough for one heading but not
     two — "Communications infrastructure" wraps and 01's copy then starts a line
     below everyone else's. Reserve the second line for all five so the copy and
     the component lists share a baseline. 2.5em = two lines at line-height 1.25. */
  .flow-step h3 { min-height: 2.5em; }

  .flow-step:not(:last-child)::after {
    content: "";
    position: absolute;
    top: 14px; /* dot centre — 28 / 2 */
    left: 28px; /* the dot's right edge */
    /* Reach the next dot: the rest of this column, plus the 32px column gap. */
    width: calc(100% - 28px + var(--s-6));
    height: 1px;
    /* Azure, not white: the rail is structure, and an alpha the token set does
       not carry — the same one .topo-core uses for the same reason. */
    background: rgba(27, 146, 167, 0.55);
  }
}

/* ------------------------------------------------- 13b. Vision topology */

/* Styles for the topology inlined into #vision (reference copy:
   images/hero-topology.svg). Geometry carries classes, the stylesheet owns the
   paint — but here the state class gates looping animation rather than
   switching colour.

   The literal rgba() values are alphas the token set does not carry, exactly
   as the §13 flow line does. Everything with a brand hue goes through a var. */
.topo-hair { fill: none; stroke: var(--hairline-hi); stroke-width: 1; }
.topo-mesh { fill: none; stroke: var(--hairline); stroke-width: 1; }
.topo-node { fill: var(--ink-800); stroke: rgba(255, 255, 255, 0.34); stroke-width: 1.25; }

/* Azure, not orange: the core is structure, and orange is reserved for the
   travelling signal. Same division as the funding figure. */
.topo-core { fill: var(--ink-800); stroke: rgba(27, 146, 167, 0.45); stroke-width: 1.5; }

.topo-blink, .topo-blink-b { fill: var(--accent); }
.topo-ring-c { fill: none; stroke: rgba(27, 146, 167, 0.35); stroke-width: 1; stroke-dasharray: 4 7; }
.topo-egress { fill: var(--ink-900); stroke: var(--accent); stroke-width: 1.75; }
.topo-egress-dot { fill: var(--accent); }

.topo-ring { transform-box: fill-box; transform-origin: center; }

/* 634 = the dash parked one full period past the path start, so before the
   reveal the routes are bare hairlines rather than three stray amber ticks. */
.topo-pulse {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-dasharray: 14 620;
  stroke-dashoffset: 634;
}

@keyframes topo-travel { from { stroke-dashoffset: 634; } to { stroke-dashoffset: 0; } }
@keyframes topo-breathe { 0%, 100% { opacity: 0.25; } 50% { opacity: 0.9; } }
@keyframes topo-spin { to { transform: rotate(360deg); } }

/* Declared only under .is-in — which the reveal observer sets on the wrapper —
   so nothing loops while the section is off screen. Offsets are staggered so
   the three routes never fire in lockstep. */
.is-in .topo-p1 { animation: topo-travel 3.2s linear infinite; }
.is-in .topo-p2 { animation: topo-travel 3.2s linear infinite 0.9s; }
.is-in .topo-p3 { animation: topo-travel 3.2s linear infinite 1.9s; }
.is-in .topo-blink { animation: topo-breathe 2.8s ease-in-out infinite; }
.is-in .topo-blink-b { animation: topo-breathe 2.8s ease-in-out infinite 1.4s; }
.is-in .topo-ring { animation: topo-spin 22s linear infinite; }

/* ------------------------------------------------------------ 14. Code block */

.tablist {
  display: flex;
  gap: var(--s-1);
  padding: var(--s-2) var(--s-2) 0;
  border-bottom: 1px solid var(--hairline);
}
.tab {
  padding: var(--s-3) var(--s-4);
  border: 0;
  border-bottom: 1px solid transparent;
  margin-bottom: -1px;
  background: none;
  color: var(--text-lo);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}
.tab:hover { color: var(--text-mid); }
.tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }

.code {
  margin: 0;
  padding: var(--s-6);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--t-code);
  line-height: 1.85;
  color: var(--text-mid);
  tab-size: 2;
}
.code .k { color: #5fc3d6; }          /* keyword  — azure, lightened for AA */
.code .s { color: var(--accent); }    /* string */
.code .c { color: var(--text-lo); }   /* comment */
.code .f { color: var(--text-hi); }   /* identifier / flag */
.code .n { color: #ffcf5c; }          /* number   — orange, lightened */

.copy-btn { position: absolute; top: var(--s-3); right: var(--s-3); }
.copy-btn[data-copied="true"] { --btn-bd: var(--accent); color: var(--accent); }

/* ------------------------------------------------------------- 15. Pricing */

/* Fixed-height header row so prices share a baseline across tiers whether or not
   a tier carries a badge. */
.tier-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  min-height: 26px;
}
.tier-head .foot-head { margin: 0; }

.tier-price {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 3.25rem;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--text-hi);
  display: flex;
  align-items: baseline;
  /* No gap: the currency glyph is its own flex item and a gap sets it adrift
     ("$ 480"). The unit gets its own margin instead. */
}
.tier-price small {
  margin-left: var(--s-2);
  font-family: var(--font-display);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-lo);
}

.feature-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.feature-list li {
  display: grid;
  grid-template-columns: 16px 1fr;
  gap: var(--s-3);
  font-size: var(--t-body);
  line-height: 1.55;
}
.feature-list svg { margin-top: 5px; color: var(--accent); }
.feature-list .off { color: var(--text-lo); }
.feature-list .off svg { color: var(--text-lo); }

.toggle {
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.02);
}
.toggle button {
  padding: var(--s-2) var(--s-4);
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-mid);
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 240ms var(--ease), color 240ms var(--ease);
}
.toggle button:hover { color: var(--text-hi); }
.toggle button[aria-pressed="true"] { background: var(--accent); color: var(--ink-900); }

.cmp {
  width: 100%;
  /* Scroll inside the wrapper on narrow screens rather than crushing four
     columns into 342px — every cell would wrap to three lines. */
  min-width: 640px;
  border-collapse: collapse;
  font-size: var(--t-small);
}
.cmp caption { text-align: left; padding-bottom: var(--s-5); color: var(--text-lo); font-size: var(--t-small); }
.cmp th, .cmp td { padding: var(--s-4) var(--s-4); border-bottom: 1px solid var(--hairline); text-align: left; }
.cmp thead th {
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-lo);
  border-bottom-color: var(--hairline-hi);
}
.cmp tbody th { font-weight: 400; color: var(--text-mid); }
.cmp td { color: var(--text-hi); font-family: var(--font-mono); font-size: var(--t-code); }
.cmp tbody tr { transition: background-color 200ms var(--ease); }
.cmp tbody tr:hover { background: rgba(255, 255, 255, 0.02); }
.cmp .col-hi { background: var(--accent-wash); }

/* ------------------------------------------------------------- 15b. Funding */

/* The federal-programs figure. An azure wash rather than a plane from the
   ink ramp: the figure is data, and azure is the system's data-emphasis
   colour (orange is reserved for actions). --azure-hi reads 5.83:1 on the
   composited wash, --text-mid 7.02:1 — both clear AA on the tinted field,
   which is lighter than --ink-900 and therefore the case that matters. */
.fund-figure {
  background: var(--azure-wash);
  border: 1px solid rgba(27, 146, 167, 0.28);
  border-radius: var(--radius-lg);
  padding: var(--s-6);
}

/* Mirrors .stat-value, including its deliberate absence of `gap` — the
   currency glyph is its own flex item and any gap sets it adrift ("$ 350"). */
.fund-value {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(3rem, 6vw, 4.5rem);
  /* Compact component: inheriting the body's 1.7 would open a 72px numeral
     into a 122px box and push the caption off the card. */
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--text-hi);
  display: flex;
  align-items: baseline;
}

/* Baseline alignment drops the smaller glyph to the numerals' baseline; the
   lift brings its cap height back up to theirs. In em of its own size, so it
   tracks the clamp instead of drifting at one viewport. */
.fund-cur {
  font-size: 0.42em;
  color: var(--azure-hi);
  transform: translateY(-0.86em);
}

.fund-mag { color: var(--azure-hi); }

/* A citation, not a call to action — so it drops .link-arrow's uppercase
   micro-label treatment and stays sentence case at caption size. The arrow
   nudge and focus ring come along unchanged.

   Caption size means --t-small, the ladder's caption rung, which is also what
   the .small caption directly above it in the markup uses — a citation and the
   line it cites should not disagree by 2px. It was 0.8125rem, a literal that
   matched neither. */
.fund-figure .link-arrow {
  color: var(--azure-hi);
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
}
.fund-figure .link-arrow:hover { color: var(--text-hi); }

/* Not .tier-price — that is 3.25rem, built for a pricing hero. These are
   card headings at h3 weight. line-height: 1 for the same reason as above.

   --t-h3-sm, the same rung as .paper-note-title and .cyber-pull: it is the only
   heading in its card, so it is an inset title like those two. It was 1.375rem,
   which left four peer card headings sitting at 20/20/20/22px. */
.fund-tier-price {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-h3-sm);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--azure-hi);
  margin: 0;
}

/* Plain rows — no markers, no check icons. Distinct from .feature-list,
   which is the affordance grid for the pricing table. */
.fund-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-3);
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--text-mid);
}

/* The top tier is marked with .surface-featured (§6 Surfaces), shared with the
   final deployment phase card. Its price stays azure like the other two tiers —
   the amber border and wash carry the emphasis on their own. */

/* ------------------------------------------------------------------ 16. FAQ */

.faq { border-top: 1px solid var(--hairline); }
.faq details { border-bottom: 1px solid var(--hairline); }
.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  padding: var(--s-5) 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-h4);
  letter-spacing: -0.015em;
  color: var(--text-hi);
  transition: color 200ms var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--accent); }
.faq summary::after {
  content: "";
  flex: none;
  width: 11px;
  height: 11px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform 300ms var(--ease);
}
.faq details[open] summary::after { transform: rotate(225deg) translateY(-2px); }
.faq .faq-body { padding-bottom: var(--s-5); max-width: 60ch; }

/* --------------------------------------------------------------- 17. Footer */

.foot-link {
  display: inline-block;
  font-size: var(--t-small);
  color: var(--text-mid);
  text-decoration: none;
  padding-block: 5px;
  transition: color 200ms var(--ease), transform 200ms var(--ease);
}
.foot-link:hover { color: var(--azure-hi); transform: translateX(3px); }

.foot-head {
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-lo);
  margin: 0 0 var(--s-4);
}

/* ------------------------------------------------------------ 18. VEX hero */

/* A self-contained block. It borrows nothing from the CEIN system above and
   nothing above reaches into it, so the two can't regress each other. */

.vex-hero {
  position: relative;
  /* Above body::after grain (z-index 60), so this section supplies its own
     atmosphere: the ::after scrim below, and nothing else. body::before
     (z-index -2) sits behind the opaque video. Both page-wide layers stay
     live over every section below. */
  z-index: 70;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: #000;
  font-family: "Inter", sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* svh keeps the bottom row off the mobile URL bar. Separate declaration so
   browsers without the unit keep the vh value above. */
@supports (height: 100svh) {
  .vex-hero { height: 100svh; }
}

/* The footage carries a CEIN lockup burned into its top-right corner, starting at
   ~83.6% of the frame width. Left unhandled it renders a second, blurry lockup
   bleeding past the glass bar, opposite the real one — read as a bug, not brand.

   Overhanging the element 22% past the hero's right edge and letting
   .vex-hero's overflow: hidden clip it removes that corner at every aspect
   ratio: when the hero is wider than 16:9 the video fills the element width
   exactly, so the visible slice is the leftmost 82% of the frame; when it is
   taller, cover crops the sides further still. Costs ~11% off the top and
   bottom of the frame, which the scrim already dims. */
.vex-hero__video {
  position: absolute;
  top: 0;
  left: 0;
  /* 122%, stated as a width rather than a negative `right`: a replaced element
     doesn't stretch between offsets, it takes its intrinsic size, so `right`
     alone is silently ignored. max-width likewise has to be released — Tailwind's
     preflight caps video at 100% and would clamp this straight back. */
  width: 122%;
  max-width: none;
  height: 100%;
  object-fit: cover;
}

/* Legibility scrim. The footage is a sunset city — bright sky behind the navbar,
   lit facades and teal arcs behind the copy — and it moves, so white text on raw
   frames swings in and out of contrast. Bottom-weighted rather than a flat veil:
   the copy lives in the lower half and the sky reads as a city either way.

   Sits over the video (position: absolute, z-auto) and under both content rows
   (relative z-10). Neutral black only — an ink or azure cast would shift the
   amber CTA and the --azure-hi headline line off their brand values. */
.vex-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    /* top: carries the glass navbar over bright sky */
    linear-gradient(to bottom, rgba(0, 0, 0, 0.45) 0, rgba(0, 0, 0, 0) 24%),
    var(--hero-scrim),
    /* flat base, so a mid-height bright frame never fully washes out */
    rgba(0, 0, 0, 0.24);
}

/* Two-column row: copy stops around 42% up, so the ramp can clear early and
   leave the skyline nearly raw. */
.vex-hero { --hero-scrim: linear-gradient(to top,
    rgba(0, 0, 0, 0.78) 0, rgba(0, 0, 0, 0.56) 22%, rgba(0, 0, 0, 0) 60%); }

/* Below lg the grid stacks, the lede reflows to six lines and the block reaches
   past mid-viewport — the desktop ramp has already faded to nothing there.
   Longer, gentler ramp instead of a heavier one, so the sky is still visible. */
@media (max-width: 1023px) {
  .vex-hero { --hero-scrim: linear-gradient(to top,
      rgba(0, 0, 0, 0.8) 0, rgba(0, 0, 0, 0.62) 28%,
      rgba(0, 0, 0, 0.4) 58%, rgba(0, 0, 0, 0) 92%); }
}

.liquid-glass {
  background: rgba(0, 0, 0, 0.4);
  background-blend-mode: luminosity;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: none;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
  position: relative;
  overflow: hidden;
}

.liquid-glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.4px;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.1) 20%,
    rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 60%,
    rgba(255, 255, 255, 0.1) 80%, rgba(255, 255, 255, 0.3) 100%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* Entrance primitives. JS adds .is-in; without JS both stay visible, because
   the hidden state is only applied once [data-vex-anim] is set on the root. */

[data-vex-anim] .vex-fade {
  opacity: 0;
  transition: opacity var(--fade-dur, 1000ms) ease;
}
[data-vex-anim] .vex-fade.is-in { opacity: 1; }

[data-vex-anim] .vex-char {
  display: inline-block;
  opacity: 0;
  transform: translateX(-18px);
  transition: opacity 500ms ease, transform 500ms ease;
}
[data-vex-anim] .vex-char.is-in { opacity: 1; transform: translateX(0); }

.vex-line { display: block; }

/* Keeps a word from breaking between its own inline-block characters. */
.vex-word { display: inline-block; white-space: nowrap; }

/* Word grouping can't span an element boundary, and each line's closing full
   stop lives in its own accent-coloured span. This keeps the two together so
   a wrap never strands the stop on its own line. */
.vex-nb { white-space: nowrap; }

/* Brand tagline lockup — see brand_assets/brand.md. Colour and weight are
   fixed by the brand, not chosen here. */
.vex-tagline {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--accent);
  line-height: 1.15;
  letter-spacing: -0.01em;
}

/* Brand --t-display is clamp(2.5rem, 6.5vw, 5.5rem). Only the floor moves:
   at 2.5rem "Connected Infrastructure." is wider than a 390px screen's
   gutters allow and wraps. Coefficient and 5.5rem ceiling are the brand's —
   brand.md caps display there deliberately. */
.vex-hero .display { font-size: clamp(1.75rem, 6.5vw, 5.5rem); }

/* ----------------------------------------------------------- 18b. Paper band */

/* Two light bands inside a dark-only page: a paper ground carrying the two
   capability rows, then a teal gradient band for cybersecurity. Ported from the
   light CEIN build to match it exactly, so a few things here deliberately break
   this file's own rules — noted at each site:

     · the band's vertical rhythm uses the light build's 5rem/7rem/4.5rem/6rem
       literals, not the --s-* ladder;
     · headings and eyebrows carry their own colours rather than --text-hi /
       .label, because amber on #fafbfb is 1.71:1 and unusable as text;
     · elevation goes through --lift-2, since the black-based --shadow-* tokens
       are invisible on paper.

   Both bands are opaque, so they paint over the fixed body::before mesh. The
   fixed body::after grain still passes over them at 3.5% overlay — the same
   texture the light build applies with its own .grain. */

.paper {
  position: relative;
  background: var(--paper);
  color: var(--ink-900);
  line-height: 1.7;
  /* Self-contained stacking context: the shot shadows are the only elevation
     inside the band and must not interleave with the dark system's. */
  isolation: isolate;
  /* Not .section-lg. That class is subject to the `section + .section-lg` trim
     in §4, which would crop 48px off the top of an opaque colour band and read
     as a mistake. Symmetric padding, owned here. */
  padding-block: 5rem;
}

.cyber-band {
  position: relative;
  overflow: hidden;
  padding-block: 4.5rem;
  background: linear-gradient(
    100deg,
    var(--teal-deep) 0%,
    var(--teal-deep) 34%,
    var(--azure) 100%
  );
}

/* Same reason as .paper above — and the section after the band needs its full
   opening padding back, or Funding starts 48px under a hard colour edge. */
.cyber-band + .section,
.cyber-band + .section-lg { padding-top: var(--s-9); }

@media (min-width: 768px) {
  .paper { padding-block: 7rem; }
  .cyber-band { padding-block: 6rem; }
}

@media (min-width: 1024px) {
  .cyber-band + .section,
  .cyber-band + .section-lg { padding-top: var(--s-10); }
}

/* Amber bloom top-right, teal-ink weight bottom-left, so the flat gradient gains
   a light source. Separate layer rather than more stops on .cyber-band: it sits
   above the background and below the content. */
.cyber-band__wash {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(58% 76% at 90% 20%, rgba(254, 185, 3, 0.20), transparent 62%),
    radial-gradient(56% 68% at 4% 92%, rgba(7, 47, 56, 0.50), transparent 66%);
}

.cyber-band__inner { position: relative; z-index: 2; }

/* Row stack. 5rem/7rem, off-ladder — see the section note. */
.paper-rows { display: grid; gap: 5rem; }

@media (min-width: 768px) {
  .paper-rows { gap: 7rem; }
}

/* ── Figure slot ──────────────────────────────────────────────────────────── */

/* A framed plate, 20px radius against the band's square edges. The light build
   fills this with a photograph under an azure multiply tint; here it holds a
   house diagram on a solid teal-deep plate instead, because every diagram in
   images/ is drawn for dark ground (near-white strokes at low alpha) and would
   disappear on paper. Same geometry, same apparent colour. */
.shot {
  position: relative;
  overflow: hidden;
  border-radius: 20px;
  box-shadow: var(--lift-2);
}

.shot img,
.shot svg { transition: transform 800ms var(--spring); }

.shot:hover img,
.shot:hover svg { transform: scale(1.045); }

/* Deep at the top falling to teal-ink at the bottom, which does two jobs: it
   tracks the light build's photo treatment (azure multiply, then a scrim
   weighting the lower edge) and it drops the plate's luminance to roughly
   --ink-800, the ground these diagrams were drawn against. On flat --teal-deep
   the near-white strokes at 0.10–0.55 alpha washed out. */
.shot-panel {
  display: grid;
  place-items: center;
  padding: var(--s-6);
  background: linear-gradient(180deg, var(--teal-deep) 0%, var(--teal-ink) 100%);
}

/* contain, not the light build's cover: a diagram cropped to fill would lose its
   edges, where a photograph would not care. */
.shot-panel img,
.shot-panel svg {
  width: 100%;
  height: auto;
  object-fit: contain;
}

/* ── Paper typography ─────────────────────────────────────────────────────── */

/* The eyebrow on paper. Same metrics as .label, but teal-deep and without the
   leading hairline — .label's amber is unreadable here. */
.tag-paper {
  display: block;
  font-family: var(--font-display);
  font-size: var(--t-label);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--teal-deep);
  margin: 0;
}

/* Row heading. Sits below .h2 in scale — these are rows within a band, not
   section openers.

   1.02/-0.03em, and no text-wrap: balance. All three come from measuring the
   light build rather than reading it: its `h3,h4 { line-height: 1.12 }` never
   applies to these headings, because the same elements carry .font-display
   (line-height 1.02, letter-spacing -.03em) and a class beats a type selector.
   Balance is likewise absent there — with it on, an 18ch measure splits into two
   near-equal lines instead of filling the first, so "AI is only as powerful as /
   the infrastructure" came out as "AI is only as powerful / as the
   infrastructure". Don't add it back. */
.paper-h {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 3vw, 2.3rem);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--ink-900);
  max-width: 18ch;
  margin: 0;
}

/* 1.5, like every other text role in these two bands. The light build sets its
   body to 1.7, but each of these roles is sized with a Tailwind `text-[…]`
   utility, which pairs its own 1.5 line-height and wins over the inherited
   value. Measured, not assumed — 17px copy resolves to a 25.5px line box there
   and a 28.9px one if you let the band's 1.7 through. */
.copy-paper {
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--ink-on-paper);
  max-width: 36rem;
  margin: 0;
}

/* ── Paper lists and notes ────────────────────────────────────────────────── */

.paper-list {
  display: grid;
  gap: var(--s-3) var(--s-6);
  list-style: none;
  padding: 0;
  margin: 0;
}

.paper-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  /* --t-small, like every other short-phrase list in the system (.fund-list,
     .feature-list li). It was 1rem — 16px, a rung that does not exist, and the
     one size on the page that agreed with nothing. */
  font-size: var(--t-small);
  /* 1.5, not the band's inherited 1.7: measured against the light build, whose
     rows sit on a 24px line box for a 36px pitch with the 12px gap. At 1.7 the
     pitch grows to 39px and the list no longer lines up with its figure. */
  line-height: 1.5;
  color: var(--ink-on-paper);
}

/* Decorative, so amber is fine here where amber text would not be. */
.paper-list li::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  margin-top: 9px;
  border-radius: 999px;
  background: var(--accent);
}

.paper-note { border-left: 2px solid var(--azure); padding-left: 20px; }
.paper-note-teal { border-left-color: rgba(27, 146, 167, 0.25); }
.paper-note-amber { border-left-color: rgba(254, 185, 3, 0.55); }

.paper-note-title {
  font-family: var(--font-display);
  font-size: var(--t-h3-sm);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--teal-deep);
}

.paper-note-body {
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-on-paper);
  margin: var(--s-2) 0 0;
}

/* Colour override only — .link-arrow owns the geometry and the arrow nudge. */
.link-arrow-paper { color: var(--teal-deep); }
.link-arrow-paper:hover { color: var(--azure); }

/* ── Teal band typography ─────────────────────────────────────────────────── */

/* Larger than .paper-h — this one opens a section rather than a row — but well
   under .h2, which at this viewport would run to 68px and overpower the band. */
/* Same 1.02/-0.03em and no-balance reasoning as .paper-h. */
.cyber-h {
  font-family: var(--font-display);
  font-size: clamp(1.85rem, 3.8vw, 2.75rem);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--text-hi);
  max-width: 18ch;
  margin: 0;
}

/* Owned here rather than as `text-white/86` on the element: that utility came
   back unapplied in this build and the paragraph inherited --text-mid, reading
   grey-blue on the gradient instead of white. Same 1.5 line box as .copy-paper.

   Solid --text-hi, not the 0.86 alpha it carried before. This band is the only
   mid-luminance ground on the page — white is 9.2:1 over --teal-deep but just
   3.7:1 over the --azure end of the gradient, so the copy has no contrast to
   spare and should not be spending it on an alpha. The column this sits in is
   held at flat teal-deep by the gradient's 34% stop. */
.cyber-copy {
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--text-hi);
  max-width: 34rem;
  margin: 0;
}

.cyber-pull {
  font-family: var(--font-display);
  font-size: var(--t-h3-sm);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--accent);
  margin: 0;
}

/* ── Chips on the teal band ───────────────────────────────────────────────── */

/* Not .pill: that is an 11px uppercase marker for dark ink surfaces, while these
   are sentence-case labels reading as a vocabulary list. At --t-small — it was
   0.875rem, the only 14px on the site and off the ladder entirely.

   The plate is teal-ink, not the rgba(255,255,255,0.10) it was. These chips are
   the only text that crosses the full width of the gradient, so they land on the
   --azure end under the amber bloom — the band's lightest point. A 10% white
   plate there lifted the ground to ≈2.8:1 against its own white label. Tinting
   down instead of lightening the text is the only move available: this band's
   heading and copy are white, so a dark label would read as a different system.
   Darkening the ground holds every chip near 5.4:1 wherever it falls. */
.chip-solid {
  display: inline-flex;
  align-items: center;
  padding: var(--s-2) var(--s-4);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 999px;
  background: rgba(7, 47, 56, 0.38);
  font-family: var(--font-body);
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--text-hi);
}

/* Focus rings default to amber, which lands on amber inside this band. Switch
   to white so the ring survives the gradient. */
.cyber-band :where(a, button, [tabindex]):focus-visible {
  outline-color: var(--text-hi);
}

/* -------------------------------------------------------- 19. Reduced motion */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  /* Content must never be left hidden by a disabled reveal. */
  [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
  }

  /* Same guarantee for the hero. The script also refuses to arm the stagger
     under this query, so this only catches a mid-session preference change. */
  .vex-fade, .vex-char {
    opacity: 1 !important;
    transform: none !important;
  }

  /* The blanket rule above would settle the loop on its last keyframe (-50%,
     mid-phrase). State the rest offset explicitly instead. */
  .marquee-track {
    animation: none !important;
    transform: translate3d(-4%, 0, 0) !important;
  }

  /* The blanket rule above settles every animation on its final keyframe, which
     for the topology is wrong twice over: topo-travel ends with the dash parked
     at the path start (three stranded amber ticks) and topo-breathe ends at
     opacity 0.25 (invisible squares). State both end states explicitly instead —
     routes as dim continuous lines, nodes lit but still. */
  .topo-pulse {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
    opacity: 0.35;
  }

  .topo-blink, .topo-blink-b { opacity: 0.75 !important; }
}
