/* ============================================================
   THE FILM — homepage only.

   The whole page is one continuous shot. A single SVG stroke is
   drawn from the top of the document to the bottom, meandering
   between chapter anchors, looping once into the PCI ring, and
   terminating in the final call to action. Its drawn tip tracks
   the middle of the viewport, so the line is always arriving
   exactly where you are looking.

   Scenes are STICKY, never GSAP-pinned. Sticky keeps the
   document height honest, which is what lets the thread be
   measured in real page pixels — and it sidesteps the pin-spacer
   refresh-order trap entirely (no pins, so nothing downstream of
   a pin can be mis-positioned).

   Every rule here is scoped under a film-only ancestor, so this
   sheet is inert on the other 19 pages and can be loaded on all
   of them. That is what keeps the page-transition code from
   having to inject stylesheets mid-navigation.
   ============================================================ */

/* ---------------- Thread ----------------
   FIXED, not absolute. An absolutely-positioned layer sized to the
   document contributes that height to scrollable overflow — and
   because the sticky header condenses 86px → 68px on scroll, the
   height measured at scroll 0 was 18px too tall for every other
   scroll position. The result was 18px of bare canvas below the
   footer at the bottom of the page.

   Fixed to the viewport, the layer contributes nothing to document
   height and can never disagree with it. The path is still authored
   in document coordinates; .thread-shift carries a translate of
   -scrollY, which film.js writes each frame. It also means the
   browser composites one viewport-sized layer instead of a
   15,000px-tall one. */
.thread-layer {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}
.thread-layer svg { width: 100%; height: 100%; overflow: visible; }

.thread-path {
  fill: none;
  stroke: var(--primary);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The un-drawn remainder, so the route is faintly legible ahead of
   you — it reads as a plan, not a scribble. Heavier than it was on
   ink: indigo at 14% over off-white is nothing at all. */
.thread-ghost {
  fill: none;
  stroke: var(--primary);
  stroke-width: 1;
  stroke-linecap: round;
  opacity: .22;
}

/* The tip is the only "light source" left, and additive light does
   not work on a light ground — so it is a solid dot with a soft
   ring rather than a bloom. */
.thread-tip { fill: var(--primary); }
.thread-halo { fill: var(--primary); opacity: .16; filter: blur(7px); }

.thread-branch {
  fill: none;
  stroke: var(--primary);
  stroke-width: 1.25;
  stroke-linecap: round;
  opacity: .5;
}

/* On a phone the copy fills the column, so the route has nowhere to
   run except behind the text. It stays — it is the whole idea — but
   quieter, so it reads as a watermark rather than a strike-through. */
@media (max-width: 720px) {
  .thread-path { stroke-width: 1.25; opacity: .7; }
  .thread-ghost { opacity: .14; }
}

@media (prefers-reduced-motion: reduce) {
  .thread-layer { opacity: .45; }
}

/* Chapters paint either nothing or one of the washes in site.css.
   There are no dark chapters and no band elements between them: a
   washed section fades to transparent at both edges, so it blends
   into its neighbours by itself. */
.chapter { position: relative; }

/* Zero-size survey markers. The route is authored in the markup as
   an x fraction plus a vertical position inside its chapter, so the
   line can be re-plotted for any viewport without touching JS. */
.thread-anchor { position: absolute; left: 0; width: 0; height: 0; }
.branch-origin { display: block; height: 0; }

/* Content rides above the thread. Deliberately NOT `.chapter >
   .scene-stage` — that selector out-specifies `.scene-stage`'s own
   `position: sticky` and silently demotes every scene to relative,
   which looks like the scene simply never appears. The stage carries
   its own z-index instead. */
.chapter > .container,
.scene-stage > .container { position: relative; z-index: 5; }

/* ---------------- HUD ----------------
   Chapter readout, bottom left. The altimeter of the film.
   z-index 55 — below the transition overlay's 58, so a navigation
   to this page never shows the incoming HUD through the curtain. */
.hud {
  position: fixed;
  left: var(--gutter);
  bottom: clamp(18px, 3vh, 34px);
  z-index: 55;
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--muted);
  pointer-events: none;
  transition: opacity 320ms var(--motion-ease);
}
/* The footer is light too, so the readout would sit on top of its
   legal line. It has done its job by then. */
.hud[data-hidden="true"] { opacity: 0; }
.hud-num { color: var(--primary); }
.hud-name { opacity: .8; }
.hud-rail {
  position: relative;
  width: clamp(60px, 9vw, 130px);
  height: 1px;
  background: currentColor;
  opacity: .3;
}
.hud-rail span {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--primary);
  transform-origin: left;
}

@media (max-width: 760px) { .hud-name { display: none; } }
@media (max-width: 520px) { .hud { display: none; } }

/* ---------------- Chapter 00 — Origin ---------------- */
.film-hero {
  position: relative;
  /* The header is sticky, so it already consumes --header-h of normal
     flow above this. Subtracting it is what makes header + hero come to
     exactly one viewport rather than one viewport plus a header. */
  min-height: calc(100svh - var(--header-h));
  display: flex;
  align-items: center;
  /* Sized so the whole opening — eyebrow, four headline lines, lede,
     both buttons and the scroll cue — fits that budget at 900px tall
     without the cue falling below the fold. Fraunces is wider and
     chunkier than the face this was first tuned for, so re-measure
     if the headline size or the authored line breaks change. */
  padding-block: clamp(16px, 4vh, 52px) clamp(62px, 8vh, 96px);
  overflow: hidden;
}
/* Soft, low-alpha and blurred — a lift in the wash, not a light
   source. On a light ground an rgba disc darkens what is behind it,
   so these are kept faint enough to read as tone rather than smudge. */
.film-hero .glow {
  position: absolute;
  pointer-events: none;
  border-radius: 999px;
  filter: blur(70px);
}
.glow-a {
  width: min(680px, 86vw); aspect-ratio: 1;
  top: -18%; left: 50%; transform: translateX(-50%);
  background: rgba(61, 54, 187, .10);
  animation: float-slow 11s ease-in-out infinite;
}
.glow-b {
  width: min(420px, 64vw); aspect-ratio: 1;
  bottom: -14%; left: 50%; transform: translateX(-50%);
  background: rgba(61, 54, 187, .05);
  animation: float-slow 14s ease-in-out infinite reverse;
}

.film-hero h1 {
  font-family: var(--display);
  font-size: var(--step-hero);
  font-weight: 500;
  line-height: .96;
  letter-spacing: var(--track-display);
  color: var(--ink);
  max-width: 15ch;
  margin-bottom: var(--space-7);
}
/* No text-shadow. A glow behind type only works on a dark ground; on
   light it reads as mud. Italic and the brand indigo carry the accent
   word on their own. */
.film-hero h1 em {
  color: var(--primary);
  font-style: italic;
  font-weight: 500;
}
.film-hero .lede { max-width: 48ch; }

.hero-cue {
  position: absolute;
  left: 50%;
  bottom: clamp(18px, 3vh, 38px);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--muted-light);
}
.hero-cue-rail {
  width: 1px; height: 46px;
  background: linear-gradient(180deg, var(--primary), transparent);
  position: relative;
  overflow: hidden;
  opacity: .6;
}
.hero-cue-rail::after {
  content: "";
  position: absolute; left: 0; top: -46px;
  width: 1px; height: 46px;
  background: linear-gradient(180deg, transparent, var(--primary));
  animation: cue 2.6s var(--motion-ease) infinite;
}
@keyframes cue { to { transform: translateY(92px); } }

@media (max-width: 620px) { .hero-cue { display: none; } }

/* ---------------- Chapter 01 — The product ----------------
   Top padding clears the sticky header: the explainer is the first
   thing you read, and at any scroll position inside this chapter the
   header is overlaying it. */
.product-chapter { padding-block: calc(var(--bar-h) + clamp(28px, 3vw, 64px)) clamp(72px, 50px + 4vw, 140px); }
.product-chapter .container { max-width: 1320px; }
/* The mock carries a fixed 196px sidebar, so it needs the larger
   share or the main panel gets crushed. Below 1050 it stacks, and
   below 760 the mock drops its own sidebar (mock.css). */
.product-split {
  display: grid;
  grid-template-columns: minmax(0, .82fr) minmax(560px, 1.18fr);
  gap: clamp(32px, 18px + 2.6vw, 72px);
  align-items: center;
}
@media (max-width: 1050px) {
  .product-split { grid-template-columns: minmax(0, 1fr); gap: var(--space-12); }
}

/* ---------------- Chapter 02 — Three pillars ---------------- */
.pillars {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  border-top: 1px solid var(--border);
}
@media (max-width: 820px) { .pillars { grid-template-columns: minmax(0, 1fr); } }
.pillar {
  padding: var(--space-10) var(--space-8) var(--space-10) 0;
  border-bottom: 1px solid var(--border);
  position: relative;
}
.pillar + .pillar { padding-left: var(--space-8); border-left: 1px solid var(--border); }
.pillar-num {
  display: block;
  margin-bottom: var(--space-6);
  color: var(--primary);
  font-family: var(--display);
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: var(--track-head);
  line-height: 1;
}
.pillar h3 { margin-bottom: var(--space-3); }
.pillar p { color: var(--muted); font-size: var(--step--1); line-height: 1.65; }

/* ---------------- Chapter 03 — The gap ---------------- */
.scene { position: relative; }
.scene-stage {
  position: sticky;
  top: 0;
  z-index: 5;
  min-height: 100svh;
  display: flex;
  align-items: center;
  /* The header is sticky and overlays the stage, so the stage has to
     hold its content clear of it — otherwise a headline that fits the
     viewport still gets its first line eaten. No overflow clipping
     here: only the runway needs it, and it has its own viewport. */
  padding-block: calc(var(--bar-h) + clamp(16px, 3vh, 40px)) clamp(24px, 5vh, 56px);
}

.gap-scene { height: 320vh; }

.chasm {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: clamp(16px, 2vw, 48px);
  margin-top: var(--space-10);
}
@media (max-width: 860px) {
  .chasm { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .chasm-void { display: none; }
}

.pillar-stat {
  padding: var(--space-8);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  background-color: var(--surface-raised);
  background-image: radial-gradient(360px circle at var(--mx, 50%) var(--my, -10%), var(--spotlight), transparent 70%);
  box-shadow: var(--elev-1), var(--highlight);
}
.pillar-stat .stat-num { font-size: var(--step-5); }
.pillar-stat p { margin-top: var(--space-4); color: var(--muted); font-size: var(--step--1); line-height: 1.6; }

.chasm-void {
  width: clamp(60px, 12vw, 190px);
  height: 1px;
  background: repeating-linear-gradient(90deg, var(--primary) 0 6px, transparent 6px 12px);
  opacity: .4;
}

/* ---------------- Terminology marquee ---------------- */
.marquee {
  position: relative;
  overflow: hidden;
  padding-block: var(--space-6);
  border-block: 1px solid var(--border);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marquee-track { display: flex; width: max-content; gap: 0; will-change: transform; }
.marquee-item {
  font-family: var(--display);
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: var(--track-head);
  line-height: 1.2;
  color: var(--ink);
  white-space: nowrap;
  padding-inline: clamp(18px, 2.4vw, 40px);
  display: inline-flex;
  align-items: center;
  gap: clamp(18px, 2.4vw, 40px);
}
.marquee-item::after {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--primary);
  opacity: .5;
  flex: none;
}

/* ---------------- Chapter 06 — The runway ----------------
   The horizontal run. Four cards travel sideways while the
   stage is stuck; the thread is what they are threaded onto. */
.runway { height: 420vh; }
.runway-stage { flex-direction: column; justify-content: center; gap: clamp(30px, 5vh, 66px); }
.runway-head { width: 100%; }

/* `overflow-x: clip`, NOT `overflow: hidden`. The cards carry a
   velocity-driven skew, and `hidden` would shear their corners off
   against the viewport edge — the clipping this fixes. `clip` also
   avoids forcing overflow-y to `auto`, which would make this a
   scroll container and break the sticky stage above it. The vertical
   padding gives the skew and the card shadows room to breathe. */
.runway-viewport {
  width: 100%;
  overflow-x: clip;
  overflow-y: visible;
  padding-block: 28px;
}
.runway-track {
  display: flex;
  gap: clamp(18px, 2vw, 34px);
  padding-inline: var(--gutter);
  width: max-content;
  will-change: transform;
}
.runway-card {
  width: clamp(280px, 32vw, 430px);
  flex: none;
  display: flex;
  flex-direction: column;
}
.runway-card .card { display: flex; flex-direction: column; height: 100%; }
.runway-index {
  display: block;
  margin-bottom: var(--space-5);
  color: var(--primary);
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
}
.runway-rail { position: relative; height: 1px; width: 100%; background: var(--border); margin-top: var(--space-1); }
.runway-rail span { position: absolute; left: 0; top: -1px; height: 3px; width: 0; background: var(--primary); border-radius: 2px; }

@media (max-width: 720px) {
  /* Below tablet the horizontal run becomes a normal stack —
     sideways scrubbing on a small screen is a trap, not a film. */
  .runway { height: auto; }
  .runway-stage { position: static; min-height: 0; padding-block: var(--space-section); }
  .runway-viewport { overflow: visible; padding-block: 0; }
  .runway-track { flex-direction: column; width: 100%; padding-inline: 0; transform: none !important; }
  .runway-card { width: 100%; }
  .runway-rail { display: none; }
}

/* ---------------- Chapter 07 — The Index ---------------- */
.ring-wrap {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  width: min(360px, 68vw);
  margin-inline: auto;
}
.ring-svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-arc { fill: none; stroke: var(--primary); stroke-width: 2.5; stroke-linecap: round; }
.ring-readout { position: relative; text-align: center; }
.ring-score {
  font-family: var(--display);
  font-size: clamp(56px, 9vw, 96px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-head);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.ring-score em { font-style: normal; font-size: .34em; font-weight: 400; color: var(--muted-light); letter-spacing: 0; }
.ring-label {
  display: block;
  margin-top: var(--space-3);
  color: var(--muted);
  font-size: var(--step--2);
  font-weight: 700;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
}

.gates { display: grid; gap: var(--space-2); margin-top: clamp(26px, 4vh, 42px); }
.gate {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-raised);
  font-size: var(--step--1);
  color: var(--muted);
  transition: border-color 500ms var(--motion-ease), color 500ms var(--motion-ease), background 500ms var(--motion-ease);
}
.gate-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--muted-light);
  flex: none;
  transition: background 500ms var(--motion-ease), box-shadow 500ms var(--motion-ease);
}
.gate.open {
  border-color: rgba(29, 158, 117, .42);
  color: var(--ink);
  background: rgba(29, 158, 117, .06);
}
.gate.open .gate-dot { background: var(--teal); box-shadow: 0 0 0 4px rgba(29, 158, 117, .16); }

/* ---------------- Chapter 08 — The branches ----------------
   The line splits into four and lands on four tabs; the panel beneath
   shows whichever audience you pick.

   The whole tab behaviour is an ENHANCEMENT. Without JS the tablist is
   gone and the panels are the plain four-up grid, because a visitor
   with no JS must not be shown one audience out of four. tabs.js opts
   in by setting [data-enhanced].

   Everything here is scoped to .audience-tabs. .role-card and the four
   .role-* colour rules are shared with contact-us and signup, which
   must keep their grids. */

/* --- no-JS / pre-enhancement: today's layout, unchanged --- */
.audience-tablist { display: none; }
.audience-panels {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gutter);
}
.audience-wave { display: none; }
@media (max-width: 1040px) { .audience-panels { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) { .audience-panels { grid-template-columns: minmax(0, 1fr); } }

/* --- enhanced --- */

/* Narrower than the container and centred. The whole chapter already
   has a centre axis — the origin sits on it and the four branches fan
   symmetrically out of it — so the block belongs on that axis rather
   than stretched to the full 1380px, where one paragraph would leave
   half the panel empty. */
.audience-tabs[data-enhanced] {
  max-width: 1040px;
  margin-inline: auto;
}

/* The gap above the tablist is load-bearing, not taste. The branch
   curves derive their control points from (target.y - origin.y); with
   the tabs sitting tight under .branch-origin that delta collapses and
   the fan becomes four cramped stubs. This is the room the split needs
   to read as a split. */
/* Full width, tabs sharing it equally — not the reference's compact
   toolbar. The four tab centres ARE the four branch endpoints, so a
   max-content bar would fan the split across 350px and the one moment
   in the film where the line becomes four would read as a small crest.
   Spread across the panel's width, it reads as a split. */
.audience-tabs[data-enhanced] .audience-tablist {
  position: relative;
  display: flex;
  gap: 4px;
  margin: clamp(56px, 7vw, 110px) 0 0;
  padding: 5px;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface-raised);
  box-shadow: var(--elev-1), var(--highlight);
}
.audience-tab {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-width: 0;
  padding: 11px 22px;
  border: 0;
  border-radius: var(--r-pill);
  background: none;
  color: var(--muted);
  font-family: var(--ui);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: -.005em;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--motion-pointer) var(--motion-ease);
}
.audience-tab:hover { color: var(--ink); }
.audience-tab[aria-selected="true"] { color: #fff; }
.audience-tab:focus-visible { outline: 2px solid var(--role, var(--primary)); outline-offset: 3px; }

/* The pill is one element that slides, not four that cross-fade —
   which is the whole trick of the reference. tabs.js sizes and
   positions it from the selected button's box. */
.audience-pill {
  position: absolute;
  top: 5px;
  left: 0;
  height: calc(100% - 10px);
  border-radius: var(--r-pill);
  background: var(--primary);
  box-shadow: 0 6px 16px -8px rgba(14, 13, 26, .5);
  pointer-events: none;
}

/* All four panels share ONE grid cell. That is what keeps the section a
   fixed height: the row is sized to the tallest panel and every panel
   is always laid out, so switching tabs cannot move the document by a
   pixel — and the thread is authored in document coordinates, so a
   section that resizes on click would invalidate every anchor below it.
   Absolute positioning would have needed JS to measure and pin a
   height, and would have gone stale on font load and resize. */
.audience-tabs[data-enhanced] .audience-panels {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  margin-top: var(--space-6);
}
.audience-tabs[data-enhanced] .audience-panel {
  grid-area: 1 / 1;
  overflow: hidden;
  /* The deep foot is the waveform's band. Without it the wave crosses
     the link and reads as a stray stroke rather than a decoration. */
  padding: clamp(30px, 3.2vw, 52px) clamp(26px, 3vw, 48px) clamp(76px, 7.5vw, 104px);
}
/* opacity, never display/visibility/aria-hidden. copycheck.mjs skips
   display:none and aria-hidden subtrees outright, and visibility:hidden
   empties innerText — any of the three would report three of the four
   audiences as missing copy. */
.audience-tabs[data-enhanced] .audience-panel:not([data-active]) {
  opacity: 0;
  pointer-events: none;
}
.audience-tabs[data-enhanced] .audience-panel:hover { transform: none; box-shadow: var(--elev-1), var(--highlight); }
.audience-panel-body {
  position: relative;
  z-index: 1;
  max-width: 64ch;
}
.audience-tabs[data-enhanced] .audience-panel h3 { font-size: var(--step-2); }
.audience-tabs[data-enhanced] .audience-panel p { font-size: var(--step-0); max-width: 62ch; }
.audience-tabs[data-enhanced] .audience-panel .text-link { margin-top: var(--space-5); padding-top: 0; }

/* The waveform, adapted from the reference's signature decoration.
   CSS drift rather than GSAP so it needs no teardown; the paths repeat
   every half-width, so a -50% translate loops seamlessly. */
.audience-tabs[data-enhanced] .audience-wave {
  display: block;
  position: absolute;
  left: 0; bottom: -1px;
  width: 200%;
  /* The base sheet's `img, svg { max-width: 100% }` would clamp that
     200% back to 100%, and the drift would then slide the wave off its
     own right edge and leave a growing bare gap. */
  max-width: none;
  height: clamp(74px, 9vw, 118px);
  fill: none;
  stroke: var(--role, var(--primary));
  pointer-events: none;
}
.audience-wave-a { stroke-width: 2; opacity: .17; }
.audience-wave-b { stroke-width: 1.4; opacity: .1; }
html[data-motion] .audience-tabs[data-enhanced] [data-active] .audience-wave {
  animation: audience-drift 24s linear infinite;
}
@keyframes audience-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (max-width: 760px) {
  /* Scrolls rather than collapsing. The tab buttons are the branch
     targets, and docRect() on a display:none element returns zeros —
     which would draw all four curves from document (0,0). */
  .audience-tabs[data-enhanced] .audience-tablist {
    overflow-x: auto;
    scrollbar-width: none;
  }
  .audience-tabs[data-enhanced] .audience-tablist::-webkit-scrollbar { display: none; }
  /* Equal shares would squeeze "Professionals" to nothing; let each tab
     take its own width and let the row scroll instead. */
  .audience-tab { flex: none; padding: 10px 16px; }
}

/* ---------------- Chapter 11 — Arrival ----------------
   The line ends here, on the dot. When the tip lands, two rings
   expand out of it and the dot gives a short pop — the punctuation
   the whole route has been travelling toward. film.js adds .arrived
   when the drawn length reaches the end and removes it when it falls
   back, so scrolling away and returning replays it. */
.arrival { position: relative; overflow: hidden; }
.terminus {
  position: relative;
  width: 11px; height: 11px;
  border-radius: 50%;
  background: var(--primary);
  box-shadow: 0 0 0 7px rgba(61, 54, 187, .12), 0 0 0 15px rgba(61, 54, 187, .06);
  margin: 0 auto clamp(28px, 4vh, 48px);
}
.terminus::before,
.terminus::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1.5px solid var(--primary);
  opacity: 0;
  pointer-events: none;
}
@keyframes terminus-ripple {
  from { transform: scale(1); opacity: .55; }
  to   { transform: scale(7); opacity: 0; }
}
@keyframes terminus-pop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.55); }
  100% { transform: scale(1); }
}
html[data-motion] .terminus.arrived { animation: terminus-pop 620ms var(--motion-ease-out); }
html[data-motion] .terminus.arrived::before { animation: terminus-ripple 1100ms var(--motion-ease-out) forwards; }
html[data-motion] .terminus.arrived::after { animation: terminus-ripple 1100ms var(--motion-ease-out) 140ms forwards; }

@media (prefers-reduced-motion: reduce) {
  .terminus.arrived,
  .terminus.arrived::before,
  .terminus.arrived::after { animation: none; }
}

/* ---------------- Film reveals ----------------
   The hero is driven by GSAP on load, not by the shared observer,
   so it needs its own pre-hidden state — otherwise the headline
   paints once at rest before the timeline yanks it back down. */
html[data-motion] .film-hero .hero-lines .line-in {
  transform: translateY(140%);
  will-change: transform;
}
html[data-motion] .film-hero [data-hero-in] { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  html[data-motion] .film-hero .hero-lines .line-in { transform: none; }
  html[data-motion] .film-hero [data-hero-in] { opacity: 1; }
  .hero-cue-rail::after { animation: none; }
  .glow-a, .glow-b { animation: none; }
}
