/* ============================================================
   RAIL Technologies — stylesheet
   Palette + type sampled from client logo (RAIL_Website_Build_Spec.md)
   ============================================================ */

:root {
  --ink:        #0B1F3D;   /* deep navy */
  --ink-deep:   #071628;   /* darker navy for full-bleed sections */
  --royal:      #1447E6;   /* brand blue (dark) */
  --sky:        #4C8DFF;   /* brand blue (light) */
  --sky-soft:   #7DB0FF;
  --paper:      #FAFBFC;   /* off-white */
  --paper-2:    #EAEFF5;   /* alternating section tint */
  --slate:      #414A5A;   /* body copy */
  /* A1: was #6B7688 = 4.59:1 on white — the bare AA minimum, and the colour of every
     card paragraph, .field label, .proof__label and .adv p. #57617A lifts that to ~6.2:1
     on #fff / ~5.4:1 on --paper-2. Colour-only change: it cannot move a box. */
  --slate-soft: #57617A;
  --line:       #D8E0EA;
  --line-strong:#C3CEDC;
  --line-dark:  rgba(255,255,255,.12);
  --card-shadow: 0 1px 2px rgba(11,31,61,.05), 0 18px 36px -26px rgba(11,31,61,.35);
  --card-shadow-hover: 0 2px 4px rgba(11,31,61,.06), 0 30px 56px -30px rgba(11,31,61,.5);

  --grad:       linear-gradient(118deg, var(--royal) 0%, var(--sky) 100%);
  --grad-soft:  linear-gradient(118deg, rgba(20,71,230,.14), rgba(76,141,255,.14));

  --font-display: 'Space Grotesk', system-ui, sans-serif;
  --font-body:    'Inter', system-ui, sans-serif;
  --font-mono:    'Space Mono', ui-monospace, monospace;

  --wrap: 1180px;
  --ease: cubic-bezier(.16, 1, .3, 1);
  --radius: 16px;
  --nav-h: 74px;
}

/* ---------- reset-ish ---------- */
* { box-sizing: border-box; }
html {
  scroll-behavior: smooth; -webkit-text-size-adjust: 100%; overflow-x: hidden;
  /* The root MUST carry a background of its own. With it unset, <body>'s
     --paper propagates to the document canvas, and iOS Safari tints its
     bottom toolbar from the canvas — which put a white bar under a page
     whose first and last screens are both near-black. Giving html its own
     colour stops body's from propagating (CSS backgrounds §2.11.2), so the
     canvas, the rubber-band overscroll and Safari's chrome all go dark.
     Nothing on screen changes: <body> still paints --paper over the whole
     document. Keep this in sync with <meta name="theme-color">. */
  background: var(--ink-deep);
  /* The actual cause of the white bar under iOS Safari's toolbar. With no
     color-scheme declared, Safari treats the document as light and paints its
     own chrome — the bottom toolbar backdrop and the safe-area fill — light,
     regardless of the canvas colour or theme-color. Declaring the scheme is
     what lets it render that furniture dark. */
  color-scheme: dark;
}
/* ...but color-scheme is inherited and also drives UA-rendered controls, so the
   light sections keep the light one. Chrome tint is decided by the ROOT only,
   so this cannot undo the fix above — it just stops a <select> on a white card
   from opening a black popup. */
.section:not(.dark) { color-scheme: light; }
body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--slate);
  /* Dark, NOT --paper. iOS Safari tints its bottom toolbar from <body>'s
     background-color specifically — not the canvas, not theme-color, not
     color-scheme (all three were tried and none moved it). A near-white body
     is what put a white bar under the page. apollopay.io has no theme-color,
     no color-scheme and no viewport-fit, and shows no bar, because its body
     is dark; that was the tell.
     Safe to do here because every section on both pages paints its own
     background, so nothing was relying on the body colour showing through —
     verified section by section before changing it. */
  background: var(--ink-deep);
  line-height: 1.6;
  /* NB: no overflow-x here — it makes <body> a scroll container, which breaks
     position:sticky for descendants (the sticky footer reveal). html still clips. */
  -webkit-font-smoothing: antialiased;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
h1, h2, h3 { font-family: var(--font-display); color: var(--ink); line-height: 1.05; margin: 0; letter-spacing: -.02em; }
p { margin: 0; }
::selection { background: var(--sky); color: #fff; }

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 24px; }

.grad-text {
  background: var(--grad);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: .74rem; letter-spacing: .22em; text-transform: uppercase;
  color: var(--royal); margin: 0 0 20px;
}
.dark .eyebrow { color: var(--sky-soft); }

.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 200;
  background: var(--ink); color: #fff; padding: 12px 18px; border-radius: 0 0 10px 0;
}
.skip-link:focus { left: 0; }

/* A7: a flat 6px drew a rounded rect around pill buttons and square-ish inputs. `inherit`
   lets each control keep its own shape so the ring hugs it. */
:focus-visible { outline: 3px solid var(--sky); outline-offset: 3px; border-radius: inherit; }
/* .skip-link is the ONE element whose own border-radius is declared above this rule, so it is
   the only one the line above can clobber (everything else — .btn 999px, .field input 11px —
   is declared later and wins). Restate its designed corner. */
.skip-link:focus-visible { border-radius: 0 0 10px 0; }

/* ============================================================
   Scroll-progress rail — the "current" through the system
   ============================================================ */
.rail-spine {
  position: fixed; left: 0; top: 0; bottom: 0; width: 4px;
  background: rgba(11,31,61,.06); z-index: 90;
}
.rail-fill {
  display: block; width: 100%; height: 0;
  background: var(--grad);
  box-shadow: 0 0 18px rgba(76,141,255,.6);
}

/* ============================================================
   Buttons
   ============================================================ */
.btn {
  --pad: 12px 22px;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-mono); font-size: .78rem; letter-spacing: .12em; text-transform: uppercase;
  padding: var(--pad); border-radius: 999px; border: 1.5px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: transform .4s var(--ease), box-shadow .4s var(--ease), background-position .5s var(--ease), color .3s, border-color .3s;
}
.btn--solid {
  color: #fff; background: var(--grad); background-size: 160% 160%; background-position: 0% 50%;
  box-shadow: 0 8px 22px -8px rgba(20,71,230,.65);
}
.btn--solid:hover { background-position: 100% 50%; transform: translateY(-2px); box-shadow: 0 14px 30px -8px rgba(20,71,230,.7); }
.btn--ghost { color: var(--ink); border-color: var(--line); background: transparent; }
.btn--ghost:hover { border-color: var(--royal); color: var(--royal); transform: translateY(-2px); }
.dark .btn--ghost { color: #fff; border-color: rgba(255,255,255,.28); }
.dark .btn--ghost:hover { border-color: var(--sky); color: var(--sky-soft); }
/* A7: pill buttons carry a solid fill / border, so the ring needs to clear them */
.btn:focus-visible { outline-offset: 4px; }
.btn--lg { --pad: 16px 30px; font-size: .82rem; }
.btn--block { width: 100%; }

/* ============================================================
   Navigation
   ============================================================ */
.nav {
  position: fixed; inset: 0 0 auto 0; height: var(--nav-h); z-index: 100;
  display: flex; align-items: center; gap: 28px;
  padding: 0 clamp(20px, 4vw, 44px);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: border-color .4s, background .4s, box-shadow .4s;
}
/* Liquid glass, one appearance everywhere. It used to flip to a light fill with dark text
   once scrolled; now it stays translucent so the section behind reads through it. The tint
   is dark rather than clear because the bar sits over both paper and ink sections, and white
   nav text needs something behind it to stay legible over the light ones. */
.nav {
  background: rgba(9, 18, 34, .42);
  -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6);
  border-bottom: 1px solid rgba(255,255,255,.09);
  box-shadow: 0 1px 0 rgba(255,255,255,.05) inset, 0 10px 30px -22px rgba(0,0,0,.7);
}
.nav.is-scrolled { background: rgba(9, 18, 34, .58); }
/* Over the light sections (#capabilities settled, #platform-in-one, #traction,
   #specs, #voices) even .58 composites to a muddy mid-gray and the pale links
   lose their contrast against it. script.js toggles .nav--on-light from a rect
   check in onScroll(); deepen the glass there so the bar reads as crisp
   white-on-ink in both contexts. Listed after .is-scrolled so it wins. */
.nav.nav--on-light, .nav.nav--on-light.is-scrolled { background: rgba(9, 18, 34, .8); }

.brand { display: inline-flex; align-items: center; gap: 12px; color: #fff; }
.brand__mark { height: 30px; width: auto; display: block; transition: transform .5s var(--ease); }
.brand:hover .brand__mark { transform: translate(2px,-2px); }
.brand__word { font-family: var(--font-display); font-weight: 700; font-size: 1.34rem; letter-spacing: .06em; }
/* logo swaps: white knockout over dark (top / footer), full colour on light scrolled nav */
.brand__mark--color { display: none; }

.nav__links { display: flex; gap: 26px; margin-left: auto; }
.nav__links a {
  font-family: var(--font-mono); font-size: .74rem; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,.82); position: relative; padding: 6px 0;
}
.nav__links a.is-active { color: #fff; }
.nav__links a.is-active::after { width: 100%; }
.nav__links a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 0;
  background: var(--grad); transition: width .4s var(--ease);
}
.nav__links a:hover { color: #fff; }
.nav__links a:hover::after { width: 100%; }

.nav .btn--ghost { color: #fff; border-color: rgba(255,255,255,.28); }
.nav .btn--ghost:hover { border-color: var(--sky); color: var(--sky-soft); }

.nav__cta { display: flex; gap: 10px; }
.nav__toggle { display: none; }

/* 921–1150px: the full desktop nav (brand + 5 links + 2 CTAs) is wider than the bar.
   Default flex-shrink let .brand and the link boxes collapse while their text kept its
   painted width, so "RAIL" drew over "KIOSK". Pin every piece (no shrink) and tighten
   spacing/type so the row genuinely fits all the way down to the 920px hamburger cutover. */
@media (min-width: 921px) and (max-width: 1150px) {
  .nav { gap: 16px; padding: 0 20px; }
  .brand, .nav__links, .nav__cta { flex-shrink: 0; }
  .brand__word { font-size: 1.15rem; }
  .nav__links { gap: 14px; }
  .nav__links a { font-size: .68rem; letter-spacing: .08em; }
  .nav__cta { gap: 8px; }
  .nav__cta .btn { --pad: 10px 14px; font-size: .7rem; letter-spacing: .08em; }
}

/* ============================================================
   Section shell
   ============================================================ */
.section { position: relative; padding: clamp(64px, 7.5vw, 104px) 0; }
/* clear divider between consecutive sections */
.section + .section { border-top: 1px solid var(--line); }
.section.dark + .section, .section + .section.dark { border-top-color: rgba(255,255,255,.09); }

/* ---- scroll-linked entrances: each section eases in from its own direction ----
   --e is written by script.js (0 as the section's top clears the fold, 1 once it's a third
   up the screen). transform + opacity only, so this stays on the compositor. */
/* --------------------------------------------------------------
   Cinematic entrances — depth, not just a slide
   Each section arrives through 3D space: it swings/lifts on an axis, rides in
   from depth on Z, and its inner layers resolve at different rates. --e (0..1)
   is written by script.js from the section's position in the fold.
   -------------------------------------------------------------- */
/* The section itself is NEVER transformed — it has to keep painting its colour block
   edge-to-edge, or sliding it exposes bare page background down the side (and on mobile
   runs straight into html{overflow-x:hidden} and clips copy mid-word). Only the content
   layers travel, and they sit inside the gutter so nothing can be exposed or cut. */
[data-enter] { --e: 1; --i: calc(1 - var(--e)); }
[data-enter] > * { opacity: calc(.6 + .4 * var(--e)); }
[data-enter].is-near > * { will-change: transform, opacity; }

/* Angles and Z kept modest: a 1440px plate at 11deg / -260px keystones hard enough to read
   as a bug. The rotate origins sit BELOW/OUTSIDE the element so it recedes rather than
   hinging on its own edge like a flap. */
/* Pivots are an ABSOLUTE distance below the element, not a percentage of its own height:
   with `50% 130%` the arc length scales with section height, so a 1062px section swung
   twice as far as a 589px one off the same tag and they stopped feeling like one system. */
[data-enter="left"]  > * { transform: perspective(1200px) translate3d(calc(var(--i) * -80px), 0, calc(var(--i) * -170px)) rotateY(calc(var(--i) * 7deg));  transform-origin: 20% 50%; }
[data-enter="right"] > * { transform: perspective(1200px) translate3d(calc(var(--i) *  80px), 0, calc(var(--i) * -170px)) rotateY(calc(var(--i) * -7deg)); transform-origin: 80% 50%; }
[data-enter="rise"]  > * { transform: perspective(1200px) translate3d(0, calc(var(--i) * 96px), calc(var(--i) * -190px)) rotateX(calc(var(--i) * 6deg)); transform-origin: 50% calc(100% + 300px); }
/* tilt is mostly depth + pitch with almost no vertical travel, so it reads as a distinctly
   different move from rise rather than a slightly smaller version of it */
[data-enter="tilt"]  > * { transform: perspective(1200px) translate3d(0, calc(var(--i) * 20px), calc(var(--i) * -220px)) rotateX(calc(var(--i) * 9deg)); transform-origin: 50% calc(100% + 360px); }

/* Inner layers ride the same --e at different rates — that difference is the parallax */
/* Scoped to DESCENDANTS of a plate, never the plate itself — otherwise a direct-child
   .beats/.fcar matched this instead of its transform and the section tore in two
   (126px apart at different depths, and the head block colliding with .beats). */
[data-enter] > * :is(.feature-grid, .pillars, .proof, .beam, .fcar, .triad,
                     .whynow, .beats, .adv-grid, .reserve__form) {
  transform: translate3d(0, calc(var(--i) * -30px), 0);
}
/* head moves against the body so it settles FIRST, which is the point of the layering */
[data-enter] > * .section-head { transform: translate3d(0, calc(var(--i) * -26px), 0); }

/* Belt and braces for every width: a mid-swing child can never grow the document.
   overflow-y stays visible (clip is the only value that pairs with visible), so
   nothing vertical is cut — only the transient horizontal overhang. */
[data-enter] { overflow-x: clip; overflow-y: visible; }

/* Below 1280px the horizontal swings become vertical. The 1180px wrap plus the ±56px
   swing is 1236px — on any narrower window a mid-swing section overhangs the viewport,
   and because --e latches until the next scroll it can SIT visibly off-centre (the
   client's "everything looks off centered" report). Above 1280 the swing always fits. */
@media (max-width: 1439px) {
  [data-enter="left"] > *, [data-enter="right"] > * {
    transform: perspective(1000px) translate3d(0, calc(var(--i) * 40px), calc(var(--i) * -70px));
    transform-origin: 50% 120%;
  }
  [data-enter] > * :is(.feature-grid, .pillars, .proof, .beam, .fcar, .triad,
                       .whynow, .beats, .adv-grid, .reserve__form) { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Children only — bare [data-enter] would kill ScrollTrigger's inline pin transforms. */
  [data-enter] > .wrap, [data-enter] * { transform: none !important; opacity: 1 !important; filter: none !important; }
}
.section--kiosks   { background: var(--paper); }
.section--verticals{ background: var(--paper-2); }
.section--investors{ background: var(--paper); }
.section--story    { background: var(--paper-2); }
.section--advantage{ background: var(--paper-2); }
.section--reserve  { background: var(--paper); }

/* dark full-bleed sections */
.section--ecosystem, .section--vision { background: var(--ink-deep); color: rgba(255,255,255,.82); }
.section--ecosystem h2, .section--vision h2,
.section--ecosystem h3, .section--vision h3 { color: #fff; }
.dark { color: rgba(255,255,255,.82); }

.section-head { font-size: clamp(1.9rem, 4vw, 3rem); max-width: 16ch; margin-bottom: 22px; }
.section--ecosystem .section-head, .section--vision .section-head,
.section--advantage .section-head { max-width: 20ch; }
.lede { font-size: clamp(1.05rem, 1.5vw, 1.18rem); line-height: 1.65; max-width: 58ch; color: var(--slate); }
.section--ecosystem .lede, .section--vision .lede, .section--visibility .lede,
.dark .lede { color: rgba(255,255,255,.84); }

.statement { font-size: clamp(3rem, 9vw, 7.2rem); line-height: .92; font-weight: 700; margin-bottom: 44px; }

/* ============================================================
   Reveal / motion — ramp in from lower-left to upper-right
   ============================================================ */
/* A8: will-change used to sit on the base rule, so all 36 .reveal elements held a promoted
   compositor layer forever (45 permanently-promoted layers after a full scroll). Hinting only
   while the element is still WAITING to reveal gives the same result — Chrome keeps the layer
   alive for the duration of a running transform/opacity transition — and releases it after. */
.reveal { opacity: 0; transform: translate3d(0, 22px, 0); transition: opacity 1s var(--ease), transform 1.1s var(--ease); }
.reveal:not(.is-visible) { will-change: opacity, transform; }
.reveal.is-visible { opacity: 1; transform: none; }

/* content tiles slide in with more travel + a spring settle, echoing the hero's glass motion */
.reveal--slide { transform: translate3d(-34px, 30px, 0) scale(.978); transition: opacity .72s var(--ease), transform .92s cubic-bezier(.2,.82,.26,1.08); }
.reveal--slide.is-visible { transform: none; }
/* alternate the entry direction across a row so tiles converge, not march */
.reveal--slide:nth-child(even) { transform: translate3d(34px, 30px, 0) scale(.978); }
.reveal--slide:nth-child(even).is-visible { transform: none; }

/* ============================================================
   HERO
   ============================================================ */
.hero {
  position: relative; min-height: 100vh; display: flex; flex-direction: column; justify-content: center;
  padding: calc(var(--nav-h) + 40px) 0 60px;
  background:
    radial-gradient(1100px 620px at 82% 18%, rgba(20,71,230,.28), transparent 60%),
    radial-gradient(900px 600px at 12% 92%, rgba(76,141,255,.16), transparent 55%),
    var(--ink-deep);
  color: rgba(255,255,255,.82); overflow: hidden;
}
.hero::before { /* faint rail grid */
  content: ""; position: absolute; inset: 0;
  background-image: linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px);
  background-size: 100% 48px; mask-image: linear-gradient(180deg, transparent, #000 30%, #000 70%, transparent);
  pointer-events: none;
}
.hero__grid {
  width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 24px;
  display: grid; grid-template-columns: 1.05fr .95fr; gap: 40px; align-items: center;
}
.hero__title { font-size: clamp(2.5rem, 6vw, 4.7rem); font-weight: 700; color: #fff; margin: 0 0 24px; letter-spacing: -.03em; }
.hero__sub { font-size: clamp(1.05rem, 1.7vw, 1.32rem); max-width: 46ch; color: rgba(255,255,255,.74); margin-bottom: 34px; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 14px; }

/* editorial part labels — technical-diagram callouts pointing to each part */
.kiosk-labels { position: absolute; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
.klabel { position: absolute; top: 0; left: 0; display: flex; align-items: center; opacity: 0; will-change: transform, opacity; transition: opacity .35s var(--ease); }
.klabel__txt { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: #eef2f8; white-space: nowrap; padding: 7px 11px; border: 1px solid rgba(255,255,255,.16); border-radius: 9px; background: rgba(18,20,26,.5); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.klabel__line { width: 62px; height: 1px; background: linear-gradient(90deg, rgba(255,255,255,.12), rgba(255,255,255,.72)); }
.klabel__dot { width: 7px; height: 7px; border-radius: 50%; background: #fff; box-shadow: 0 0 9px rgba(120,170,255,.95); margin-left: -3px; }

/* the teardown page is a neutral editorial studio (anime.js "blend"): a grey
   backdrop behind the transparent 3D canvas, keeping the kiosk's LED accents */
.hero.dark.hero--teardown {
  background:
    radial-gradient(1100px 760px at 68% 34%, #232529, transparent 62%),
    linear-gradient(180deg, #141519, #0d0e11);
  color: rgba(255,255,255,.82);
}
/* --- kiosk deconstruction: crossfade steps in the pinned left column --- */
.hero--teardown .hero__copy { position: relative; min-height: 58vh; }
/* The intro step carries the full brand headline, which runs much longer than the five
   part-step headlines. The steps are absolutely positioned and vertically centred, so an
   over-tall step 0 bleeds past the nav and off the bottom of the pinned viewport — it needs
   its own (smaller) type scale. Scoped to step 0 so the part steps keep their exact look. */
/* The intro headline runs much longer than the five part-step headlines, and the steps are
   absolutely positioned + vertically centred, so an over-tall step 0 bleeds past the nav and
   off the bottom of the pinned viewport. Its own scale, scoped to step 0. */
.hero--teardown .hero__step[data-step="0"] .hero__title { font-size: clamp(2rem, 3.9vw, 3.5rem); }
.hero--teardown .hero__step[data-step="0"] .hero__sub { font-size: clamp(.98rem, 1.15vw, 1.06rem); max-width: 40ch; }
.hero--teardown .hero__step {
  position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%) translateX(10px);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .55s var(--ease), transform .65s var(--ease);
}
.hero--teardown .hero__step.is-active { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(-50%); }
/* staggered child reveal — each line of the active step cascades in */
body.has-kiosk-scroll .hero--teardown .hero__step > * { opacity: 0; transform: translateY(16px); transition: opacity .5s var(--ease), transform .6s var(--ease); }
body.has-kiosk-scroll .hero--teardown .hero__step.is-active > * { opacity: 1; transform: none; }
body.has-kiosk-scroll .hero--teardown .hero__step.is-active > *:nth-child(2) { transition-delay: .07s; }
body.has-kiosk-scroll .hero--teardown .hero__step.is-active > *:nth-child(3) { transition-delay: .14s; }
body.has-kiosk-scroll .hero--teardown .hero__step.is-active > *:nth-child(4) { transition-delay: .21s; }
body.has-kiosk-scroll .hero--teardown .hero__step.is-active > *:nth-child(5) { transition-delay: .28s; }
/* .hero__step-k (the "01 — Bill validator" kicker) and .hero__step-tag (the
   pill repeating the title at the foot of the card) were removed from the
   markup — the teardown cards now carry the blue title and the copy only. */
.hero__step-h { font-size: clamp(2.2rem, 4.6vw, 3.6rem); font-weight: 700; color: #fff; letter-spacing: -.025em; margin: 0 0 18px; max-width: 15ch; line-height: 1.05; }
.hero__step-p { font-size: clamp(1.02rem, 1.5vw, 1.22rem); color: rgba(255,255,255,.74); max-width: 44ch; margin: 0; }
.hero__teardown-cue {
  margin: 26px 0 0; font-family: var(--font-mono); font-size: .68rem; letter-spacing: .2em; text-transform: uppercase;
  color: rgba(255,255,255,.56); /* A1: .4 measured 3.81:1 — failed AA */
}
body:not(.has-kiosk-scroll) .hero__teardown-cue { display: none; }

/* --- progress dots on the visual side --- */
.hero--teardown .hero__dots { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: none; flex-direction: column; gap: 13px; z-index: 3; }
.hero--teardown .hero__dots::before { content: ""; position: absolute; left: 50%; top: 4px; bottom: 4px; width: 1px; transform: translateX(-50%); background: linear-gradient(rgba(76,141,255,.05), rgba(76,141,255,.35), rgba(76,141,255,.05)); }
body.has-kiosk-scroll .hero--teardown .hero__dots { display: flex; }
.hero__dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,.2); transition: transform .3s var(--ease), background .3s, box-shadow .3s; }
.hero__dot.is-active { background: var(--sky); box-shadow: 0 0 12px rgba(76,141,255,.85); transform: scale(1.3); }

.hero__scroll {
  position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .3em; text-transform: uppercase; color: rgba(255,255,255,.45);
}
.hero__scroll i { width: 1px; height: 40px; background: linear-gradient(var(--sky), transparent); animation: scrollpulse 2.2s var(--ease) infinite; transform-origin: top; }
@keyframes scrollpulse { 0%,100% { transform: scaleY(.3); opacity: .3; } 50% { transform: scaleY(1); opacity: 1; } }

/* --- hero visual slot (holds the 3D kiosk stage) --- */
.hero__visual { position: relative; display: flex; justify-content: center; align-items: center; min-height: 440px; }

/* --- interactive 3D kiosk model (three.js) --- */
.kioskstage { position: relative; width: 100%; height: min(80vh, 800px); }
.kioskstage canvas { display: block; }

/* desktop: the 3D scene is a full-bleed cinematic stage behind the hero copy */
@media (min-width: 921px) {
  /* the grid must stay non-positioned so the stage's absolute containing block is the hero */
  .hero:has(.kioskstage) .hero__visual { position: static; }
  .hero:has(.kioskstage) .kioskstage { position: absolute; inset: 0; height: auto; z-index: 0; }
  .hero:has(.kioskstage)::before { z-index: 1; }
  .hero:has(.kioskstage) .hero__grid { pointer-events: none; }
  .hero:has(.kioskstage) .hero__copy { position: relative; z-index: 2; }
  .hero:has(.kioskstage) .hero__grid a, .hero:has(.kioskstage) .hero__grid button { pointer-events: auto; }
  .hero:has(.kioskstage) .hero__scroll, .hero:has(.kioskstage) .hero__dots { z-index: 3; }
  .hero:has(.kioskstage) .kioskstage__fallback { inset: auto; right: 6%; top: 50%; transform: translateY(-50%); margin: 0; }

  /* liquid-glass panel behind the hero copy so it stays legible over the moving
     3D scene (desktop teardown) — translucent, blurred, with an edge sheen */
  .hero--teardown .hero__step {
    left: 0; width: fit-content; max-width: 30rem;
    padding: 30px 34px; border-radius: 26px;
    background: linear-gradient(155deg, rgba(38,41,48,.5), rgba(18,20,25,.38));
    -webkit-backdrop-filter: blur(26px) saturate(1.35); backdrop-filter: blur(26px) saturate(1.35);
    border: 1px solid rgba(255,255,255,.1);
    /* pale-blue halo on every step tile, matching the intro card */
    box-shadow:
      0 0 0 1px rgba(150,195,255,.13),
      0 0 34px -6px rgba(143,196,255,.24),
      0 0 90px -18px rgba(120,175,255,.3),
      0 30px 70px -34px rgba(0,0,0,.7),
      inset 0 1px 0 rgba(255,255,255,.18), inset 0 0 40px -18px rgba(255,255,255,.1);
    /* dynamic entry: slide + scale with a spring overshoot */
    transform: translateY(-50%) translateX(56px) scale(.965);
    transition: opacity .5s var(--ease), transform .72s cubic-bezier(.2,.86,.3,1.15);
  }
  .hero--teardown .hero__step.is-active { transform: translateY(-50%) translateX(0) scale(1); }
  .hero--teardown .hero__step[data-step="0"] { max-width: 33rem; }
  /* children cascade in behind the tile */
  .hero--teardown .hero__step > * { opacity: 0; transform: translateY(12px); transition: opacity .5s var(--ease), transform .6s var(--ease); }
  .hero--teardown .hero__step.is-active > * { opacity: 1; transform: none; }
  .hero--teardown .hero__step.is-active > *:nth-child(2) { transition-delay: .08s; }
  .hero--teardown .hero__step.is-active > *:nth-child(3) { transition-delay: .15s; }
  .hero--teardown .hero__step.is-active > *:nth-child(4) { transition-delay: .22s; }
}
.kioskstage__fallback {
  position: absolute; inset: 0; margin: auto; height: min(74vh, 760px); width: auto; max-width: 100%;
  object-fit: contain; pointer-events: none; transition: opacity .7s var(--ease);
  filter: drop-shadow(0 42px 55px rgba(0,0,0,.6));
}
/* The still is a WebGL/no-JS fallback ONLY. It used to be visible by default and hidden once
   the 3D booted, so every load briefly flashed the old flyer-cutout kiosk. Now: no JS at all
   (no html.js) keeps it visible, JS hides it immediately, and kiosk3d.js puts it back via
   .is-fallback only if WebGL actually fails to start. */
.js .kioskstage__fallback { opacity: 0; }
.js .kioskstage.is-fallback .kioskstage__fallback { opacity: 1; }

/* --- subpage hero (investors) --- */
.hero--sub { min-height: 78vh; }
.hero--sub .hero__grid { grid-template-columns: 1fr; }
.hero--sub .hero__title { max-width: 18ch; }
.hero--sub .hero__sub { max-width: 60ch; }

/* --- investor advantage grid + home cross-link --- */
.adv-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; margin-top: 44px; }

/* --- kiosk: vendor consolidation (before -> after) --- */
/* full-bleed dark stage: the whole section is the dark studio; the orbit fills it edge to edge */
.section--replace { position: relative; overflow: hidden; min-height: 100svh; min-height: 100lvh; padding: 0; display: flex; flex-direction: column;
  /* the site's navy family — this was the only section on the page in neutral grey */
  background:
    radial-gradient(900px 720px at 50% 76%, #16233c, transparent 66%),
    linear-gradient(180deg, #0b1830, var(--ink-deep));
  color: rgba(255,255,255,.82); }
/* GSAP pins by setting the section position:fixed with an INLINE PIXEL height measured at
   refresh time — so `lvh` is already baked to a number before iOS retracts its toolbar. When
   it does, the visual viewport grows past that frozen height and the transparent .pin-spacer
   lets the light body background show as a white strip along the bottom. Painting the spacers
   dark makes any such shortfall read as more section instead of a white gap. Both pinned
   elements on this page (the teardown hero and this stage) are dark, so one rule covers them.
   Height alone can't fix this — the px value is frozen at pin time. */
/* Allow-list, NOT a blanket rule. This exists because iOS can expose a pinned
   section's spacer when the toolbar retracts, and these three sections are dark.
   Applying it to every .pin-spacer painted a black band under the light
   #traction and a black square across the #capabilities zoom — it needed an
   opt-out every time anything new got pinned, so name the dark ones instead. */
body.has-kiosk-scroll .pin-spacer:has(> #hero) { background: #0d0e11; } /* matches the 3D studio grey */
.pin-spacer:has(> #replaces),
.pin-spacer:has(> #platform),
.pin-spacer:has(> #built-for),
.pin-spacer:has(> #visibility) { background: var(--ink-deep); } /* matches their navy sections */
/* ^ #visibility joins the list for the compact height band below: pinned 'top top' at a
   section slightly SHORTER than the window, the strip under it is the spacer. */
/* ^ NOT gated on body.has-kiosk-scroll (except the hero, which only pins on desktop):
   mobile no longer sets that class since the hero became the video reel, and the light
   body was flashing through under the pinned dark sections when the toolbar retracted. */
.section--replace .eyebrow { color: var(--sky); }
.section--replace .section-head { color: #fff; font-size: clamp(1.9rem, 3.4vw, 3rem); margin-bottom: 12px; }
.section--replace .lede { color: rgba(255,255,255,.84); font-size: 1.02rem; max-width: 40ch; }
/* compact intro pinned to the top-left so it clears the radial fan below it */
/* clears the 74px fixed nav so the eyebrow + title stay fully visible through the pinned reveal */
/* A2: this element is `.wrap .replace__intro`, so a max-width here BEATS .wrap's
   max-width:var(--wrap) on source order and the whole block auto-centred off the site grid
   (eyebrow at 460px @1512 / 114px @820 vs 190 / 24 everywhere else). No max-width here =
   .wrap governs the box. The *text* is already constrained tighter than 20ch/44ch by
   .section-head{max-width:16ch} and .section--replace .lede{max-width:40ch} above, so no
   extra caps are needed here — adding them would only loosen the measure. */
.replace__intro { position: relative; z-index: 3; padding-top: clamp(94px, 12vh, 118px); }
/* radial orbital funnel — six legacy systems fan out and converge into the kiosk.
   Fills the section below the intro; nodes/lines reveal on the pinned scroll (script.js). */
.orbit { position: absolute; left: 0; right: 0; bottom: 0; top: clamp(320px, 40vh, 470px); z-index: 1; overflow: visible; background: transparent; }
.orbit__links { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
.orbit__links line { stroke: rgba(120,170,255,.42); stroke-width: 1.3;
  stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset .7s var(--ease), stroke .5s var(--ease); }
.orbit__links line.is-lit { stroke-dashoffset: 0; stroke: rgba(120,170,255,.6); }

.orbit__hub { position: absolute; z-index: 4; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; text-align: center; }
.orbit__kiosk { height: clamp(104px, 17vw, 162px); width: auto; filter: drop-shadow(0 22px 40px rgba(0,0,0,.6)); }
.orbit__kiosk3d { width: 240px; height: 300px; }
/* the live 3D kiosk is used at every width now; the image is only the reduced-motion fallback */
.orbit__kiosk--img { display: none; }
@media (prefers-reduced-motion: reduce) { .orbit__kiosk3d { display: none; } .orbit__kiosk--img { display: block; } }
.orbit__hub-glow { position: absolute; top: 40%; left: 50%; width: 230px; height: 230px; transform: translate(-50%,-50%);
  background: radial-gradient(circle, rgba(76,141,255,.42), transparent 62%); filter: blur(6px); z-index: -1; pointer-events: none; }
.orbit__hub-label { margin-top: 10px; font-family: var(--font-mono); font-size: .66rem; letter-spacing: .18em; text-transform: uppercase; color: #eaf1ff; white-space: nowrap; }

/* dots + cards are positioned independently by script.js (dot on the arc,
   card pushed radially outward) so nothing collides in the crowded middle */
/* Node opacity is driven continuously from scroll progress by script.js. It used to be a
   bare class toggle with no transition, so each node popped on at its threshold — that hard
   step is what read as glitchy. */
.orbit__node { position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: 0; }
.orbit__dot { position: absolute; display: flex; align-items: center; justify-content: center; width: 46px; height: 46px; border-radius: 50%;
  background: rgba(22,25,31,.66); border: 1px solid rgba(255,255,255,.2); color: #cfe0ff;
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  box-shadow: 0 10px 26px -12px rgba(0,0,0,.7); transform: translate(-50%,-50%) scale(.5); transition: transform .5s cubic-bezier(.2,.86,.3,1.3); }
.orbit__node.is-shown .orbit__dot { transform: translate(-50%,-50%) scale(1); }
.orbit__dot svg { width: 22px; height: 22px; }
/* No scale on the card — the node's own fade carries it in. The card used to grow .94 -> 1
   on top of that, which read as an odd little zoom. */
.orbit__card { position: absolute; width: clamp(140px, 15vw, 168px); padding: 15px 17px; text-align: center;
  border-radius: 15px; background: rgba(20,23,29,.55); border: 1px solid rgba(255,255,255,.14);
  -webkit-backdrop-filter: blur(16px) saturate(1.3); backdrop-filter: blur(16px) saturate(1.3);
  box-shadow: 0 20px 46px -26px rgba(0,0,0,.75);
  opacity: 1; transform: translate(-50%,-50%); }
.orbit__card h4 { font-size: .95rem; color: #fff; margin: 0; letter-spacing: -.01em; }
/* The caption FADES. It used to animate max-height 0 -> 72px, so the card grew as the words
   arrived and every neighbour shifted — that was the "weird enlargement". It now always
   occupies its space and only opacity changes, so nothing reflows. */
.orbit__card p { font-size: .78rem; line-height: 1.34; color: rgba(255,255,255,.66);
  margin: 6px 0 0; opacity: var(--cap, 0); }

.orbit__foot { position: absolute; left: 50%; bottom: clamp(20px, 4vh, 40px); transform: translateX(-50%); z-index: 4;
  width: min(90%, 620px); text-align: center; font-size: .86rem; color: rgba(255,255,255,.58); margin: 0; }
/* SHORT desktop windows: at ≤700px tall the stage under the intro was ~300px while the
   fan kept full-size cards, so they overlapped each other and the kiosk's screen
   (settled mid-pin state). Give the stage more of the viewport and shrink the cards +
   hub kiosk so the enlarged card ring (layoutOrbit's ringMin, script.js) clears the
   intro copy and the nav. Width-gated: the ≤620 vertical timeline keeps its own sizes. */
@media (min-width: 621px) and (max-height: 700px) {
  .orbit { top: clamp(250px, 36vh, 470px); }
  .orbit__card { width: clamp(126px, 12vw, 148px); padding: 10px 12px; }
  .orbit__card h4 { font-size: .84rem; }
  .orbit__card p { font-size: .7rem; }
  .orbit__kiosk3d { width: 170px; height: 212px; }
  .orbit__kiosk { height: clamp(88px, 12vw, 118px); }
  .orbit__hub-glow { width: 170px; height: 170px; }
}
/* Narrow AND short (≲1280 wide at ≤700 tall): even the shrunken captioned cards need a
   ring wide enough to lift the top cards into the intro copy. Fold the cards down to
   title-only chips and drop the lede — the same trade the ≤620 mobile timeline already
   makes — so the fan sits clear of the heading with room to spare. */
@media (min-width: 621px) and (max-width: 1279px) and (max-height: 700px) {
  .section--replace .lede { display: none; }
  .orbit__card p { display: none; }
}
@media (max-width: 620px) {
  /* MOBILE = vertical converge timeline. Intro compact (long lede dropped); the strip of
     systems rides a central spine and folds into the kiosk (positions/motion in script.js). */
  .section--replace .lede { display: none; }
  /* clear the 74px fixed nav (+ notch) so the eyebrow + title are never cut off */
  .replace__intro { padding-top: clamp(102px, 13.5vh, 126px); }
  .orbit { top: clamp(236px, 30vh, 288px); overflow: hidden; }
  .orbit__links { display: none; }
  .orbit__dot { display: none; }
  /* the central "current" the six systems ride into the kiosk */
  .orbit::before { content: ''; position: absolute; left: 50%; top: 4px; bottom: 12%; width: 2px; transform: translateX(-50%);
    background: linear-gradient(180deg, rgba(120,170,255,.04), rgba(120,170,255,.5) 16%, rgba(120,170,255,.5) 74%, rgba(76,141,255,.95));
    box-shadow: 0 0 14px rgba(76,141,255,.4); z-index: 0; }
  .orbit__node { will-change: transform, opacity; }
  /* JS controls the node's fade + slide; the card itself just sits at its timeline row */
  /* Every tile is the SAME fixed box — the caption reveals inside it rather than growing it.
     Constant height means the strip never reflows mid-reveal and, more importantly, the
     kiosk's clearance (measured in layoutOrbit) holds before and after the captions open. */
  .orbit__card, .orbit__node.is-shown .orbit__card { width: 43%; max-width: 188px; height: 92px;
    padding: 10px 13px; border-radius: 13px; display: flex; flex-direction: column; justify-content: center;
    opacity: 1; transform: translateY(-50%); transition: none; box-shadow: 0 16px 34px -20px rgba(0,0,0,.8); }
  .orbit__node--l .orbit__card { right: calc(50% + 20px); left: auto; text-align: right; }
  .orbit__node--r .orbit__card { left: calc(50% + 20px); right: auto; text-align: left; }
  .orbit__card h4 { font-size: .86rem; }
  .orbit__card p { font-size: .72rem; line-height: 1.3; }
  .orbit__hub { will-change: transform; }
  .orbit__kiosk { height: clamp(96px, 26vw, 132px); }
  /* live 3D hub kiosk sized for the timeline foot */
  .orbit__kiosk3d { width: min(64vw, 240px); height: min(84vw, 300px); }
  .orbit__hub.is-converging .orbit__kiosk,
  .orbit__hub.is-converging .orbit__kiosk3d {
    filter: drop-shadow(0 0 calc(10px + 26px * var(--conv, 0)) rgba(76,141,255, calc(.25 + .55 * var(--conv, 0)))); }
  .orbit__hub-label { font-size: .6rem; }
  .orbit__foot { display: none; }
}
@media (prefers-reduced-motion: reduce) { .orbit__node { opacity: 1; } .orbit__node .orbit__dot, .orbit__node .orbit__card { transform: none; opacity: 1; } .orbit__card p { opacity: 1; } .orbit__links line { stroke-dashoffset: 0; } }

/* FINALE — the real capabilities section is CLONED into #capScreen and positioned by the
   3D loop directly onto the kiosk screen (letterboxed by the black screen). The overlay is a
   CONSTANT full-viewport layer moved purely by transform: translate(--tx,--ty) scale(--kx,--ky)
   shrinks it onto the projected screen rect, and the inner card counter-scales (--isx/--isy =
   iscale/kx, iscale/ky) so its content stays uniform. Animating transform instead of
   left/top/width/height keeps the finale off the layout/paint path (it was a full relayout of a
   fixed full-viewport element every frame, on top of the WebGL render). All factors land at
   identity when the break-out completes, so the hard swap alignment is unchanged. Vars from JS:
   --tx/--ty px offset, --kx/--ky outer scale, --isx/--isy inner scale, --bo break-out, --op opacity. */
#capScreen { --tx: 0px; --ty: 0px; --kx: 1; --ky: 1; --isx: 1; --isy: 1; --bo: 0; --op: 0; --va: 0;
  position: fixed; z-index: 60; pointer-events: none; overflow: hidden; opacity: var(--op);
  left: 0; top: 0; width: 100%; height: 100svh; height: 100lvh; /* 100% not 100vw: vw includes the classic-scrollbar gutter and overhangs by ~15px on always-on-scrollbar systems */
  transform: translate(var(--tx), var(--ty)) scale(var(--kx), var(--ky));
  /* dark screen while on the kiosk → page paper as it breaks out to fill (covers the kiosk) */
  background: color-mix(in srgb, #07080c, var(--paper) calc(var(--bo) * 100%));
  /* as it breaks out, a matching full-viewport field (--va fades in) veils the 3D kiosk so its
     bezel/grid never flash around the expanding card; fades away with --op at the swap.
     250vmax (not 100) because the spread is scaled down by --kx/--ky along with the element. */
  box-shadow: 0 0 0 250vmax color-mix(in srgb, transparent, color-mix(in srgb, #07080c, var(--paper) calc(var(--bo) * 100%)) calc(var(--va) * 100%));
  display: flex; align-items: center; justify-content: center; will-change: transform; }
/* horizontal padding matches the real section's .wrap (0 24px) so the content box lines up
   exactly when the overlay hands off to the real section; vertical padding keeps the card look */
.capscreen__inner { flex: none; width: min(1180px, 100%); background: var(--paper); border-radius: 6px; padding: 40px 24px;
  transform: scale(var(--isx), var(--isy)); transform-origin: center center; }
.capscreen__inner .wrap { max-width: none; padding: 0; }
/* keep the reveal animations off inside the clone — it's shown fully assembled */
.capscreen__inner .reveal { opacity: 1 !important; transform: none !important; }
/* centred so the real section lines up with the (centred) break-out overlay = no ghosting */
#capabilities[data-capfinale] { position: relative; min-height: 100svh; min-height: 100lvh; display: flex; flex-direction: column; justify-content: center; }
/* Pulled up to overlap the hero so the real section sits exactly where the overlay ends, and
   held at opacity 0 until the overlay swaps to it (kiosk3d.js drives the inline opacity).
   padding:0 so justify-content centres the content at the TRUE viewport centre = the #capScreen
   overlay's centred card, so the two are pixel-aligned at the swap. Mobile overrides the
   padding below (it top-aligns instead), but otherwise runs the same finale. */
#capabilities[data-capfinale].is-finale { margin-top: -100lvh; opacity: 0; padding: 0; }
@media (prefers-reduced-motion: reduce) {
  #capScreen { display: none; }
  #capabilities[data-capfinale].is-finale { margin-top: 0; opacity: 1; }
}
/* MOBILE finale: the capabilities section is a tall 1-col, so TOP-align it on the kiosk screen
   (and through the break-out) — shows the "Six functions" header, not the middle cards.
   Both the overlay clone and the real section get the same top padding so the header clears the
   fixed nav at break-out AND they stay aligned at the swap. Placed after the base
   #capScreen rules so it wins (same specificity — source order decides). */
@media (max-width: 620px) {
  #capScreen { align-items: flex-start; }
  .capscreen__inner { transform-origin: top center; padding-top: 92px; }
  /* bottom padding matters here: without it the last tile (Data Intelligence) sits flush
     against the dark consolidation section with no breathing room */
  #capabilities[data-capfinale].is-finale { padding: 92px 0 56px; }
}
/* MOBILE (no finale below 921 — kiosk3d never runs): the section pins at top:0 and the
   base .section padding parked the h2 at 64px, under the 74px fixed nav, for the whole
   scrub. Clear it — same fix as .replace__intro / .section--proof. */
@media (max-width: 920px) {
  #capabilities[data-capfinale] { padding-top: clamp(102px, 13.5vh, 126px); }
}

/* --- kiosk: operator visibility --- */
.section--visibility { background: var(--ink-deep); color: rgba(255,255,255,.82); }
.section--visibility h2 { color: #fff; }
.reserve__flag { margin-top: 22px; font-family: var(--font-mono); font-size: .72rem; letter-spacing: .03em; color: var(--slate-soft); border-left: 2px solid var(--sky); padding-left: 14px; }
/* Direct contact details beside the form. <address> is italic by default; reset it. */
.reserve__direct { font-style: normal; margin-top: 30px; display: grid; gap: 10px; justify-items: start; }
.reserve__direct-label { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; color: var(--slate-soft); }
.reserve__direct a, .reserve__direct-place { font-size: .95rem; font-weight: 500; color: var(--ink); overflow-wrap: anywhere; }
.reserve__direct a { transition: color .3s; }
.reserve__direct a:hover { color: var(--royal); }

/* ============================================================
   Section 01 — feature grid
   ============================================================ */
.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.feature {
  padding: 30px; border-radius: var(--radius); background: #fff;
  border: 1px solid var(--line); box-shadow: var(--card-shadow);
  transition: transform .5s var(--ease), box-shadow .5s var(--ease), border-color .5s;
}
.feature:hover { transform: translate(2px,-6px); box-shadow: var(--card-shadow-hover); border-color: var(--line-strong); }
.feature h3 { font-size: 1.18rem; font-weight: 600; letter-spacing: -.01em; margin: 20px 0 8px; }
.feature p { font-size: .95rem; color: var(--slate-soft); }
.feature__icon { display: block; width: 46px; height: 46px; color: var(--royal); }

/* ============================================================
   Section 02 — ecosystem flow (self-drawing spine)
   ============================================================ */
.flow { list-style: none; margin: 60px 0 0; padding: 0; position: relative; max-width: 720px; }
.flow::before { /* the track */
  content: ""; position: absolute; left: 15px; top: 10px; bottom: 10px; width: 2px;
  background: rgba(255,255,255,.12);
}
.flow::after { /* the drawn current */
  content: ""; position: absolute; left: 15px; top: 10px; width: 2px; height: var(--flow, 0%);
  background: var(--grad); box-shadow: 0 0 16px rgba(76,141,255,.7);
  transition: height 1.4s var(--ease);
}
.flow__node { position: relative; padding: 0 0 40px 56px; }
.flow__node:last-child { padding-bottom: 0; }
.flow__dot {
  position: absolute; left: 6px; top: 4px; width: 20px; height: 20px; border-radius: 50%;
  background: var(--ink-deep); border: 2px solid rgba(255,255,255,.25); z-index: 1; transition: border-color .5s, box-shadow .5s;
}
.flow__node.is-visible .flow__dot { border-color: var(--sky); box-shadow: 0 0 0 5px rgba(76,141,255,.16); }
.flow__k { font-family: var(--font-display); font-weight: 600; font-size: 1.28rem; color: #fff; display: block; }
.flow__card p { color: rgba(255,255,255,.62); font-size: .96rem; margin-top: 4px; }


/* ============================================================
   Section 04 — investor pillars
   ============================================================ */
.pillars { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-top: 50px; }
.pillar {
  position: relative; padding: 34px 26px; border-radius: var(--radius); overflow: hidden;
  background: #fff; border: 1px solid var(--line); box-shadow: var(--card-shadow);
  transition: transform .5s var(--ease), box-shadow .5s var(--ease), border-color .5s;
}
.pillar:hover { transform: translateY(-5px); box-shadow: var(--card-shadow-hover); border-color: var(--line-strong); }
.pillar::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--grad); }
.pillar__no { font-family: var(--font-mono); font-size: .8rem; letter-spacing: .1em; color: var(--sky); }
.pillar h3 { font-size: 1.22rem; font-weight: 600; letter-spacing: -.01em; margin: 14px 0 10px; }
.pillar p { font-size: .95rem; color: var(--slate-soft); }

/* ============================================================
   Section 05 — story beats
   ============================================================ */
.beats { max-width: 860px; margin: 46px auto 0; padding: 0 24px; }
.beat { display: grid; grid-template-columns: 88px 1fr; gap: 20px; padding: 34px 0; border-top: 1px solid var(--line); }
.beat:first-child { border-top: none; }
.beat__no { font-family: var(--font-mono); font-size: 1.5rem; color: transparent; background: var(--grad); -webkit-background-clip: text; background-clip: text; font-weight: 700; }
.beat h3 { font-size: 1.5rem; margin-bottom: 12px; }
.beat p { color: var(--slate); font-size: 1.02rem; }
.beat p + p { margin-top: 12px; }
.beat__quote { font-family: var(--font-display); font-size: 1.24rem; font-weight: 600; color: var(--ink); border-left: 3px solid var(--sky); padding-left: 18px; }

.thesis { margin: 50px auto 0; max-width: 860px; padding: 46px; text-align: center; border-radius: 22px; background: var(--ink-deep); color: rgba(255,255,255,.72); }
.thesis p { font-size: 1.1rem; max-width: 60ch; margin: 0 auto; }
.thesis__line { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.7rem, 4vw, 2.9rem); margin-top: 20px; letter-spacing: -.02em; }

/* ============================================================
   Section 06 — journey timeline
   ============================================================ */

/* ============================================================
   Advantage cards (.adv-grid — investors page)
   ============================================================ */
@keyframes fadeUp { from { opacity: 0; transform: translate3d(-10px, 18px, 0); } to { opacity: 1; transform: none; } }
.adv { padding: 28px; border-radius: var(--radius); background: #fff; border: 1px solid var(--line); box-shadow: var(--card-shadow); }
.adv__no { font-family: var(--font-mono); font-size: .8rem; color: var(--sky); }
.adv h3 { font-size: 1.18rem; margin: 12px 0 10px; }
.adv p { font-size: .96rem; color: var(--slate-soft); }

/* ============================================================
   Section 08 — bigger vision toggle
   ============================================================ */
.vision-toggle { position: relative; display: inline-flex; margin: 44px 0 34px; padding: 5px; border-radius: 999px; background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.14); }
.vtoggle { position: relative; z-index: 1; background: none; border: none; cursor: pointer; padding: 11px 34px; border-radius: 999px; color: rgba(255,255,255,.6); font-family: var(--font-mono); font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; transition: color .4s; }
.vtoggle.is-active { color: #fff; }
.vtoggle__thumb { position: absolute; z-index: 0; top: 5px; bottom: 5px; left: 5px; width: calc(50% - 5px); border-radius: 999px; background: var(--grad); transition: transform .5s var(--ease); box-shadow: 0 8px 20px -8px rgba(20,71,230,.8); }
.vision-toggle[data-active="tomorrow"] .vtoggle__thumb { transform: translateX(100%); }

.vision-panels { min-height: 96px; }
.chips { list-style: none; margin: 0; padding: 0; display: none; flex-wrap: wrap; gap: 12px; }
.chips.is-active { display: flex; animation: fadeUp .6s var(--ease); }
.chips li { font-family: var(--font-mono); font-size: .84rem; letter-spacing: .04em; padding: 12px 20px; border-radius: 999px; background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.16); color: #fff; }
.chips[data-when="tomorrow"] li { border-color: rgba(76,141,255,.5); background: rgba(76,141,255,.12); }

.pullquote {
  margin: 60px auto 0; max-width: 900px; text-align: center;
  font-family: var(--font-display); font-weight: 600; font-size: clamp(1.5rem, 3.6vw, 2.6rem);
  line-height: 1.24; color: #fff; letter-spacing: -.02em;
  background: var(--grad); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}

/* ============================================================
   Section 09 — reserve / contact
   ============================================================ */
.reserve__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: start; }
.reserve__points { list-style: none; padding: 0; margin: 30px 0 0; display: grid; gap: 14px; }
.reserve__points li { display: flex; align-items: center; gap: 12px; font-weight: 500; color: var(--ink); }
.reserve__points span { display: inline-flex; width: 22px; height: 22px; color: var(--royal); }

.reserve__form { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 34px; border-radius: 22px; background: #fff; border: 1px solid var(--line); box-shadow: 0 40px 80px -50px rgba(11,31,61,.5); }
.field { display: flex; flex-direction: column; gap: 7px; }
.field--full { grid-column: 1 / -1; }
.field label { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--slate-soft); }
.field input, .field select, .field textarea {
  font-family: var(--font-body); font-size: .98rem; color: var(--ink);
  padding: 13px 15px; border: 1.5px solid var(--line); border-radius: 11px; background: var(--paper); resize: vertical;
  transition: border-color .3s, box-shadow .3s;
}
/* A7: the .1 ring was effectively invisible, and `outline:none` left it as the ONLY keyboard
   indicator. Ring strengthened to .3; keyboard focus additionally gets a real outline below. */
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--royal); box-shadow: 0 0 0 3px rgba(20,71,230,.3); }
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--royal); outline-offset: 1px;
}
.reserve__form .btn { grid-column: 1 / -1; }
.form__note { grid-column: 1 / -1; font-family: var(--font-mono); font-size: .78rem; color: var(--royal); min-height: 1em; margin: 0; }

/* ============================================================
   Footer
   ============================================================ */
/* same glass treatment as the nav — translucent over the page rather than a solid block */
.footer {
  background: rgba(7, 22, 40, .93);
  -webkit-backdrop-filter: blur(18px) saturate(1.5); backdrop-filter: blur(18px) saturate(1.5);
  border-top: 1px solid rgba(255,255,255,.08);
  color: rgba(255,255,255,.6); padding: 80px 0 34px;
}
/* The footer is dark but carries no .dark class, so .btn--ghost fell back to its
   light-background colour (--ink) and the label was unreadable against the footer. */
.footer .btn--ghost { color: #fff; border-color: rgba(255,255,255,.34); }
.footer .btn--ghost:hover { color: var(--sky-soft); border-color: var(--sky); }
.footer .brand, .footer h3 { color: #fff; }
.footer .brand__mark { height: 34px; width: auto; }
/* Three columns above 920 (brand | contact | nav); the ≤920 block below already
   forces 1fr, so the added track only ever exists on desktop. */
.footer__top { display: grid; grid-template-columns: 1.3fr .8fr 1fr; gap: 40px; padding-bottom: 46px; border-bottom: 1px solid var(--line-dark); }
/* <address> defaults to italic — reset it, this is contact data not an aside. */
.footer__contact { font-style: normal; display: grid; gap: 12px; align-content: start; }
.footer__contact a, .footer__contact-place { font-family: var(--font-mono); font-size: .78rem; letter-spacing: .06em; color: rgba(255,255,255,.6); overflow-wrap: anywhere; }
.footer__contact a { transition: color .3s; }
.footer__contact a:hover { color: var(--sky-soft); }
.footer__contact-label { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; color: rgba(255,255,255,.38); }
.footer__tag { margin: 18px 0 26px; max-width: 34ch; }
.footer__cta { display: flex; flex-wrap: wrap; gap: 12px; }
.footer__nav { display: grid; grid-template-columns: repeat(2, auto); gap: 14px 40px; align-content: start; justify-content: end; }
.footer__nav a { font-family: var(--font-mono); font-size: .78rem; letter-spacing: .1em; text-transform: uppercase; color: rgba(255,255,255,.6); transition: color .3s; }
.footer__nav a:hover { color: var(--sky-soft); }
.footer__bottom { display: flex; justify-content: space-between; gap: 20px; padding-top: 26px; font-family: var(--font-mono); font-size: .72rem; letter-spacing: .04em; flex-wrap: wrap; }
.footer__note { color: rgba(255,255,255,.58); } /* A1: .36 measured 3.24:1 — failed AA */

/* ============================================================
   Proof / traction band
   ============================================================ */
.section--proof { background: var(--paper-2); }
.proof { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-top: 46px; }
.proof__item {
  padding: 30px 26px; border-radius: var(--radius); background: #fff;
  border: 1px solid var(--line); box-shadow: var(--card-shadow);
  transition: transform .5s var(--ease), box-shadow .5s var(--ease), border-color .5s;
}
.proof__item:hover { transform: translateY(-5px); box-shadow: var(--card-shadow-hover); border-color: var(--line-strong); }
.proof__lead { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.7rem, 2.6vw, 2.2rem); color: var(--ink); letter-spacing: -.02em; line-height: 1.02; }
.proof__label { margin-top: 12px; font-family: var(--font-mono); font-size: .74rem; letter-spacing: .06em; text-transform: uppercase; color: var(--slate-soft); line-height: 1.5; }
.proof__note { margin-top: 30px; font-family: var(--font-mono); font-size: .72rem; letter-spacing: .03em; color: var(--slate-soft); }

/* ============================================================
   Cash → Commerce → Data triad
   ============================================================ */
.section--flow-hl { background: var(--ink-deep); color: rgba(255,255,255,.82); }
.section--flow-hl .section-head, .section--flow-hl h3 { color: #fff; }
.section--flow-hl .lede { color: rgba(255,255,255,.84); }
.triad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-top: 50px; }
.triad__stage {
  position: relative; padding: 34px 30px; border-radius: var(--radius);
  background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.12);
}
.triad__stage:not(:first-child)::before {
  content: "→"; position: absolute; left: -25px; top: 50%; transform: translateY(-50%);
  color: var(--sky); font-family: var(--font-mono); font-size: 1.4rem; line-height: 1;
}
.triad__k { font-family: var(--font-mono); font-size: .76rem; letter-spacing: .18em; text-transform: uppercase; color: var(--sky-soft); }
.triad__stage h3 { font-size: clamp(1.9rem, 3.4vw, 2.6rem); margin: 12px 0 14px; }
.triad__stage p { color: rgba(255,255,255,.72); font-size: 1rem; }
.triad__close { margin: 44px auto 0; max-width: 760px; text-align: center; font-family: var(--font-display); font-weight: 600; font-size: clamp(1.2rem, 2.4vw, 1.7rem); line-height: 1.35; color: #fff; letter-spacing: -.01em; }
.triad__close b { background: var(--grad); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; }

/* ============================================================
   Why Now
   ============================================================ */
.section--why { background: var(--paper); }
.whynow { list-style: none; margin: 46px 0 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; counter-reset: why; }
.whynow li {
  position: relative; padding: 26px 28px 26px 66px; border-radius: var(--radius);
  background: #fff; border: 1px solid var(--line); box-shadow: var(--card-shadow);
  font-size: 1.06rem; color: var(--ink); font-weight: 500; line-height: 1.5;
}
.whynow li::before {
  counter-increment: why; content: counter(why, decimal-leading-zero);
  position: absolute; left: 28px; top: 28px; font-family: var(--font-mono); font-size: .82rem; letter-spacing: .08em; color: var(--sky);
}
.whynow li:last-child { grid-column: 1 / -1; background: var(--ink-deep); color: #fff; border: none; font-weight: 600; }
.whynow li:last-child::before { color: var(--sky-soft); }

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 920px) {
  .nav__cta { display: none; }
  /* The panel stays in the DOM so it can animate — `display:none` can't transition.
     visibility + pointer-events keep it inert and out of the tab order when shut. */
  .nav__links {
    display: flex; flex-direction: column; gap: 2px; margin: 0;
    position: absolute; top: var(--nav-h); left: 0; right: 0;
    padding: 14px 24px 22px;
    /* OPAQUE, because the blur below NEVER RENDERS here: the panel is
       nested inside the header, which has its own backdrop-filter, and Chrome
       only lets an element sample its backdrop root — the inner filter is
       silently dropped. Even .98/.96 wasn't enough: the saturate() leg lifts
       what leaks through the remaining 2-4%, and the contact form's labels and
       the footer's white logo still ghosted behind the menu items (verified by
       pixel diff). Solid fill is the only thing that actually wins. (The
       filter is kept for engines that do support nesting — behind an opaque
       fill it simply does nothing.) */
    background: linear-gradient(180deg, rgb(9, 17, 31), rgb(8, 16, 30));
    -webkit-backdrop-filter: blur(30px) saturate(1.7); backdrop-filter: blur(30px) saturate(1.7);
    border-bottom: 1px solid rgba(255,255,255,.10);
    box-shadow: 0 1px 0 rgba(255,255,255,.05) inset, 0 24px 44px -28px rgba(0,0,0,.85);
    opacity: 0; visibility: hidden; pointer-events: none;
    transform: translateY(-8px);
    clip-path: inset(0 0 100% 0);
    transition: opacity .3s var(--ease), transform .42s var(--ease),
                clip-path .44s var(--ease), visibility .44s;
  }
  .nav__links.is-open {
    opacity: 1; visibility: visible; pointer-events: auto;
    transform: none; clip-path: inset(0 0 0 0);
  }
  /* keeps the desktop bar's type — mono, uppercase, tracked, white — rather than
     switching to slate body copy on a white card */
  .nav__links a {
    padding: 13px 2px; color: rgba(255,255,255,.84);
    border-bottom: 1px solid rgba(255,255,255,.06);
    opacity: 0; transform: translateY(-6px);
    transition: opacity .34s var(--ease), transform .34s var(--ease), color .3s;
  }
  .nav__links a:last-child { border-bottom: 0; }
  .nav__links.is-open a { opacity: 1; transform: none; }
  .nav__links.is-open a:nth-child(1) { transition-delay: .05s; }
  .nav__links.is-open a:nth-child(2) { transition-delay: .10s; }
  .nav__links.is-open a:nth-child(3) { transition-delay: .15s; }
  .nav__links.is-open a:nth-child(4) { transition-delay: .20s; }
  .nav__links.is-open a:nth-child(5) { transition-delay: .25s; }
  /* the underline sweep is a desktop hover affordance; on touch it just sat under
     whatever was tapped last */
  .nav__links a::after { display: none; }

  /* tapping an item: it lights, a brand sweep runs under it, then the panel closes */
  .nav__links a.is-picked { color: #fff; }
  .nav__links a.is-picked::before {
    content: ''; position: absolute; left: -8px; right: -8px; top: 2px; bottom: 2px;
    border-radius: 8px; background: var(--grad-soft);
    animation: navPick .42s var(--ease) forwards;
  }
  @keyframes navPick {
    from { opacity: 0; transform: scaleX(.6); }
    60%  { opacity: 1; transform: scaleX(1); }
    to   { opacity: 0; transform: scaleX(1); }
  }
  .nav__toggle {
    display: inline-flex; flex-direction: column; gap: 5px; margin-left: auto;
    background: none; border: none; cursor: pointer; padding: 8px;
  }
  .nav__toggle span { width: 24px; height: 2px; background: #fff; transition: transform .4s var(--ease), opacity .3s, background .4s; }
  .nav__toggle[aria-expanded="true"] span:first-child { transform: translateY(3.5px) rotate(45deg); }
  .nav__toggle[aria-expanded="true"] span:last-child { transform: translateY(-3.5px) rotate(-45deg); }

  .hero__grid { grid-template-columns: 1fr; }
  /* static fallback (reduced-motion / no JS): show intro copy only */
  body:not(.has-kiosk-scroll) .hero--teardown .hero__copy { min-height: 0; }
  body:not(.has-kiosk-scroll) .hero--teardown .hero__step { position: static; transform: none; opacity: 1; visibility: visible; pointer-events: auto; }
  body:not(.has-kiosk-scroll) .hero--teardown .hero__step:not([data-step="0"]) { display: none; }
  body:not(.has-kiosk-scroll) .hero__teardown-cue { display: none; }
  /* pinned mobile deconstruction: the kiosk stage is full-bleed the WHOLE time,
     with copy overlaid — so the resting hero eases into the teardown with no
     layout swap. The kiosk is framed low (below the copy) at rest by the camera. */
  /* 100lvh (largest viewport) so the dark hero always covers the screen — otherwise when the
     mobile toolbar retracts on scroll, a strip of the white page body shows below it */
  body.has-kiosk-scroll .hero--teardown { position: relative; min-height: 100svh; min-height: 100lvh; padding: 0; }
  body.has-kiosk-scroll .hero--teardown .hero__grid { display: block; height: 100svh; height: 100lvh; padding: 0; }
  body.has-kiosk-scroll .hero--teardown .hero__visual { position: absolute; inset: 0; min-height: 0; z-index: 0; }
  body.has-kiosk-scroll .hero--teardown .kioskstage { position: absolute; inset: 0; height: auto; }
  body.has-kiosk-scroll .hero--teardown .hero__copy { position: absolute; inset: 0; min-height: 0; z-index: 2; pointer-events: none; }
  body.has-kiosk-scroll .hero--teardown .hero__copy a, body.has-kiosk-scroll .hero--teardown .hero__copy button { pointer-events: auto; }
  /* Intro sits in the same frosted tile as the capability steps, pinned to the top of the
     fold. On load this tile is the ONLY thing on screen — the kiosk is framed below the
     fold by the camera (see kiosk3d.js REST_Y) and rises in as the teardown starts. */
  body.has-kiosk-scroll .hero--teardown .hero__step[data-step="0"] {
    /* Centred between the nav and the foot of the fold. Done with auto margins rather than
       top:50% + translateY(-50%), because kiosk3d.js writes `transform` directly to drive the
       retract — a centring transform here would be overwritten by it. */
    position: absolute; top: calc(var(--nav-h) + 8px); bottom: 8px; left: 16px; right: 16px;
    height: fit-content; margin-top: auto; margin-bottom: auto; transform: none;
    background: rgba(9, 16, 32, .5);
    -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
    border: 1px solid rgba(120, 160, 255, .16); border-radius: 18px;
    padding: 20px 18px 18px;
    /* overflow clips the copy as kiosk3d.js collapses max-height; transition off so the
       shrink/fade scrubs 1:1 with scroll instead of lagging behind the kiosk */
    overflow: hidden; transition: none;
  }
  /* the tile fades as a whole now, so its children must not re-fade on their own */
  body.has-kiosk-scroll .hero--teardown .hero__step[data-step="0"].is-active > * { transition-delay: 0s; }
  /* capability steps ride in a frosted-glass tile anchored high on the fold */
  body.has-kiosk-scroll .hero--teardown .hero__step:not([data-step="0"]) {
    position: absolute; left: 16px; right: 16px; top: auto; bottom: 16svh; transform: translateY(14px);
    background: rgba(9, 16, 32, .5);
    -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
    border: 1px solid rgba(120, 160, 255, .16); border-radius: 18px;
    padding: 18px 18px 16px;
    box-shadow:
      0 0 0 1px rgba(150,195,255,.13),
      0 0 28px -8px rgba(143,196,255,.22),
      0 0 70px -20px rgba(120,175,255,.26);
  }
  body.has-kiosk-scroll .hero--teardown .hero__step:not([data-step="0"]).is-active { transform: translateY(0); }
  body.has-kiosk-scroll .hero--teardown .hero__title { font-size: clamp(1.7rem, 6.6vw, 2.2rem); line-height: 1.08; margin-bottom: 12px; }
  body.has-kiosk-scroll .hero--teardown .hero__sub { font-size: .95rem; margin-bottom: 18px; max-width: none; }
  body.has-kiosk-scroll .hero--teardown .hero__step-h { font-size: clamp(1.6rem, 6.5vw, 2.2rem); }
  body.has-kiosk-scroll .hero--teardown .hero__step-p { font-size: .95rem; }
  body.has-kiosk-scroll .hero--teardown .hero__scroll { display: none; }
  .hero__visual { min-height: 300px; order: -1; }
  .kioskstage { height: min(54vh, 460px); }
  .kioskstage__fallback { height: min(48vh, 430px); }
  .feature-grid { grid-template-columns: 1fr 1fr; }
  .pillars { grid-template-columns: 1fr 1fr; }
  /* single column, both blocks centred and width-capped so the form is centred on the
     page rather than stretched edge-to-edge; tighter form padding so the inputs still
     have room at 390px, and the flag note can always wrap. */
  .reserve__grid { grid-template-columns: 1fr; gap: 36px; justify-items: center; }
  .reserve__intro, .reserve__form { width: 100%; max-width: 460px; margin-inline: auto; }
  .reserve__form { padding: 22px 18px; }
  /* .btn is white-space:nowrap, so the long submit label ('Request Briefing & Reserve
     Capacity') has a ~399px min-content. That sets the automatic minimum of the grid
     track it sits in, which blows the whole reserve column past a 390px viewport — the
     form ran off the right edge. Let the submit label wrap, and let the tracks shrink
     below their content's min-content (grid/flex default is min-width:auto). */
  .reserve__form .btn { white-space: normal; }
  .reserve__grid > *, .reserve__form > * { min-width: 0; }
  .reserve__flag { overflow-wrap: anywhere; }
  .adv-grid { grid-template-columns: 1fr; }
  .footer__top { grid-template-columns: 1fr; }
  .footer__nav { justify-content: start; }
  .proof { grid-template-columns: 1fr 1fr; }
  .triad { grid-template-columns: 1fr; gap: 36px; }
  .triad__stage:not(:first-child)::before { left: 50%; top: -26px; transform: translateX(-50%) rotate(90deg); }
  .whynow { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .feature-grid, .pillars, .reserve__form { grid-template-columns: 1fr; }
  .beat { grid-template-columns: 1fr; gap: 8px; }
  .thesis { padding: 32px 22px; }
  .proof { grid-template-columns: 1fr; }
}

/* ============================================================
   Reduced motion
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
  .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
  /* reduced motion: no scroll teardown — show intro copy only */
  .hero--teardown .hero__copy { min-height: 0 !important; }
  .hero--teardown .hero__step { position: static !important; transform: none !important; opacity: 1 !important; visibility: visible !important; transition: none !important; }
  .hero--teardown .hero__step:not([data-step="0"]) { display: none !important; }
  .hero__teardown-cue, .hero--teardown .hero__dots { display: none !important; }
}

/* ============================================================
   Hero intro tile — soft pale-blue glow around the whole tile
   ============================================================ */
/* Layered outward shadows read as a gentle halo on every edge. The desktop tile already
   sets a box-shadow, so this restates the full stack (shadows don't merge across rules).
   NB do NOT set `position` here — the step is absolutely positioned by the teardown rules
   further up, and re-declaring it in this later block breaks the centring. */
.hero--teardown .hero__step[data-step="0"] {
  box-shadow:
    0 0 0 1px rgba(150,195,255,.13),
    0 0 34px -6px rgba(143,196,255,.24),
    0 0 90px -18px rgba(120,175,255,.3),
    0 30px 70px -34px rgba(0,0,0,.7),
    inset 0 1px 0 rgba(255,255,255,.16);
}
@media (max-width: 920px) {
  /* the tile is overflow:hidden here (so kiosk3d can collapse it), so keep the halo tight */
  body.has-kiosk-scroll .hero--teardown .hero__step[data-step="0"] {
    box-shadow:
      0 0 0 1px rgba(150,195,255,.13),
      0 0 28px -8px rgba(143,196,255,.22),
      0 0 70px -20px rgba(120,175,255,.26),
      0 16px 34px -20px rgba(0,0,0,.8);
  }
}

/* ============================================================
   Platform ring — six glass tiles fanned on a circle, spun by scroll
   ============================================================ */
.section--ecosystem:has(.ring) { min-height: 100svh; min-height: 100lvh; display: flex; flex-direction: column; justify-content: center; }
/* A4: the far tiles of the carousel used to reach past the viewport (x=490 at a 390 screen
   = 79px of document overflow) and got hard-cut mid-word by html{overflow-x:hidden}.
   overflow-x:clip contains them in layout; the mask dissolves them at both edges instead of
   snapping. overflow-y stays visible (clip is the only value that may pair with visible), so
   the magnified front tile is never cropped top/bottom. Clipping the `perspective` element is
   safe — transform-style:preserve-3d lives on the child .ring__stage, so 3D is preserved. */
.ring { position: relative; height: clamp(320px, 42vh, 410px); margin-top: 26px; perspective: 1150px;
  overflow-x: clip; overflow-y: visible;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 11%, #000 89%, transparent 100%); }
.ring__stage { position: absolute; inset: 0; transform-style: preserve-3d; transform: rotateY(var(--spin, 0deg)); will-change: transform; }
.ring__tile {
  --th: 178px;
  position: absolute; left: 50%; top: 50%; width: var(--tw, 240px);
  height: var(--th); display: flex; flex-direction: column; justify-content: center;
  margin-left: calc(var(--tw, 240px) / -2); margin-top: calc(var(--th) / -2);
  transform: rotateY(var(--a, 0deg)) translateZ(var(--r, 320px));
  /* without this the tiles that have swung past 90deg show their content mirrored */
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  padding: 20px 20px 22px; border-radius: 18px; opacity: var(--o, 1);
  background: linear-gradient(155deg, rgba(120,160,255,.15), rgba(10,18,36,.52));
  -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3);
  border: 1px solid rgba(140,180,255,.22);
  box-shadow: 0 26px 60px -32px rgba(0,0,0,.85), inset 0 1px 0 rgba(255,255,255,.16);
}
.ring__ico { display: block; width: 30px; height: 30px; color: var(--sky); }
.ring__tile h3 { font-size: 1.06rem; color: #fff; margin: 12px 0 7px; }
.ring__tile p { font-size: .9rem; line-height: 1.45; color: rgba(255,255,255,.66); }
/* no GSAP / reduced motion: drop the circle, show a plain readable grid */
/* static fallback is a plain grid — no ring to fade, so drop the edge mask (it would just
   wash out the first and last column) */
.ring.is-static { height: auto; perspective: none; margin-top: 40px;
  -webkit-mask-image: none; mask-image: none; overflow: visible; }
.ring.is-static .ring__stage { position: static; transform: none; display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.ring.is-static .ring__tile { position: static; width: auto; height: auto; margin: 0; transform: none; opacity: 1; backface-visibility: visible; }

@media (max-width: 920px) {
  .ring.is-static .ring__stage { grid-template-columns: 1fr; }
}
@media (max-width: 620px) {
  /* Perspective magnifies the front tile by P/(P-r). At 820px with r~345 that was 1.7x, so
     one card swallowed the screen and its neighbours ran off both edges. 1200 brings it to
     ~1.4x — the facing card still leads, but the cards either side stay on screen. */
  .ring { height: clamp(290px, 48vh, 360px); perspective: 1300px; }
  .ring__tile { --th: 172px; padding: 13px 14px; border-radius: 14px; }
  .ring__tile h3 { font-size: .94rem; }
  .ring__tile p { font-size: .8rem; line-height: 1.38; }
}
/* the venue carousel's glass tiles need a dark backdrop to read — the light paper-2 default
   left the white tile copy almost invisible */
/* A9: the section is pinned at a full 100lvh but only carries ~660px of content, so
   justify-content:center split the slack into two equal dead bands and pushed the header a
   third of the way down the screen before anything happened. Top-aligning against a measured
   pad puts the heading on screen the moment the pin engages and drops the ring into the lower
   two-thirds. NB deliberately NOT applied to #capabilities — same symptom, but that section is
   the finale hand-off target and its geometry must not move. */
.section--verticals:has(.ring) {
  background: radial-gradient(900px 640px at 50% 42%, #16233c, var(--ink-deep) 70%);
  min-height: 100svh; min-height: 100lvh; display: flex; flex-direction: column;
  justify-content: flex-start; padding-top: clamp(110px, 15vh, 190px);
}
.section--verticals:has(.ring) .section-head { color: #fff; }
.section--verticals:has(.ring) .lede { color: rgba(255,255,255,.84); }

/* ============================================================
   Platform beams — six stages wired into the RAIL Platform hub
   ============================================================ */
.beam { position: relative; display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: clamp(20px, 6vw, 88px); margin-top: 52px; }
.beam__svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; overflow: visible; }
.beam__col { position: relative; z-index: 1; display: flex; flex-direction: column; gap: clamp(14px, 2.6vh, 30px); }
.beam__node {
  display: flex; align-items: center; gap: 13px; padding: 14px 16px; border-radius: 14px;
  background: linear-gradient(155deg, rgba(120,160,255,.14), rgba(10,18,36,.55));
  -webkit-backdrop-filter: blur(12px) saturate(1.25); backdrop-filter: blur(12px) saturate(1.25);
  border: 1px solid rgba(140,180,255,.2); box-shadow: 0 18px 42px -28px rgba(0,0,0,.85);
}
.beam__node [data-icon] { display: block; width: 24px; height: 24px; flex: none; color: var(--sky); }
.beam__node b { display: block; font-family: var(--font-display); font-size: .96rem; color: #fff; letter-spacing: -.01em; }
.beam__node i { display: block; font-style: normal; font-size: .78rem; line-height: 1.4; color: rgba(255,255,255,.6); margin-top: 3px; }
.beam__hub {
  position: relative; z-index: 1; justify-self: center;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 26px 24px; border-radius: 20px; text-align: center;
  background: linear-gradient(155deg, rgba(120,160,255,.2), rgba(10,18,36,.6));
  -webkit-backdrop-filter: blur(16px) saturate(1.4); backdrop-filter: blur(16px) saturate(1.4);
  border: 1px solid rgba(140,180,255,.42);
  /* Tight glow, layered outward so it falls off like light rather than like a
     ring. Lives on box-shadow rather than a pseudo-element because .beam__hub
     is a stacking context (position + z-index), which would paint a -1 child
     ON TOP of its own background instead of behind it. */
  box-shadow:
    0 0 0 1px rgba(150,190,255,.16),
    0 0 22px -4px rgba(76,141,255,.55),
    0 0 54px 2px rgba(76,141,255,.30),
    0 0 110px 18px rgba(76,141,255,.16),
    0 28px 60px -30px rgba(0,0,0,.9),
    inset 0 1px 0 rgba(255,255,255,.22);
}

/* Ambient halo behind the hub. It hangs off .beam, not .beam__hub, so it can
   sit UNDER the beams and the tile — as the hub's first child it would be
   trapped above the tile's own background. The breathe is transform+opacity
   only, so it stays on the compositor, and it is parked until the diagram is
   actually on screen (the dashPan animation taught us not to leave loops
   running thousands of px off-screen). */
.beam::before {
  /* --hubx/--huby are written by drawBeams(); the hub is centre-stage on desktop
     but sits at the top of the grid on mobile, so a fixed 50%/50% missed it */
  content: ''; position: absolute; left: var(--hubx, 50%); top: var(--huby, 50%);
  width: clamp(280px, 34vw, 460px); aspect-ratio: 1; border-radius: 50%;
  transform: translate(-50%, -50%); pointer-events: none; z-index: 0;
  background: radial-gradient(closest-side,
    rgba(76,141,255,.30) 0%, rgba(76,141,255,.15) 42%, rgba(76,141,255,.05) 66%, transparent 78%);
}
.js .beam::before { animation: hubBreathe 5.2s ease-in-out infinite alternate; animation-play-state: paused; }
.js .beam.is-live::before { animation-play-state: running; }
@keyframes hubBreathe {
  from { opacity: .68; transform: translate(-50%, -50%) scale(.94); }
  to   { opacity: 1;   transform: translate(-50%, -50%) scale(1.07); }
}
@media (prefers-reduced-motion: reduce) {
  .js .beam::before { animation: none; opacity: .85; }
}
.beam__hub img { width: 52px; height: auto; }
.beam__hub b { font-family: var(--font-display); font-size: 1.04rem; color: #fff; white-space: nowrap; }

/* Mobile: a 2-up grid under the hub, not a single stack. Six full-width rows
   read as a list and pushed the section past two screens; paired compact tiles
   fit it on one and keep the hub as the thing everything sits under.
   `display: contents` dissolves the two .beam__col wrappers so their six nodes
   become grid items of .beam directly — with the hub pulled first by order:-1
   and spanning both columns, auto-placement then fills the 2x3 grid cleanly. */
@media (max-width: 760px) {
  .beam { grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 36px; align-items: stretch; }
  .beam__col { display: contents; }
  .beam__hub { grid-column: 1 / -1; order: -1; margin-bottom: 6px; padding: 16px 22px; }
  .beam__hub img { width: 40px; }
  .beam__node {
    /* .beam__col carried position+z-index on desktop; with it dissolved to
       `display: contents` the tiles need their own, or the wires paint over them */
    position: relative; z-index: 1;
    flex-direction: column; align-items: flex-start; gap: 8px;
    padding: 13px 12px; border-radius: 12px;
    /* The wires sweep down THROUGH the 2-up grid here, not the side gutters, and
       the desktop glass let them burn at near-full strength over the copy. This
       is the same composite as that glass pre-mixed over --ink-deep, just near
       solid — text stays legible and the beams read as passing underneath. */
    background: linear-gradient(155deg, rgba(23,41,70,.94), rgba(9,20,38,.96));
  }
  .beam__node [data-icon] { width: 20px; height: 20px; }
  .beam__node b { font-size: .84rem; line-height: 1.2; }
  .beam__node i { font-size: .7rem; line-height: 1.32; margin-top: 2px; }
}

/* ============================================================
   Sticky footer reveal — the footer is held behind the page and
   uncovered as the content above scrolls off it
   ============================================================ */
/* The outer box is the only thing that occupies flow (so document height is unchanged).
   clip-path both clips the overflow and gives the sticky child a containing block. The
   inner is a viewport taller and pulled up by that amount, which is what gives the sticky
   child a range to have already parked in BEFORE the outer scrolls into view — so the
   footer reads as being revealed rather than sliding up. */
/* --fh hugs the footer's real content (top block 230 + legal row 44 + padding) rather than a
   round 60vh — at 540px there was ~175px of dead space between the two blocks. */
.footer-reveal { --fh: clamp(340px, 40vh, 400px);
  position: relative; height: var(--fh); clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
.footer-reveal__inner { position: relative; height: calc(100vh + var(--fh)); top: -100vh; }
.footer-reveal__stick { position: sticky; top: calc(100vh - var(--fh)); height: var(--fh); }
.footer-reveal .footer {
  height: 100%; padding: clamp(38px, 5vh, 64px) 0 26px;
  display: flex; flex-direction: column; justify-content: space-between;
}
/* Mobile stacks the brand block, the CTAs and the nav, so the content is ~605px — the
   container has to clear that or the legal row gets clipped by the reveal's clip-path. */
/* .footer__top collapses to one column at 920, not 620, so the whole 620-920 band was stacking
   ~500px of footer into a 400px reveal and clipping the legal row off the bottom — a
   pre-existing bug that A6's 44px tap targets deepened by another 48px. This band hugs the
   stacked-but-not-yet-wrapped footer (~548px at 820). The 620 rule below still wins on phones. */
@media (max-width: 920px) { .footer-reveal { --fh: clamp(576px, 50vh, 620px); } }
/* 620 was sized to a ~605px stack. A6 gave .footer__nav a 44px tap targets, which takes the
   3-row nav from 88px to 136px and the stack to ~627px — at 620 the legal row was clipped by
   the reveal's clip-path by 8px. Re-hugged to the real content with the same ~25px headroom. */
@media (max-width: 620px) { .footer-reveal { --fh: clamp(652px, 64vh, 732px); } }
/* Short viewports can't spare a screen of scroll for the reveal — fall back to a normal footer */
@media (max-height: 560px) {
  .footer-reveal { height: auto; clip-path: none; }
  .footer-reveal__inner { height: auto; top: 0; }
  .footer-reveal__stick { position: static; height: auto; }
  .footer-reveal .footer { height: auto; }
}

/* ============================================================
   Narrow-viewport refinements (A3 / A5 / A6)
   Appended last on purpose: every rule here intentionally wins on source order over the
   earlier @media (max-width:920px) block. Nothing here re-declares `position`, `display`
   or `transform` for .hero--teardown / .orbit — kiosk3d.js drives those inline.
   ============================================================ */
@media (max-width: 920px) {
  /* A3: .reveal--slide enters from translate3d(±34px, …). In a 2-up grid at 820 that put the
     .pillar tiles at left:-6 / right:826 on a 820 viewport — clipped by html{overflow-x:hidden},
     so tiles visibly slid in from under the screen edge. Below 920 the horizontal travel is
     dropped and the move becomes a straight lift + settle, matching what [data-enter] already
     does at this width. The .is-visible resting states are higher-specificity, so they still win. */
  .reveal--slide,
  .reveal--slide:nth-child(even) { transform: translate3d(0, 30px, 0) scale(.978); }

  /* A6: tap targets. .nav__toggle measured 40x25 and .footer__nav a measured 20px tall.
     min-height is against the border box (* { box-sizing: border-box }), so 12px padding on a
     ~20px line box lands exactly on 44. Row gap drops 14px -> 2px so the nav block barely grows. */
  .nav__toggle { min-width: 44px; min-height: 44px; align-items: center; justify-content: center; }
  /* Same 44px rule for the contact links — mail/tel are the likeliest phone taps on the page. */
  .footer__contact { gap: 2px; }
  .footer__contact a, .reserve__direct a { display: inline-block; padding: 12px 0; min-height: 44px; }
  .reserve__direct { gap: 2px; }
}
.footer__nav { gap: 2px 40px; }
.footer__nav a { display: inline-block; padding: 12px 0; min-height: 44px; }

/* NB on the A4 mask stops: 11%/89% is the widest safe ramp. The ring passes through a moment
   where TWO tiles share the front at ~96% of peak magnification, and the trailing one reaches
   x=345 on a 390 viewport. A 15% or 20% ramp starts fading at 331/312 and clips that tile's
   copy mid-word — the exact defect the mask exists to remove. Do not widen these stops; the
   far tiles are taken to zero by the opacity ramp in JS, which is the other half of this fix. */

/* A5: at 820x1180 the mobile teardown (tuned for 390) stretched the step tile edge to edge —
   788px wide holding ~55% of that in copy, marooned in ~660px of empty dark. Cap and centre it.
   Centring is done with inset-inline + margin-inline:auto, NOT a transform: kiosk3d.js writes
   `transform` directly onto [data-step="0"] to drive the retract and would overwrite it. */
@media (min-width: 621px) and (max-width: 920px) {
  body.has-kiosk-scroll .hero--teardown .hero__step[data-step="0"],
  body.has-kiosk-scroll .hero--teardown .hero__step:not([data-step="0"]) {
    left: 0; right: 0;
    width: min(560px, calc(100% - 48px));
    margin-inline: auto;
  }
}

/* ============================================================
   Seam bridges  (Loop 2)
   Every light<->dark boundary was a hard `1px solid var(--line)` cut, which
   is what made the page read as a stack of slides rather than one surface.
   Bleed the OUTGOING section's colour ~110px into the entering one so the
   change of ground resolves as a dissolve.

   Static gradients only. An animated treatment was tried before and reverted
   for feeling glitchy — this must cost nothing at scroll time.

   Notes on the selectors:
   - `.section--replace` (the pinned kiosk stage) deliberately has no `.dark`
     class, so it is excluded for free. A bridge there would be pinned with
     the section and sit as a bright band over the stage for the whole pin.
   - `#capabilities` is only ever preceded by `.hero`, not `.section`, so the
     sibling combinator never reaches it. That matters: it is the finale
     hand-off target and its clone must stay pixel-identical.
   - Colours are carried as space-separated channels so the fade can end on
     `rgb(... / 0)`. Fading to the `transparent` keyword interpolates through
     transparent BLACK in sRGB, which greys out the middle of a light bridge.
   ============================================================ */
/* `border-top-color: transparent` is NOT enough: backgrounds paint under the
   border box by default, so a transparent border just lets the section's own
   dark background show through as a 1px line — the exact artefact we're
   removing. Take the width to 0. */
.section:not(.dark) + .section.dark,
.section.dark + .section:not(.dark) { border-top-width: 0; }

.section:not(.dark) + .section.dark::before,
.section.dark + .section:not(.dark)::before {
  content: ''; position: absolute; inset: 0 0 auto; height: clamp(60px, 6vw, 92px);
  pointer-events: none;
  /* -1 paints above the section's own background but below in-flow content,
     so the bridge can never cover copy. */
  z-index: -1;
  /* Deliberately faint. A strong bleed reads as a white haze washing into the
     dark section, which looks like a rendering fault rather than a transition.
     This is a vignette that softens the edge, not a cross-fade. */
  background: linear-gradient(to bottom,
    rgb(var(--seam-rgb) / .20) 0%,
    rgb(var(--seam-rgb) / .11) 30%,
    rgb(var(--seam-rgb) / .04) 62%,
    rgb(var(--seam-rgb) / 0)  100%);
}

/* entering a dark section: bleed the outgoing paper down into it */
.section:not(.dark) + .section.dark { --seam-rgb: 250 251 252; }   /* --paper   */
/* `:not(.dark)` is carried purely for specificity: without it these are (0,3,0)
   and lose to the generic `.section:not(.dark) + .section.dark` above at
   (0,4,0), so every paper-2 section was bleeding the wrong white. Matching the
   specificity lets source order decide, and these come later. */
.section--proof:not(.dark)     + .section.dark,
.section--advantage:not(.dark) + .section.dark,
.section--story:not(.dark)     + .section.dark { --seam-rgb: 234 239 245; }   /* --paper-2 */

/* The investors page opens on `.hero.dark`, which is not a `.section`, so the
   sibling rules never reached the first boundary on that page. */
.hero.dark + .section:not(.dark) { border-top-width: 0; isolation: isolate; --seam-rgb: 7 22 40; }
.hero.dark + .section:not(.dark)::before {
  content: ''; position: absolute; inset: 0 0 auto; height: clamp(60px, 6vw, 92px);
  pointer-events: none; z-index: -1;
  background: linear-gradient(to bottom,
    rgb(var(--seam-rgb) / .20) 0%,
    rgb(var(--seam-rgb) / .11) 30%,
    rgb(var(--seam-rgb) / .04) 62%,
    rgb(var(--seam-rgb) / 0)  100%);
}

/* entering a light section: bleed the outgoing navy down into it */
.section.dark + .section:not(.dark) { --seam-rgb: 7 22 40; }       /* --ink-deep */

/* The kiosk stage keeps a hard edge, but it was inheriting the LIGHT divider
   (it has no `.dark` class), so a #D8E0EA hairline was sitting across the top
   of a near-black section. */
.section + .section--replace,
.section--replace + .section { border-top-color: rgba(255,255,255,.09); }

/* ============================================================
   Loop 2 — craft fixes
   ============================================================ */

/* The pullquote painted --grad (royal -> sky) as text on --ink-deep. The royal
   end measures ~3.0:1 — the first half of the sentence was the hardest thing
   on the page to read. Same gesture, rebalanced for a dark ground: both stops
   now sit above 5:1. */
.pullquote { background: linear-gradient(118deg, var(--sky) 0%, var(--sky-soft) 100%);
  -webkit-background-clip: text; background-clip: text; }

/* GSAP wraps a pinned section in a `.pin-spacer` div, which severs the sibling
   relationship the seam rules above depend on — so the boundary coming OUT of a
   pinned section was still a hard cut. Both pinned sections here are dark, so a
   spacer always means "dark ground above". (Without GSAP no spacer is inserted
   and the plain sibling rules already cover it.) */
.pin-spacer + .section:not(.dark) { border-top-width: 0; --seam-rgb: 7 22 40; }
.pin-spacer + .section:not(.dark)::before {
  content: ''; position: absolute; inset: 0 0 auto; height: clamp(60px, 6vw, 92px);
  pointer-events: none; z-index: -1;
  background: linear-gradient(to bottom,
    rgb(var(--seam-rgb) / .20) 0%,
    rgb(var(--seam-rgb) / .11) 30%,
    rgb(var(--seam-rgb) / .04) 62%,
    rgb(var(--seam-rgb) / 0)  100%);
}
/* the kiosk stage bottoms out on #0d0e11, not --ink-deep */
#platform-in-one { --seam-rgb: 13 14 17; }

/* `.section` is position:relative with z-index:auto, so it does NOT form a
   stacking context — which meant the z-index:-1 bridge resolved against a
   distant ancestor and painted BEHIND the section's own background, i.e.
   invisibly. `isolation` creates the stacking context without changing how the
   section orders against its siblings, so -1 now means "above my background,
   below my content", which is exactly the layer the bridge needs. */
.section:not(.dark) + .section.dark,
.section.dark + .section:not(.dark),
.pin-spacer + .section:not(.dark) { isolation: isolate; }

/* ============================================================
   Operator dashboard carousel  (#visibility)
   A chip wheel drives a stack of dashboard shots. Vertical wheel at
   desktop/tablet; on phones the wheel becomes a horizontal scroller,
   because a vertical one eats the whole screen. script.js owns state.
   ============================================================ */
.fcar {
  /* Symmetric ease-in-out, and long enough that the expansion is a movement
     rather than a state change. Both are read by .fcar__card and .fcar__cap. */
  --fcar-ease: cubic-bezier(.42, 0, .22, 1);
  --fcar-dur: .92s;
  display: grid; grid-template-columns: minmax(230px, 296px) 1fr;
  margin-top: 48px; border-radius: 28px; overflow: hidden;
  border: 1px solid rgba(255,255,255,.10);
  background: #0B1523;
  box-shadow: 0 40px 90px -50px rgba(0,0,0,.9);
}

/* ---- left rail ---- */
.fcar__rail {
  position: relative; overflow: hidden;
  /* Blue-tinted glass rather than a solid royal panel. It samples .fcar's own
     #0B1523 stage, so the composite lands around #192F53 at the top and #0B1526
     at the foot — dark enough that the white chip copy measures ~9:1, better
     than the 6.8:1 the solid royal gave it. (Sky-blue glass would have been the
     obvious choice and is the one to avoid: it lifts the panel to ~3:1.) */
  background: linear-gradient(158deg,
    rgba(76,141,255,.22) 0%, rgba(20,71,230,.17) 52%, rgba(10,22,44,.30) 100%);
  -webkit-backdrop-filter: blur(22px) saturate(1.5); backdrop-filter: blur(22px) saturate(1.5);
  border-right: 1px solid rgba(140,180,255,.16);
}
.fcar__rail::before, .fcar__rail::after {
  content: ''; position: absolute; left: 0; right: 0; height: 74px; z-index: 2; pointer-events: none;
}
/* these sit OVER the chips to dissolve them at the ends of the wheel, so they
   have to match the composited glass, not the tint that produces it */
.fcar__rail::before { top: 0; background: linear-gradient(to bottom, #192F53, rgba(25,47,83,0)); }
.fcar__rail::after { bottom: 0; background: linear-gradient(to top, #0B1526, rgba(11,21,38,0)); }

.fcar__wheel { position: relative; height: 100%; min-height: 430px; }
.fcar__chip {
  position: absolute; left: 28px; top: 50%; margin-top: -21px;
  display: inline-flex; align-items: center; gap: 11px;
  height: 42px; padding: 0 20px; border-radius: 999px; white-space: nowrap;
  font-family: var(--font-body); font-size: .82rem; letter-spacing: .01em;
  background: transparent; color: rgba(255,255,255,.80);
  border: 1px solid rgba(255,255,255,.26); cursor: pointer;
  transition: background .4s var(--fcar-ease), color .4s var(--fcar-ease),
              border-color .4s var(--fcar-ease),
              transform var(--fcar-dur) var(--fcar-ease), opacity var(--fcar-dur) var(--fcar-ease);
}
.fcar__chip:hover { color: #fff; border-color: rgba(255,255,255,.5); }
.fcar__chip[aria-selected="true"] {
  background: #fff; color: var(--royal); border-color: #fff; font-weight: 600; z-index: 3;
}
.fcar__ico { width: 16px; height: 16px; flex: none; opacity: .78; }
.fcar__chip[aria-selected="true"] .fcar__ico { opacity: 1; }

/* ---- right stage ---- */
.fcar__stage {
  position: relative; display: grid; place-items: center;
  padding: clamp(22px, 3vw, 44px);
  background: radial-gradient(120% 100% at 50% 0%, #12243A 0%, #0A1421 70%);
  border-left: 1px solid rgba(255,255,255,.08);
  min-height: 430px;
}
.fcar__card {
  grid-area: 1 / 1; margin: 0; width: 100%; max-width: 860px;
  border-radius: 14px; overflow: hidden; background: #0A1622;
  border: 1px solid rgba(255,255,255,.12);
  box-shadow: 0 30px 60px -34px rgba(0,0,0,.95);
  /* NOT var(--ease). That curve is cubic-bezier(.16,1,.3,1) — a hard expo-out
     that puts opacity at .30 by 55ms and .90 by 220ms of a 620ms transition,
     so the swap lands almost instantly and then creeps invisibly for the
     remaining 400ms. That reads as a snap followed by dead air. Expo-out is
     right for something arriving from off-screen; it is wrong for a card
     expanding in place, which needs to be legible across its whole duration. */
  transition: transform var(--fcar-dur) var(--fcar-ease),
              opacity calc(var(--fcar-dur) * .82) var(--fcar-ease),
              filter calc(var(--fcar-dur) * .9) var(--fcar-ease);
  /* No `will-change` here on purpose. Eight cards holding a permanent
     compositor layer put the page's promoted-element count from 3 back up to
     17; Chrome promotes for the duration of a running transform/opacity
     transition anyway, so the animation is unaffected. */
}
.fcar__card img { display: block; width: 100%; height: auto; }
/* `hidden` stays honoured so a no-JS visitor sees one card rather than eight
   stacked on the same grid cell. script.js strips it at init and swaps to
   aria-hidden, which is the attribute that still works once all eight are
   painted on top of each other. */

/* three visible positions; everything else is parked and inert */
.fcar__card[data-pos="active"] { transform: none; opacity: 1; filter: none; z-index: 3; }
/* Kept quiet on purpose: these only exist to say "there is more here". At the
   reference's opacity they read as a second, competing dashboard. */
.fcar__card[data-pos="prev"],
.fcar__card[data-pos="next"] { opacity: .20; filter: grayscale(1) brightness(.55) blur(1px); z-index: 2; }
.fcar__card[data-pos="prev"] { transform: translateX(-9%) scale(.9) rotate(-2deg); }
.fcar__card[data-pos="next"] { transform: translateX(9%) scale(.9) rotate(2deg); }
.fcar__card[data-pos="off"] { opacity: 0; transform: scale(.72); z-index: 1; pointer-events: none; }

.fcar__cap {
  /* The tall padding + deep scrim are one unit: the caption overlays the mock's
     bottom rows (Recent Intake / Payout Ledger tables), and at the old
     26%-.94 / 62%-.55 ramp the rows read at near-full strength behind the badge
     — white caption text interleaving with fake UI rows looked like an overlap
     glitch. The badge and copy now sit on the ≥.78 band and the rows dissolve
     over the fade instead of cutting through it. */
  position: absolute; inset: auto 0 0 0; padding: 84px 22px 20px;
  background: linear-gradient(to top, rgba(4,10,18,.96) 42%, rgba(4,10,18,.78) 66%, rgba(4,10,18,.28) 86%, transparent);
  opacity: 0; transform: translateY(12px);
  /* trails the card so the copy lands after the frame has settled */
  transition: opacity calc(var(--fcar-dur) * .6) var(--fcar-ease) calc(var(--fcar-dur) * .34),
              transform calc(var(--fcar-dur) * .6) var(--fcar-ease) calc(var(--fcar-dur) * .34);
}
.fcar__card[data-pos="active"] .fcar__cap { opacity: 1; transform: none; }
.fcar__badge {
  display: inline-block; margin-bottom: 9px; padding: 5px 11px; border-radius: 999px;
  background: rgba(255,255,255,.94); color: var(--ink);
  font-family: var(--font-mono); font-size: .62rem; letter-spacing: .18em; text-transform: uppercase;
}
.fcar__cap p { margin: 0; color: #fff; font-size: clamp(.92rem, 1.35vw, 1.1rem); line-height: 1.35; max-width: 46ch; }

/* ---- narrow desktop: rail keeps the wheel but tightens ---- */
@media (max-width: 1120px) {
  .fcar { grid-template-columns: minmax(206px, 250px) 1fr; }
  .fcar__chip { left: 18px; font-size: .76rem; height: 38px; padding: 0 15px; margin-top: -19px; }
}

/* ---- tablet and below: stack, and turn the wheel into a horizontal scroller.
   The side-by-side layout leaves the dashboard ~470px wide at 820, where a
   1200px screenshot is pure texture — no operator could read a figure. Stacking
   buys back the full width and the data becomes legible again. ---- */
@media (max-width: 1000px) {
  /* min-width: 0 everywhere in the stack: without it a child's min-content width
     propagates up through the grid and pushes the wrap ~35px past the viewport
     in the 921-1000px band (the reserve-form button bug, same mechanism) */
  .fcar { grid-template-columns: 1fr; margin-top: 34px; border-radius: 20px; min-width: 0; max-width: 100%; }
  .fcar > *, .fcar__stage, .fcar__card, .fcar__card > * { min-width: 0; max-width: 100%; }
  .fcar__rail { padding: 16px 0; }
  .fcar__rail::before, .fcar__rail::after { width: 30px; height: auto; top: 0; bottom: 0; }
  /* stacked layout: the glass runs left-to-right, so the composite is even —
     fade both ends from the same mid tint */
  .fcar__rail::before { left: 0; right: auto; background: linear-gradient(to right, #14274A, rgba(20,39,74,0)); }
  .fcar__rail::after { right: 0; left: auto; background: linear-gradient(to left, #14274A, rgba(20,39,74,0)); }
  .fcar__rail { border-right: 0; border-bottom: 1px solid rgba(140,180,255,.16); }
  .fcar__wheel {
    min-height: 0; height: auto; display: flex; gap: 9px;
    overflow-x: auto; scroll-snap-type: x mandatory;
    padding: 0 56px 0 26px; -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  /* The strip usually overruns the viewport and the last pill was cut mid-word with no
     signal that the row continues — the 30px tint fades don't survive compositing over
     a white-bordered pill. Dissolve the pills THEMSELVES at the edge instead. The mask
     lives on the RAIL, not the wheel: masks on a scroll container scroll away with its
     content, so there the right fade sat past the 1200px-wide strip's far end, never in
     view. The wheel's 56px right padding matches the fade, so a chip snapped fully into
     view always sits in the opaque zone. */
  .fcar__rail {
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 22px, #000 calc(100% - 52px), transparent);
    mask-image: linear-gradient(to right, transparent 0, #000 22px, #000 calc(100% - 52px), transparent);
  }
  .fcar__wheel::-webkit-scrollbar { display: none; }
  /* static flow overrides the wheel's absolute positioning */
  .fcar__chip { position: static; margin: 0; flex: none; scroll-snap-align: center; opacity: 1 !important; transform: none !important; }
  .fcar__stage { min-height: 0; border-left: 0; border-top: 1px solid rgba(255,255,255,.08); padding: 20px 16px 22px; }
  .fcar__card[data-pos="prev"], .fcar__card[data-pos="next"] { opacity: 0; transform: scale(.8); }
  .fcar__cap { padding: 44px 15px 15px; }
  .fcar__cap p { font-size: .88rem; }
}

@media (prefers-reduced-motion: reduce) {
  .fcar__chip, .fcar__card, .fcar__cap { transition: none; }
  .fcar__card[data-pos="prev"], .fcar__card[data-pos="next"] { opacity: 0; }
}

/* Desktop windows a little shorter than the section (832-999px tall): with the
   default padding #visibility ran ~950px, pinned 'bottom bottom', and the heading
   spent the whole 1900px pin sliced under the 74px fixed nav. Compact the section
   in this band so it fits the window and the pin holds it from the top with the
   heading clear (the start() function in script.js prefers 'top top' whenever the
   bottom padding can absorb the leftover overflow). 92px top padding keeps the
   heading under, not behind, the nav while pinned. */
@media (min-width: 921px) and (max-height: 999px) {
  .section--visibility:has([data-fcar]) { padding-top: 92px; padding-bottom: 48px; }
  .section--visibility:has([data-fcar]) .fcar { margin-top: 28px; }
}
/* side-by-side layout (>1000px): the dashboard card cedes width as the window gets
   shorter (220vh − 1043px ≈ the widest card whose stage still lets the section fit),
   and the chip wheel's floor drops so it stops propping the section taller than the
   window. The wheel is a scroller by design — a 380px window of it still reads. */
@media (min-width: 1001px) and (max-height: 999px) {
  .section--visibility:has([data-fcar]) .fcar__card { max-width: clamp(460px, 220vh - 1043px, 680px); }
  .section--visibility:has([data-fcar]) .fcar__wheel,
  .section--visibility:has([data-fcar]) .fcar__stage { min-height: 380px; }
}
/* stacked layout (921-1000px): same idea, fixed cap — the full-bleed screenshot is
   what made the stacked section outrun an 832px window */
@media (min-width: 921px) and (max-width: 1000px) and (max-height: 999px) {
  .section--visibility:has([data-fcar]) .fcar__card { max-width: 560px; }
}

/* On a phone the card is only ~156px tall, so an overlaid caption swallows
   more than half the dashboard. Drop it below the image instead — the shot
   stays whole and the copy stays readable. */
@media (max-width: 620px) {
  .fcar__card { display: flex; flex-direction: column; }
  .fcar__cap {
    position: static; padding: 12px 14px 14px; background: #0A1622;
    opacity: 1; transform: none; border-top: 1px solid rgba(255,255,255,.08);
  }
  .fcar__badge { margin-bottom: 7px; }
}

/* Introduces the contact form: grows out from --x (scale origin) at --y (its top),
   both written by JS. Kept from the landing gate, which is where this originated. */
.gate__line {
  position: absolute; left: 0; top: var(--y, 100%); width: 100%; height: 2px;
  border-radius: 2px; pointer-events: none;
  transform: scaleX(0); transform-origin: var(--x, 50%) 50%;
  background: linear-gradient(90deg, transparent, var(--sky) 18%, #BFD8FF 50%, var(--sky) 82%, transparent);
  box-shadow: 0 0 10px rgba(76,141,255,.9), 0 0 26px rgba(76,141,255,.45);
  opacity: 0; transition: transform .58s var(--ease), opacity .3s var(--ease);
}
.reserve__reveal.is-open .gate__line { transform: scaleX(1); opacity: 1; }

/* Blind reveal: 0fr -> 1fr animates the height smoothly, the inset clip pulls the
   leading edge down over it so it unrolls rather than simply growing. */
.gate__drop {
  display: grid; grid-template-rows: 0fr; width: 100%;
  transition: grid-template-rows .78s var(--ease) .22s;
}
.gate__drop > * {
  overflow: hidden; min-height: 0;
  /* Insets are negative on three sides so the finished state does NOT clip the
     tile's own glow — clip-path cuts box-shadow, and inset(0) would shave the
     halo off flush with the border box. Only the bottom edge animates, which is
     what makes it read as a blind rather than a box growing. */
  clip-path: inset(-72px -72px 100% -72px);
  transition: clip-path .78s var(--ease) .22s;
}
.reserve__reveal.is-open .gate__drop { grid-template-rows: 1fr; }
.reserve__reveal.is-open .gate__drop > * { clip-path: inset(-72px); }

.gform {
  margin-top: 16px; padding: 22px; border-radius: 18px; text-align: left;
  background: linear-gradient(165deg, rgba(28,50,96,.72), rgba(8,18,34,.82));
  -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3);
  border: 1px solid rgba(140,180,255,.28);
  box-shadow: 0 24px 60px -34px rgba(0,0,0,.95);
  transition: box-shadow .7s var(--ease) .5s, border-color .7s var(--ease) .5s;
}
/* the tile lights up once it has finished unrolling */
.reserve__reveal.is-open .gform {
  border-color: rgba(150,190,255,.44);
  box-shadow:
    0 0 0 1px rgba(150,190,255,.14),
    0 0 26px -6px rgba(76,141,255,.45),
    0 0 70px 4px rgba(76,141,255,.22),
    0 24px 60px -34px rgba(0,0,0,.95);
}
.gform__h { margin: 0 0 6px; font-family: var(--font-display); font-size: 1.24rem; color: #fff; }
.gform__sub { margin: 0 0 16px; font-size: .85rem; line-height: 1.45; color: rgba(255,255,255,.62); }
.gform__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
.gform .field label { color: rgba(255,255,255,.68); }
.gform .field input, .gform .field select, .gform .field textarea {
  background: rgba(6,14,28,.6); border-color: rgba(150,190,255,.24); color: #fff;
}
.gform .field input:focus, .gform .field select:focus, .gform .field textarea:focus { border-color: var(--sky); }
.gform .form__note { color: var(--sky-soft); }


@media (prefers-reduced-motion: reduce) {
  .gate__line, .gate__drop, .gate__drop > *, .gform { transition: none; }
}

/* ============================================================
   iOS safe areas
   With the default viewport-fit=auto, Safari insets the web view by the
   home-indicator strip and fills that strip ITSELF — in light mode that fill
   is white, which is the bar that reappears after a scroll and never clears.
   `viewport-fit=cover` (both pages) hands the strip to the page instead, so
   the dark body paints it. Everything below just keeps content out from under
   the indicator and the status bar now that we own those regions.

   NOTE: deliberately NOT applied to .wrap. Its horizontal padding is part of
   the finale hand-off measurement, and the landscape inset is not worth
   touching that.
   ============================================================ */
.nav {
  height: calc(var(--nav-h) + env(safe-area-inset-top, 0px));
  padding-top: env(safe-area-inset-top, 0px);
}
.footer { padding-bottom: calc(34px + env(safe-area-inset-bottom, 0px)); }

/* --- contact form reveal --- the line grows from the centre and sits at the top
   of the wrapper. Originated on the landing gate, which has since been removed. */
.reserve__reveal { position: relative; }
.reserve__reveal .gate__line { --x: 50%; --y: 0px; }
/* .gform lands after .reserve__form in source, so its glass skin already wins on
   padding/background/border/shadow; the grid itself still comes from
   .reserve__form. Only the width cap needs undoing. */
/* Real glass, same as the gate tile — which is why the section under it had to
   go dark. backdrop-filter can only work with what's behind it: over --paper it
   sampled white and came back muddy grey, which is no kind of glass. */
.reserve__form.gform { max-width: none; }

/* --- the CTA section as a dark stage, so the glass has something to hold --- */
.section--reserve.dark {
  background: radial-gradient(900px 640px at 50% 26%, #12203a, var(--ink-deep) 72%);
}
.section--reserve.dark .section-head { color: #fff; }
.section--reserve.dark .lede { color: rgba(255,255,255,.84); }
.section--reserve.dark .reserve__points li { color: rgba(255,255,255,.88); }
.section--reserve.dark .reserve__points span { color: var(--sky); }
.section--reserve.dark .reserve__flag { color: rgba(255,255,255,.56); }
.section--reserve.dark .reserve__direct a,
.section--reserve.dark .reserve__direct-place { color: rgba(255,255,255,.88); }
.section--reserve.dark .reserve__direct a:hover { color: var(--sky); }
.section--reserve.dark .reserve__direct-label { color: rgba(255,255,255,.5); }
.reserve__form.gform .btn { margin-top: 4px; }
@media (prefers-reduced-motion: reduce) {
  .reserve__reveal .gate__line, .reserve__reveal .gate__drop,
  .reserve__reveal .gate__drop > * { transition: none; }
}

/* `1fr` resolves its automatic minimum to min-content, so a wide field — the mono
   uppercase labels, or the select's longest option — widens its own track past an
   equal share and pushes the whole grid out of the padding box. On #contact at
   1512 that gave tracks of 211px and 279.5px and left the right-hand fields 6.5px
   from the edge against 23px on the left. min-width:0 lets the tracks be equal.
   Same root cause as the 390px submit-button overflow fixed earlier. */
.field { min-width: 0; }
.field input, .field select, .field textarea { max-width: 100%; }

/* ============================================================
   Proof sequence (#traction)
   The section pins and each card plays in turn: slide up, then draw its
   checkmark, then the next one starts. --p (slide) and --c (check) are written
   per card by script.js off the pin progress.

   Both default to 1 so the section is fully visible with no JS, with GSAP
   missing, or under reduced motion — script.js only zeroes them when it is
   actually going to drive them.
   ============================================================ */
.proof__item {
  --p: 1; --c: 1;
  opacity: calc(.05 + .95 * var(--p));
  transform: translateY(calc((1 - var(--p)) * 44px));
  will-change: auto;
}
/* hover has to carry the reveal offset too, or hovering a half-revealed card
   snaps it to its final position */
.proof__item:hover { transform: translateY(calc((1 - var(--p)) * 44px - 5px)); }

.proof__check {
  display: grid; place-items: center;
  width: 30px; height: 30px; margin-bottom: 16px; border-radius: 50%;
  background: rgba(20,71,230, calc(var(--c) * .10));
  box-shadow: 0 0 0 1px rgba(20,71,230, calc(var(--c) * .22));
  transform: scale(calc(.82 + .18 * var(--c)));
}
.proof__check svg { width: 17px; height: 17px; display: block; overflow: visible; }
.proof__check path {
  fill: none; stroke: var(--royal); stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
  /* path measures ~23.4 units; 26 clears it with room to spare */
  stroke-dasharray: 26; stroke-dashoffset: calc(26 - 26 * var(--c));
}

/* Keep 2x2 rather than a single column on phones: the pin holds the section in
   the viewport, so four stacked cards would not fit at 390 and the sequence
   would play partly off screen. */
@media (max-width: 620px) {
  .proof { grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 34px; }
  .proof__item { padding: 20px 16px; }
  .proof__lead { font-size: clamp(1.15rem, 5.6vw, 1.5rem); }
  .proof__label { font-size: .64rem; letter-spacing: .04em; margin-top: 8px; }
  .proof__check { width: 24px; height: 24px; margin-bottom: 11px; }
  .proof__check svg { width: 14px; height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .proof__item { opacity: 1; transform: none; }
}

/* The pinned proof section has to fill the viewport, or it sticks at the top and
   leaves the rest of the screen showing the spacer behind it. Scoped by
   :has([data-proof]) so it only touches the pinned one on the platform page —
   the investors page reuses .section--proof unpinned. */
.section--proof:has([data-proof]) {
  min-height: 100svh; min-height: 100lvh;
  display: flex; flex-direction: column; justify-content: center;
}
/* clears the 74px fixed nav while pinned — the centred layout parked the h2 at
   65px, under the bar. Same fix as .replace__intro; the proof grid ends ~380px
   above the fold at 390x844, so the push-down costs nothing below. */
@media (max-width: 920px) {
  .section--proof:has([data-proof]) { padding-top: clamp(102px, 13.5vh, 126px); }
}
/* `body.has-kiosk-scroll .pin-spacer { background: #0d0e11 }` exists because the
   other three pinned sections are dark and iOS can expose the spacer when the
   toolbar retracts. This one is --paper-2, so it needs its own colour or it
   shows as a black band under a light section. */


/* ============================================================
   Capability tiles fill in sequence (#capabilities)
   The section pins and each tile turns royal in turn. --b is written per tile
   by script.js off the pin progress.

   Defaults to 0 (white) for a reason beyond no-JS: #capScreen is a STATIC deep
   clone taken at init (script.js), so it renders whatever the tiles look like
   with nothing set. The hand-off happens at pin progress 0, where the real
   tiles are also white — that is what keeps clone and section identical.
   ============================================================ */
.feature {
  --b: 0;
  background: color-mix(in srgb, #fff, var(--royal) calc(var(--b) * 100%));
  border-color: color-mix(in srgb, var(--line), #2E63FF calc(var(--b) * 100%));
  box-shadow: var(--card-shadow), 0 22px 50px -30px rgba(20,71,230, calc(var(--b) * .85));
}
/* The text rides a STEEPER ramp than the tile ground (saturates by --b ≈ .55): on the
   shared linear ramp the background already read blue at half-fill while the h3 was
   still mid-navy — a beat of dark-on-blue on every tile as it lit. min() clamps at 1,
   and 0 still lands at 0, so the white clone hand-off at --b:0 is untouched. */
.feature h3 { color: color-mix(in srgb, var(--ink), #fff calc(min(1, var(--b) * 1.8) * 100%)); }
.feature p { color: color-mix(in srgb, var(--slate-soft), rgba(255,255,255,.9) calc(min(1, var(--b) * 1.8) * 100%)); }
.feature__icon { color: color-mix(in srgb, var(--royal), #fff calc(min(1, var(--b) * 1.8) * 100%)); }

/* The line's job on #contact is to introduce the form — it stems out, the blind
   unrolls beneath it, and then it has nothing left to say. Retire it once the
   form is fully in, rather than leaving a bright rule pinned above the tile.
   (The landing gate keeps its line: there it is the stem the panel hangs from.) */
.reserve__reveal.is-open .gate__line {
  animation: reserveLineOut 2.1s var(--ease) forwards;
}
@keyframes reserveLineOut {
  0%   { transform: scaleX(0); opacity: 0; }
  28%  { transform: scaleX(1); opacity: 1; }
  62%  { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .reserve__reveal.is-open .gate__line { animation: none; opacity: 0; }
}

/* The shared entrance drives depth with translateZ(-170px) against a 1200px
   perspective — a 15% scale swing. On most sections that reads as parallax, but
   #visibility's payload is a single large dashboard screenshot, so the whole
   thing visibly starts small and grows into place. Measured 630x349 on approach
   against 745x392 settled. Flatten the depth here (and widen the perspective) so
   the card only moves, it doesn't resize: ~3% instead of ~15%. */
[data-enter="right"]:has(.fcar) > * {
  transform: perspective(1800px)
             translate3d(calc(var(--i) * 44px), 0, calc(var(--i) * -56px))
             rotateY(calc(var(--i) * 3deg));
}
@media (max-width: 1439px) {
  /* below 1280 every horizontal swing goes vertical (see the shared entrance rules) —
     this section's special-case must follow or its 44px translate + rotateY overhangs
     narrow windows and sits off-centre when --e latches mid-entrance */
  [data-enter="right"]:has(.fcar) > * {
    transform: perspective(1000px) translate3d(0, calc(var(--i) * 40px), calc(var(--i) * -70px));
  }
}

/* #capabilities is opacity:0 for the whole teardown (kiosk3d drives it) and the
   WebGL canvas is transparent below the floor line, so whatever sits behind the
   section is what you see under the kiosk. Before it was pinned that was the
   body, #071628. Pinning gave it a spacer, and the blanket
   `body.has-kiosk-scroll .pin-spacer { background: #0d0e11 }` painted that
   spacer near-black — which read as a big dark square cutting across the zoom.
   Let the body show through again; the section covers this once it is opaque. */


/* ============================================================
   Pillars sequence (#platform-in-one)
   The section pins and the four cards slide in left to right. --s is written per
   card by script.js off the pin progress; it defaults to 1 so the section reads
   normally with no JS, no GSAP, or under reduced motion.
   ============================================================ */
.pillar {
  --s: 1;
  opacity: calc(.04 + .96 * var(--s));
  transform: translateX(calc((1 - var(--s)) * -72px));
}
/* hover has to carry the slide offset, or hovering a mid-flight card snaps it home */
.pillar:hover { transform: translateX(calc((1 - var(--s)) * -72px)) translateY(-5px); }

/* White cards on --paper (#FAFBFC) were near-invisible. Drop the ground to a
   cooler, deeper tint and let the cards stay white — that reads as separation
   without restyling the card itself. Scoped by :has(.pillars) because
   .section--investors is reused unpinned on the investors page. */
.section--investors:has(.pillars) {
  background: linear-gradient(180deg, #E7EDF6 0%, #D8E3F1 100%);
  min-height: 100svh; min-height: 100lvh;
  display: flex; flex-direction: column; justify-content: center;
}
/* clears the 74px fixed nav while pinned (h2 sat at 71px) — same as .replace__intro */
@media (max-width: 920px) {
  .section--investors:has(.pillars) { padding-top: clamp(102px, 13.5vh, 126px); }
}
.section--investors:has(.pillars) .pillar {
  background: #fff;
  border-color: rgba(11,31,61,.09);
  box-shadow: 0 2px 5px rgba(11,31,61,.07), 0 28px 54px -30px rgba(11,31,61,.45);
}
.section--investors:has(.pillars) .pillar:hover {
  box-shadow: 0 3px 8px rgba(11,31,61,.09), 0 38px 68px -32px rgba(11,31,61,.55);
}
/* the seam into #platform now leaves this deeper ground, not --paper */
.section--investors:has(.pillars) + .section.dark { --seam-rgb: 216 227 241; }

/* 2-up on phones: four stacked cards make the pinned section far taller than the
   viewport, so most of the sequence would play off screen */
@media (max-width: 620px) {
  .pillars { grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 32px; }
  .pillar { padding: 20px 15px; }
  .pillar h3 { font-size: 1rem; margin: 10px 0 7px; }
  .pillar p { font-size: .8rem; line-height: 1.45; }
  .pillar__no { font-size: .7rem; }
}
@media (prefers-reduced-motion: reduce) {
  .pillar { opacity: 1; transform: none; }
}

/* ============================================================
   Beam sequence (#platform)
   The section pins and one wire lights per scroll, staying lit, with a glow
   blooming around the tile it feeds. --lit is written per node by script.js;
   it defaults to 0 here but the wires' dim rail is always drawn, so with no JS
   or under reduced motion the diagram still reads — just unlit.
   ============================================================ */
.beam__node { --lit: 0; }
.section--ecosystem:has(.beam) .beam__node {
  border-color: color-mix(in srgb, rgba(140,180,255,.2), #8FC0FF calc(var(--lit) * 100%));
  box-shadow:
    0 18px 42px -28px rgba(0,0,0,.85),
    0 0 0 1px rgba(150,190,255, calc(var(--lit) * .26)),
    0 0 26px -4px rgba(76,141,255, calc(var(--lit) * .70)),
    0 0 62px 2px rgba(76,141,255, calc(var(--lit) * .32));
}
.beam__node [data-icon] { transition: color .5s var(--ease); }
.section--ecosystem:has(.beam) .beam__node [data-icon] {
  color: color-mix(in srgb, var(--sky), #DCEAFF calc(var(--lit) * 100%));
}
/* the pinned section has to fill the viewport, or it sticks at the top and shows
   the spacer below it. #platform is --ink-deep and so is the body, so the spacer
   needs no colour of its own (it is not in the dark-spacer allow-list). */
.section--ecosystem:has(.beam) {
  min-height: 100svh; min-height: 100lvh;
  display: flex; flex-direction: column; justify-content: center;
}

/* A pinned section that carries its content up leaves the space it vacated
   behind as dead section — 825px of empty white under the last capability tile
   at 390.

   Do NOT fix this by capping the section's height. #capabilities is
   `justify-content: center` and that centring IS the finale alignment: the
   comment on #capabilities[data-capfinale] spells it out — the content is
   centred at the true viewport centre so it matches #capScreen's centred card
   at the swap. Capping the height re-centres shorter content and threw dy from
   -1px to +392px. dx and dw stayed 0, so it looked fine until measured.

   Pull the FOLLOWING content up with a negative margin on the spacer instead.
   That leaves the section's own box, its centring and its children untouched. */
/* !important because GSAP writes the spacer's margins inline, and an inline
   style beats a stylesheet rule. This is the one place it is warranted. */
.pin-spacer:has(> .is-pinshift) { margin-bottom: calc(-1 * var(--pinshift, 0px)) !important; }

/* ============================================================
   MOBILE HERO + TEARDOWN REEL — below 921px the hero is a plain
   dark panel (headline / CTAs / chips) and the next section is the
   looping teardown video (assets/kiosk-loop.mp4, info tiles baked
   in, recorded via kiosk3d.js ?capture). kiosk3d.js skips the hero
   3D init on mobile so body.has-kiosk-scroll never applies there.
   Desktop hides the reel and runs the live WebGL scroll teardown.
   ============================================================ */
.hero__chips, .section--reel { display: none; }
@media (max-width: 920px) {
  /* flex-start + the grid's fixed top padding compose the opening frame from the
     top down: at rest the card centred, which left ~26% of the first viewport as
     flat void between the nav and the glass panel. */
  .hero--teardown { position: relative; min-height: 100svh; padding: 0; display: flex; justify-content: flex-start; overflow: hidden;
    background:
      radial-gradient(120% 90% at 80% 10%, rgba(30, 64, 140, .45), transparent 60%),
      radial-gradient(120% 100% at 15% 90%, rgba(20, 71, 230, .18), transparent 55%),
      var(--ink-deep); }
  /* the WebGL stage / dots never exist on mobile now — reclaim their layout box */
  .hero--teardown .hero__visual { display: none; }
  /* 128px = room for the credibility chips, which sit ABOVE the card (below) */
  .hero--teardown .hero__grid { position: relative; z-index: 2; display: flex; width: 100%; min-height: 0; height: auto;
    padding: calc(var(--nav-h) + 128px) 16px 24px; }
  .hero--teardown .hero__copy { min-height: 0; width: 100%; display: flex; }
  .hero--teardown .hero__step[data-step="0"] { position: static; margin: auto 0; width: 100%;
    background: rgba(9, 16, 32, .55);
    -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px);
    border: 1px solid rgba(120, 160, 255, .16); border-radius: 18px;
    padding: 20px 18px 18px; }
  .hero--teardown .hero__title { font-size: clamp(1.7rem, 6.6vw, 2.2rem); line-height: 1.08; margin-bottom: 12px; }
  .hero--teardown .hero__sub { font-size: .95rem; margin-bottom: 18px; max-width: none; }
  /* the scroll cue STAYS on mobile (it was display:none): the glass card tops out
     ~270px above the fold and the band below it read as dead panel without one.
     Base rule positions it (absolute, centred, bottom 26px); under the grid's z:2
     and inert so a tall card on a short phone can never be blocked by it. */
  .hero--teardown .hero__scroll { z-index: 1; pointer-events: none; }
  /* credibility chips (Steven: proof points near the top on mobile). They now
     genuinely sit near the top: absolute against .hero__copy (position:relative
     at its base rule, stable in every mobile state incl. reduced motion), lifted
     into the zone between the nav and the glass card — the void that used to
     open the page as mostly empty background. */
  .hero__chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0;
    position: absolute; bottom: calc(100% + 16px); left: 2px; right: 2px; }
  .hero__chips span { font-family: var(--font-mono); font-size: .62rem; letter-spacing: .08em; text-transform: uppercase;
    color: rgba(255,255,255,.74); padding: 6px 10px; border-radius: 999px;
    border: 1px solid rgba(140,180,255,.25); background: rgba(20,40,80,.35); }
  /* the reel: a dark stage sized to the video's OWN 720x1280 frame (aspect-ratio, not
     min-height:100svh) so contain leaves no letterbox; normal scroll continues below.
     The full-height stage boxed ~75px of #30353C above and below the loop, and during
     the dark kiosk-closeup segments (~t 14-19s) the frame edge goes near-black — the
     sampled grey read as an unstyled strip with a hard horizontal edge. */
  .section--reel { display: block; position: relative; padding: 0; aspect-ratio: 720 / 1280; background: #30353C; }
  /* contain, not cover: cover's side-crop would clip the baked-in info tiles on narrow
     phones. The stage matches the frame's aspect, so contain fills it edge to edge.
     Kept transparent until 'playing' fires (script.js adds .is-playing): a policy-paused
     video (iOS Low Power Mode) would otherwise show the system play glyph — this way the
     poster background shows instead, and the first scroll-touch starts the loop. */
  .section--reel { background: url('assets/kiosk-loop-poster.jpg?v=11') center / contain no-repeat, #30353C; }
  .reel__video { display: block; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
    opacity: 0; transition: opacity .45s var(--ease); }
  .reel__video.is-playing { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  /* reduced motion: the reel shows the poster still (script.js also strips autoplay) */
  .reel__video { display: none; }
  @media (max-width: 920px) {
    .section--reel { background: url('assets/kiosk-loop-poster.jpg?v=11') center / cover no-repeat, #26282d; }
  }
}

/* ============================================================
   STEVEN BATCH — hardware specs, outcomes, operator voices,
   sticky mobile CTA (2026-07-30)
   ============================================================ */
/* every section paints its own ground (the body goes dark past the hero) */
/* specs: dark blueprint panel — faint drafting grid over ink, corner-bracketed glass cards */
.section--specs { position: relative; background:
  linear-gradient(rgba(76,141,255,.045) 1px, transparent 1px),
  linear-gradient(90deg, rgba(76,141,255,.045) 1px, transparent 1px),
  radial-gradient(120% 90% at 85% 0%, rgba(20,71,230,.16), transparent 55%),
  var(--ink);
  background-size: 44px 44px, 44px 44px, 100% 100%, 100% 100%; }
.section--specs .section-head { color: #fff; }

/* hardware specs grid */
.specs { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; gap: 18px; margin-top: 34px; }
.spec { --s: 1; position: relative; display: flex; flex-direction: column; padding: 26px 24px 22px; border-radius: 14px;
  background: linear-gradient(158deg, rgba(120,160,255,.12), rgba(10,18,36,.55));
  -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3);
  border: 1px solid rgba(140,180,255,.2);
  box-shadow: 0 18px 40px -26px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.14);
  /* scroll-scrubbed entrance (script.js drives --s, staggered; default 1 = settled for no-JS) */
  opacity: calc(.12 + .88 * var(--s));
  transform: translateY(calc((1 - var(--s)) * 34px)); }
/* drafting corner brackets */
.spec::before, .spec::after { content: ''; position: absolute; width: 16px; height: 16px; pointer-events: none; }
.spec::before { top: 8px; left: 8px; border-top: 2px solid rgba(143,196,255,.55); border-left: 2px solid rgba(143,196,255,.55); border-top-left-radius: 4px; }
.spec::after { bottom: 8px; right: 8px; border-bottom: 2px solid rgba(143,196,255,.35); border-right: 2px solid rgba(143,196,255,.35); border-bottom-right-radius: 4px; }
.spec h3 { font-family: var(--font-mono); font-size: .72rem; font-weight: 400; letter-spacing: .1em; text-transform: uppercase; color: var(--sky-soft); margin-bottom: 10px; }
.spec p { font-family: var(--font-display); font-size: 1.22rem; font-weight: 600; color: #fff; line-height: 1.25; letter-spacing: -.01em; }
.spec__sub { display: block; margin-top: 8px; font-size: .86rem; line-height: 1.5; color: rgba(255,255,255,.62); }
/* measuring scan: a light sweep fires once as each card finishes its entrance (.is-on) */
.spec__scanline { position: absolute; inset: 0; border-radius: inherit; overflow: hidden; pointer-events: none; }
.spec__scanline::before { content: ''; position: absolute; top: 0; bottom: 0; left: -40%; width: 34%;
  background: linear-gradient(100deg, transparent, rgba(143,196,255,.16), transparent);
  transform: translateX(-120%); }
.spec.is-on .spec__scanline::before { animation: specScan .9s var(--ease) forwards; }
@keyframes specScan { to { transform: translateX(460%); } }
@media (prefers-reduced-motion: reduce) { .spec.is-on .spec__scanline::before { animation: none; } }

/* illustrative outcomes strip (inside the proof band) */
/* outcome tiles: navy against the light section (contrast pop), revealed as ONE group
   by the proof pin's closing beat (--g from script.js; default 1 = settled for no-JS) */
.outcomes { --g: 1; display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 40px;
  opacity: var(--g); transform: translateY(calc((1 - var(--g)) * 26px)); }
.outcome { padding: 22px 24px; border-radius: 16px;
  background: radial-gradient(120% 130% at 85% 0%, rgba(20,71,230,.32), transparent 55%), var(--ink);
  border: 1px solid rgba(76,141,255,.28);
  box-shadow: 0 22px 46px -26px rgba(7,22,40,.55), inset 0 1px 0 rgba(255,255,255,.08); }
.outcome__num { display: block; font-family: var(--font-display); font-size: 1.7rem; line-height: 1.1; margin-bottom: 6px; }
/* the page-wide grad-text starts at royal, which only measures ~3:1 on navy — rebalance
   both stops for the dark tile (same fix as the investors pullquote) */
.outcome__num.grad-text { background: linear-gradient(118deg, var(--sky) 0%, #8fc4ff 100%);
  -webkit-background-clip: text; background-clip: text; }
.outcome p { font-size: .92rem; line-height: 1.5; color: rgba(255,255,255,.78); }
.outcomes__note { --g: 1; margin-top: 14px; font-size: .8rem; color: var(--slate-soft); font-style: italic; opacity: var(--g); }

/* operator-voice placeholders — visibly pending, never fabricated. Dark section,
   glass cards on a scroll-snap slider (auto-advanced by script.js, IO-gated). */
.section--voices { background: var(--ink-deep); }
.section--voices .section-head { color: #fff; }
.vslider { position: relative; margin-top: 34px; overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%); }
/* ticker: script.js clones the card set and drives translateX at constant speed;
   overflow stays visible inside the strip so card shadows aren't clipped */
.vslider__strip { display: flex; gap: 18px; padding: 8px 0 22px; width: max-content; will-change: transform; }
.voice { position: relative; flex: 0 0 min(340px, 82vw);
  padding: 26px 22px 22px; border-radius: 18px;
  background: linear-gradient(158deg, rgba(120,160,255,.14), rgba(10,18,36,.5));
  -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3);
  border: 1px dashed rgba(140,180,255,.34);
  box-shadow: 0 18px 40px -26px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.16); }
.voice__mark { position: absolute; top: 6px; left: 16px; font-family: var(--font-display); font-size: 2.6rem; line-height: 1; color: rgba(143,196,255,.4); }
.voice__role { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sky-soft); margin: 14px 0 8px; }
.voice__pending { margin: 0; font-size: .92rem; line-height: 1.55; color: rgba(255,255,255,.72); font-style: italic; }

/* sticky mobile CTA — mobile only, slides up once the hero is gone, ducks over the form */
.mcta { display: none; }
@media (max-width: 620px) {
  .mcta { display: block; position: fixed; left: 16px; right: 16px;
    bottom: calc(14px + env(safe-area-inset-bottom)); z-index: 80;
    padding: 15px 22px; border-radius: 999px; text-align: center;
    font-family: var(--font-mono); font-size: .82rem; letter-spacing: .1em; text-transform: uppercase;
    color: #fff; text-decoration: none; background: var(--grad);
    box-shadow: 0 14px 34px -12px rgba(20,71,230,.55), 0 4px 14px rgba(7,22,40,.35);
    transform: translateY(calc(100% + 24px + env(safe-area-inset-bottom)));
    transition: transform .45s var(--ease); pointer-events: none; }
  .mcta.is-on { transform: none; pointer-events: auto; }
}

@media (max-width: 920px) {
  .specs, .outcomes { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 620px) {
  .specs, .outcomes { grid-template-columns: 1fr; gap: 14px; }
  .spec p { font-size: 1.08rem; }
}

/* phone: compact capability cards — the desktop-sized padding/icon made each card a
   third of the screen and the six-card run read as endless repeated blocks */
@media (max-width: 620px) {
  .feature { padding: 18px 18px 16px; }
  .feature__icon { width: 30px; height: 30px; }
  .feature h3 { font-size: 1.02rem; margin: 12px 0 5px; }
  .feature p { font-size: .88rem; line-height: 1.5; }
  /* consolidation timeline: pull the first card up toward the intro — the old top
     clamp left a dead band between the heading and the first vendor card */
  .orbit { top: clamp(206px, 26vh, 250px); }
}

/* accessibility: lift the dimmest copy on the dark ring tiles (Steven #9) */
.ring__tile p { color: rgba(255,255,255,.8); }

/* ============================================================
   BOOT LOADER — ~2.5s branded hold; timing in the inline <head>
   script. Reduced motion keeps the hold, drops the movement.
   ============================================================ */
.loader { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 90% at 70% 18%, rgba(20,71,230,.18), transparent 55%), var(--ink-deep);
  transition: opacity .55s var(--ease); }
.loader.is-done { opacity: 0; pointer-events: none; }
.loader__inner { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.loader__markwrap { position: relative; overflow: hidden; border-radius: 12px; padding: 6px 10px; }
.loader__mark { display: block; width: 92px; height: auto; animation: loadMark 1s var(--ease) both; }
.loader__sheen { position: absolute; top: -20%; bottom: -20%; left: -55%; width: 40%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.32), transparent);
  transform: skewX(-18deg); animation: loadSheen 1.5s ease-in-out .45s infinite; }
.loader__word { font-family: var(--font-display); font-weight: 700; font-size: 1.28rem; color: #fff;
  letter-spacing: .42em; padding-left: .42em; /* recentres tracked text */
  animation: loadWord 1.1s var(--ease) .15s both; }
.loader__bar { width: 148px; height: 2px; border-radius: 2px; background: rgba(255,255,255,.14); overflow: hidden; }
.loader__bar i { display: block; height: 100%; background: var(--grad); transform-origin: left;
  transform: scaleX(0); animation: loadBar 2.3s cubic-bezier(.3,.1,.3,1) .1s forwards; }
@keyframes loadMark { from { opacity: 0; transform: scale(.86); filter: blur(6px); } to { opacity: 1; transform: none; filter: none; } }
@keyframes loadSheen { from { transform: translateX(0) skewX(-18deg); } to { transform: translateX(430%) skewX(-18deg); } }
@keyframes loadWord { from { opacity: 0; letter-spacing: .8em; } to { opacity: 1; letter-spacing: .42em; } }
@keyframes loadBar { to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
  .loader__mark, .loader__word { animation: none; }
  .loader__bar i { animation: none; transform: scaleX(1); }
  .loader__sheen { display: none; }
}


/* MOBILE: the scroll-linked [data-enter] section entrances are desktop-only — the
   per-frame perspective transforms during momentum scrolling judder against the
   transition-based reveals. Phones keep the IO reveals alone (compositor-smooth);
   script.js skips the --e writes below 921px to match. */
@media (max-width: 920px) {
  /* NOTE: never include bare [data-enter] here — the sections themselves are
     ScrollTrigger pin targets, and transform:none !important wipes the inline
     post-pin translate, snapping content away and leaving dead spacer scroll. */
  [data-enter] > *, [data-enter] > * .section-head,
  [data-enter] > * :is(.feature-grid, .pillars, .proof, .beam, .fcar, .triad,
                       .whynow, .beats, .adv-grid, .reserve__form) {
    transform: none !important; opacity: 1 !important; filter: none !important;
  }
  /* ...so the promotion hint is promising a phone GPU a transform that can never
     run. Every near section was holding a compositor layer per direct child for an
     animation this same block disables. */
  [data-enter].is-near > * { will-change: auto; }
}

/* MOBILE: THE ECOSYSTEM CURRENT IS DRAWN WITH A TRANSFORM, NOT A HEIGHT
   `.flow::after` animates `height` 0 → 100% over 1.4s, and it carries a 16px glow.
   Animating height means laying out and repainting the bar on every one of those
   ~84 frames, and it fires exactly as the section scrolls into view — main-thread
   paint work landing precisely when the compositor is busiest. Scaling a
   full-height bar from the top draws the identical line on the compositor alone.
   `--flowS` is set alongside `--flow` in script.js; desktop keeps the height ramp. */
@media (max-width: 920px) {
  .flow::after {
    height: calc(100% - 20px);
    transform: scaleY(var(--flowS, 0)); transform-origin: 50% 0;
    transition: transform 1.4s var(--ease);
  }
}

/* MOBILE: PROMOTE A REVEAL ONLY WHEN IT IS ABOUT TO RUN
   `.reveal:not(.is-visible)` is true for EVERY reveal below the fold, which on first
   load is nearly all 28 of them — so a phone was holding 28 compositor layers at
   DPR 3 before the first scroll, for transitions most of which were minutes away.
   That memory pressure is not visible as a dropped frame in any JS measurement; it
   shows up as the compositor evicting and re-rasterising tiles while you scroll.

   script.js arms a reveal (`.is-armed`) once it is within ~30% of a viewport, and
   disarms it when it leaves — so the hint still lands before the transition starts,
   which is the whole point of it, but only a handful are ever live at once. Desktop
   is untouched: it has the headroom, and the class is simply unused there. */
@media (max-width: 920px) {
  .reveal:not(.is-visible)           { will-change: auto; }
  .reveal.is-armed:not(.is-visible)  { will-change: opacity, transform; }
}


/* ============================================================
   MOBILE: NO backdrop-filter ON THE SCROLL PATH
   ------------------------------------------------------------
   backdrop-filter re-samples and re-blurs whatever sits BEHIND the element. While the
   page is scrolling, what sits behind it changes every single frame, so iOS has to
   redo the blur every frame — for six to twelve cards at once in these sections, plus
   the fixed nav which is on screen the entire time. That is the judder and the
   "gets held up" feel on a real iPhone, and it is invisible in a desktop browser,
   which has orders of magnitude more GPU headroom.

   It is ALSO why this got worse rather than better: while these sections were pinned,
   the content behind the glass was static and the blur could be cached. Now that they
   scroll, every frame is a fresh backdrop.

   These sections all sit on near-black navy, so an opaque colour close to the blurred
   result is visually almost indistinguishable — and costs nothing to composite.
   Desktop keeps the real glass.
   ============================================================ */
@media (max-width: 920px) {
  .nav, .nav.is-scrolled,
  .orbit__dot, .orbit__card, .ring__tile, .beam__node, .beam__hub,
  .spec, .voice, .fcar__rail, .klabel__txt {
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
  }
  .nav            { background: rgba(11, 20, 37, .90); }
  .nav.is-scrolled{ background: rgba(9, 17, 32, .96); }
  .orbit__dot     { background: #1a1d24; }
  .orbit__card    { background: #191c23; }
  .ring__tile     { background: linear-gradient(155deg, #1b2740, #0d1526); }
  .beam__node     { background: linear-gradient(155deg, #1a2540, #0d1526); }
  .beam__hub      { background: linear-gradient(155deg, #22315a, #101a30); }
  .spec           { background: linear-gradient(158deg, #17203a, #0d1526); }
  .voice          { background: linear-gradient(158deg, #18213c, #0d1526); }
  .fcar__rail     { background: linear-gradient(158deg, #1c2c55, #131f3c); }
  .klabel__txt    { background: #12141a; }

  /* ---- second pass (2026-08-15): the four that were missed ----
     Same reasoning, four elements the first sweep did not cover. All four sit on a
     smooth dark gradient with no high-frequency detail behind them, so blurring that
     backdrop produces almost exactly the backdrop itself — the opaque replacements
     below were sampled off the ORIGINAL composited pixels in WebKit at 390px, so this
     is a cost removal, not a restyle.

       .hero__step  the opening glass card. On screen from the first frame and the
                    first thing any scroll touches, so its blur was being redone
                    before the client had scrolled anything at all.
       .footer      a full-bleed blurred layer at the end of every scroll, behind a
                    background that is already 93% opaque — the blur was buying
                    nothing and costing a full-width readback.
       .gform       the lead form. Blur plus a 60px shadow on the one element the
                    page exists to get people into.
       .nav__links  the open menu sheet: blur(30px) saturate(1.7), the most expensive
                    declaration in the file, re-blurring the live page underneath it
                    while the sheet slides. */
  .hero--teardown .hero__step[data-step="0"],
  .footer, .gform, .nav__links {
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
  }
  /* sampled composites: card top rgb(19,24,35) → bottom rgb(15,19,30) */
  .hero--teardown .hero__step[data-step="0"] {
    background: linear-gradient(180deg, rgb(19,24,35), rgb(15,19,30));
  }
  .footer     { background: #071628; }
  /* .gform keeps its authored translucent gradient — measured against the blurred
     original, an opaque approximation of a 165deg gradient landed ~9/255 dark at the
     top. Blurring a smooth backdrop returns almost the same backdrop, so simply not
     blurring it composites to within ~2/255 while dropping the readback entirely.
     A 60px shadow on a card this size is the other expensive half; it reads the same
     at 22px on dark ground. */
  .gform      { box-shadow: 0 12px 22px -16px rgba(0,0,0,.95); }
  /* the "it lights up once unrolled" beat is kept — three overlapping 26/70/60px
     blurs are not. Same look, a fraction of the paint area. Selector matches the
     base rule's specificity (0,3,0) on purpose or the open state would win. */
  .reserve__reveal.is-open .gform {
    box-shadow:
      0 0 0 1px rgba(150,190,255,.14),
      0 0 18px -6px rgba(76,141,255,.45),
      0 0 30px 2px rgba(76,141,255,.20),
      0 12px 22px -16px rgba(0,0,0,.95);
  }
  .nav__links { background: #08101f; }
}

/* MOBILE: STEPPED ANIMATIONS, INTERPOLATED BY CSS
   script.js quantises these scrubs into discrete steps on phones (see bin()/quant()) so
   the main thread stops rewriting expensive properties on every scroll frame. These
   transitions are the other half of that: they carry the motion BETWEEN steps, off the
   scroll path entirely. Without them the steps would visibly snap; with them the section
   reads as a smooth sequence while scrolling itself stays untouched.
   Durations are deliberately a little longer than a step takes to cross, so one beat is
   still finishing as the next begins and the whole thing flows. */
@media (max-width: 920px) {
  .feature {
    transition: background .5s var(--ease), border-color .5s var(--ease),
                box-shadow .5s var(--ease);
  }
  .feature h3 { transition: color .5s var(--ease); }
  .beam__node { transition: border-color .55s var(--ease), box-shadow .55s var(--ease); }
  .beam__node [data-icon] { transition: color .55s var(--ease); }
  .pillar { transition: opacity .5s var(--ease), transform .5s var(--ease); }
  .proof__item { transition: opacity .5s var(--ease), transform .55s var(--ease); }
  .proof__item .proof__check { transition: background .5s var(--ease), box-shadow .5s var(--ease), transform .5s var(--ease); }
  .outcomes, .outcomes__note { transition: opacity .55s var(--ease), transform .55s var(--ease); }
  /* the ring no longer steps on phones — it is a swipeable strip, see the block below */
  /* Big soft shadows are the other expensive half of every repaint. These cards sit
     on dark panels where a 40-60px blur reads almost identically at 20px. */
  .feature     { --card-shadow: 0 12px 26px -18px rgba(7,22,40,.5); }
  .orbit__card { box-shadow: 0 10px 20px -14px rgba(0,0,0,.75); }
  .orbit__dot  { box-shadow: 0 6px 14px -8px rgba(0,0,0,.7); }
  .ring__tile  { box-shadow: 0 12px 24px -16px rgba(0,0,0,.85), inset 0 1px 0 rgba(255,255,255,.14); }
  .beam__node  { box-shadow: 0 10px 20px -14px rgba(0,0,0,.85); }
  .spec        { box-shadow: 0 10px 20px -14px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.12); }
  .voice       { box-shadow: 0 10px 20px -14px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.14); }
}

/* ============================================================
   MOBILE: THE VENUE RING IS A SWIPEABLE STRIP
   ------------------------------------------------------------
   The 3D wheel is a desktop idiom. Bringing twelve tiles around takes a long
   scroll-linked run, and on a phone the section is only content-tall, so the client
   watched it scroll past mid-turn. The only ways to buy that time are a pin or
   position:sticky — and the real-device measurement (scrollscope on a native iPhone 16
   Pro Max recording, 2026-07-31) proved both are the shake.

   So phones hand the pace to the user instead: one horizontal scroll-snap strip they
   swipe at their own speed. All twelve are reachable, nothing is held, and this costs
   ZERO scroll-linked work — script.js creates no ScrollTrigger for the ring below
   921px, so there is no --spin/--o/z-index write on any frame, and the strip scrolls on
   its own compositor layer independent of the page. Same shape as #replaces, the one
   section the device measurement found completely clean.

   Placed AFTER the base ring rules on purpose: the mobile overrides are the same
   specificity, so source order is what makes them win (the same trap the mobile finale
   overrides hit).

   :not(.is-static) so the reduced-motion / no-GSAP fallback keeps its plain grid.
   ============================================================ */
/* desktop turns the wheel on its own — nothing to swipe, nothing to hint at. Declared
   BEFORE the mobile block: same specificity, so whichever comes last would win. */
.ring__hint { display: none; }
@media (max-width: 920px) {
  .ring:not(.is-static) {
    /* one card, plus enough of its neighbours to read as continuable */
    --sw: min(78vw, 296px);
    height: auto; perspective: none; margin-top: 20px;
    /* overflow-y must stay visible so the card shadows are not clipped; `auto` on one
       axis pairs with `visible` on the other, unlike `clip`. */
    overflow-x: auto; overflow-y: visible;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    /* a swipe past the last card must not turn into a browser back-navigation */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* The edge mask is a paint-time effect on a layer that moves every swipe frame.
       The peeking neighbour is a better affordance and costs nothing to composite. */
    -webkit-mask-image: none; mask-image: none;
  }
  .ring:not(.is-static)::-webkit-scrollbar { display: none; }
  /* script.js makes the strip focusable so it can be scrolled by keyboard; give that
     focus somewhere visible to land, without a ring on every touch. */
  .ring:not(.is-static):focus-visible {
    outline: 2px solid var(--sky); outline-offset: 4px; border-radius: 14px;
  }
  .ring:not(.is-static) .ring__stage {
    position: static; inset: auto; transform: none; transform-style: flat;
    will-change: auto; transition: none;
    display: flex; gap: 12px;
    /* Percentage padding resolves against .ring's content width (NOT its scrollWidth),
       so this centres the first and last card exactly, leaving the same peek at both
       ends of the strip as in the middle. */
    padding: 4px calc((100% - var(--sw)) / 2) 14px;
  }
  .ring:not(.is-static) .ring__tile {
    position: static; left: auto; top: auto; margin: 0;
    flex: 0 0 var(--sw); width: var(--sw);
    height: auto; min-height: 158px;
    transform: none; opacity: 1;
    -webkit-backface-visibility: visible; backface-visibility: visible;
    transition: none;
    scroll-snap-align: center;
  }
  .ring__hint {
    display: block; margin: 14px auto 0; text-align: center;
    font-family: var(--font-mono); font-size: .68rem; letter-spacing: .14em;
    text-transform: uppercase; color: rgba(255,255,255,.42);
  }
  .ring__hint span { display: inline-block; margin-left: 4px; }
  /* reduced motion / no GSAP falls back to a stacked grid — there is nothing to swipe */
  .ring.is-static ~ .ring__hint { display: none; }
}

/* ============================================================
   MOBILE: NOTHING IS STICKY
   ------------------------------------------------------------
   Measured on a real iPhone 16 Pro Max (scrollscope, native screen recording): the
   only sections that still shook were the ones still using position:sticky, and the
   one converted to normal flow did not appear in the hotspots at all. On iOS the
   compositor places a sticky element while the main thread paints its contents, and
   every step of an animation inside it makes the two disagree for a frame — a small
   (~5px) but constant vibration. The sticky footer reveal produced the stalls.

   So phones get no sticky anywhere: sections simply scroll, and their animations run
   as the section passes through the viewport. Desktop keeps sticky and pinning.
   ============================================================ */
@media (max-width: 920px) {
  #platform, #built-for, #capabilities {
    display: block; padding-top: clamp(100px, 13.5vh, 124px); padding-bottom: 56px;
    overflow-x: clip; overflow-y: visible;
  }
  /* the sticky footer parallax costs a clip-path plus a sticky layer on every frame */
  .footer-reveal        { height: auto; clip-path: none; }
  .footer-reveal__inner { height: auto; top: 0; }
  .footer-reveal__stick { position: static; height: auto; }
  .footer-reveal .footer{ height: auto; }
}

/* ------------------------------------------------------------
   MOBILE #replaces — the consolidation timeline.
   This is the one section the real-device measurement found CLEAN (it never appeared
   in a single jank hotspot), because it is plain document flow: the page scrolls it on
   the compositor and the reveals are an IntersectionObserver, so there is no
   scroll-linked work at all. Keep it that way.

   These rules are load-bearing: .orbit is `position: absolute; top: 40vh; bottom: 0`
   by default, sized against the SECTION. With the section's min-height removed that
   computes to zero and the whole timeline vanishes, leaving only the heading. It must
   be put back into normal flow so the height script.js gives it actually applies.
   ------------------------------------------------------------ */
@media (max-width: 920px) {
  #replaces {
    display: block; overflow-x: clip; overflow-y: visible;
    padding-top: clamp(100px, 13.5vh, 124px); padding-bottom: 64px;
  }
  #replaces > .hold { position: static; }
  /* normal flow — script.js sets the height to the full strip in layoutOrbit */
  #replaces .orbit { position: relative; inset: auto; overflow: visible; }
  #replaces .replace__intro { position: relative; z-index: 5; }
  /* the caption had its own scrubbed beat once; the node reveal now carries the card */
  #replaces .orbit__card { --cap: 1; }
  #replaces .orbit__node {
    opacity: 0; transform: translateX(var(--slide, 0));
    transition: opacity .55s var(--ease), transform .6s var(--ease);
  }
  #replaces .orbit__node--l { --slide: -60px; }
  #replaces .orbit__node--r { --slide:  60px; }
  #replaces .orbit__node.is-in { opacity: 1; transform: none; }
  #replaces .orbit__hub {
    opacity: 0; transform: translate(-50%, -50%) scale(.94);
    transition: opacity .6s var(--ease), transform .6s var(--ease);
  }
  #replaces .orbit__hub.is-in { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* MOBILE: these seven sections are no longer pinned on phones (script.js scrubCfg), so
   a forced full-viewport min-height now only reserves empty space beneath content that
   does not fill it — the client's "empty space until the next section". Sized to their
   content instead; the section padding still gives them room to breathe, and the next
   section starts directly below. Desktop keeps 100lvh, which its pinning depends on. */
@media (max-width: 920px) {
  #capabilities, #replaces, #platform-in-one, #platform,
  #visibility, #built-for, #traction { min-height: 0; }
}

/* MOBILE: the outcome tiles are a ~400px block, and at --g:0 that is a ~400px hole in
   the middle of a pinned section — one of the "large blank space" reports. The group
   entrance the client asked for still reads (they lift and resolve together), but from
   a visible floor rather than from nothing. Desktop keeps the full 0 → 1 fade. */
@media (max-width: 920px) {
  .outcomes { opacity: calc(.34 + .66 * var(--g)); }
  .outcomes__note { opacity: calc(.4 + .6 * var(--g)); }
}

/* MOBILE #visibility pin: pinned from the top so the title never scrolls under the nav
   while stepping through the dashboard functions; the lede folds away once the pin
   engages so the card + caption fit beneath the heading. */
@media (max-width: 920px) {
  .section--visibility:has([data-fcar]) { padding-top: clamp(102px, 13.5vh, 126px); }
  .section--visibility .lede { max-height: 220px; overflow: hidden;
    transition: max-height .55s var(--ease), opacity .4s var(--ease), margin-bottom .55s var(--ease); }
  .section--visibility.is-vispinned .lede { max-height: 0; opacity: 0; margin-bottom: 0; }
}
