/* =====================================================================
   CRAFT & EFFECT — STAGE STYLES
   Scaffold pass: both screens are laid out and styled to the mockups.
   The scrubbed transition layer (GSAP/Lenis) mounts on top of this and
   is NOT required for the page to be correct, readable or usable.
   ===================================================================== */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body, h1, p, figure, form, fieldset, legend { margin: 0; padding: 0; }
fieldset { border: 0; }

/* `[hidden]` must actually hide. Any element that sets its own `display`
   (`.confirm` and `.form` both do) beats the UA stylesheet's
   [hidden]{display:none} — which is exactly why the confirmation message was
   showing on page load rather than only after a submit. */
[hidden] { display: none !important; }

body {
  /* Defence in depth against the sideways-scroll bug. `clip` rather than
     `hidden` on purpose: `hidden` would make this a scroll container and
     break ScrollTrigger's pinning. The real fix is `overflow: hidden` on
     .intro, which clips the scaling wordmark at source; this only catches
     anything that ever escapes it. */
  overflow-x: clip;
  font-family: var(--font-text);
  font-size: var(--fs-body);
  line-height: 1.5;
  color: var(--text-primary);
  background: var(--bg-intro);
  -webkit-font-smoothing: antialiased;
}

/* Focus is never removed — only restyled. A keyboard user must always be
   able to see where they are. */
:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

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

/* =====================================================================
   SCREEN 1 — INTRO
   ===================================================================== */
.intro {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  background: var(--bg-intro);
  position: relative;
  /* The wordmark scales past 24x inside here. Without this the overflow
     becomes real document width and the page scrolls sideways, exposing
     fragments of the giant ampersand over the section below. */
  overflow: hidden;
}

.intro__lockup {
  display: grid;
  justify-items: center;
  gap: clamp(1.5rem, 3.5vw, 2.5rem);
  width: 100%;
  max-width: 900px;
}

/* The wordmark is outlined SVG, so it is resolution-independent and carries
   no font dependency. It is also the LCP element, which is why it is inlined
   in the document rather than fetched. */
.intro__wordmark {
  width: min(72vw, 900px);
  height: auto;
  display: block;
}

.intro__tagline {
  font-family: var(--font-text);
  font-weight: 700;
  font-size: var(--fs-tagline);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-primary);
  text-align: center;
  text-indent: var(--track-label); /* balance the trailing letter-space */
}
.intro__tagline .arrow { color: var(--text-primary); }

/* --- Scroll cue. Ported from paris-str at identical scale, re-rendered
       in Ink to sit on alabaster. --------------------------------------- */
.scrollcue {
  position: absolute;
  left: 50%; bottom: 2.25rem;
  transform: translateX(-50%);
  display: grid; justify-items: center; gap: 0.7rem;
  color: var(--text-primary);
}
.scrollcue__label {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  padding-left: var(--track-label);
}
.scrollcue__mouse {
  width: 32px; height: 52px;
  border: 2px solid var(--text-primary);
  border-radius: 17px;
  display: grid; justify-items: center;
  padding-top: 9px;
}
.scrollcue__wheel {
  width: 4px; height: 11px; border-radius: 2px;
  background: var(--text-primary);
  animation: wheel 1.9s var(--ease) infinite;
}
.scrollcue__arrow { animation: cue 1.9s var(--ease) infinite; }

@keyframes wheel {
  0%   { transform: translateY(0);    opacity: 1; }
  55%  { transform: translateY(17px); opacity: 0; }
  56%  { transform: translateY(0);    opacity: 0; }
  100% { transform: translateY(0);    opacity: 1; }
}
@keyframes cue {
  0%, 100% { transform: translateY(-4px); opacity: 0.4; }
  50%      { transform: translateY(4px);  opacity: 1; }
}

/* Paris' clearance rule: the cue needs ~160px, which a short viewport does
   not yield. Shed the label first, then drop the cue entirely. */
@media (max-height: 780px) {
  .scrollcue { bottom: 1.5rem; gap: 0.4rem; }
  .scrollcue__label { display: none; }
  .scrollcue__mouse { width: 26px; height: 42px; padding-top: 7px; }
  .scrollcue__arrow { width: 24px; height: 24px; }
}
@media (max-height: 640px) { .scrollcue { display: none; } }

/* =====================================================================
   SECTION GROUNDS
   Flat, opaque, and owned by the section. There was a crossfade here that
   tweened a fixed panel between orange and light; it is gone. Scrubbing a
   colour across a section boundary means the page spends most of that
   boundary in a half-mixed tint that belongs to neither screen, and it
   forced both sections to be padded out with empty scroll to give the
   fade somewhere to happen. A hard edge scrolling past reads as two
   rooms. A tweened one reads as a rendering fault.

   The intro's ampersand flood is untouched: that is a zoom landing on its
   own colour, not a section changing its mind.
   ===================================================================== */
.intro, .signals, .leadform, .sitefoot { position: relative; z-index: 1; }

.signals  { background: var(--bg-orange); color: var(--text-on-orange); }
.leadform { background: var(--bg-light);  color: var(--text-primary); }
.sitefoot { background: var(--bg-orange); color: var(--text-on-orange); }

/* =====================================================================
   SCREEN 2 — THE FOUR SIGNALS
   ===================================================================== */
/* The cards are parked 48px off to one side before they drift in, and on a
   393px phone a 320px card plus that offset reaches past the viewport edge.
   `overflow-x: clip` on BODY does not catch it: body clips its own overflow,
   but `html` stays `overflow-x: visible` and the document scrolls anyway.
   So the clip goes on the element that actually contains the movement.

   `clip` rather than `hidden`, and x-only: when one axis is `clip` the other
   may stay `visible` without computing to `auto`, so this never becomes a
   scroll container — which would re-parent the sticky screen inside it and
   break the desktop stage. */
.signals { overflow-x: clip; }

.signals__screen {
  min-height: 100svh;
  max-width: 76rem;
  margin-inline: auto;
  padding: clamp(2.5rem, 9svh, 6rem) var(--gutter);
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: center;
  gap: clamp(1.5rem, 5svh, 3.25rem);
}

/* The ONLY alabaster-on-orange text in the page. At this size it is large
   text, so 3.58:1 clears the 3:1 threshold properly. Everything smaller on
   orange is Ink at 4.58:1. MASTER §2.2. */
.signals__title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--fs-display);
  line-height: 1.1;
  letter-spacing: -0.015em;
  text-align: center;
  text-wrap: balance;
  color: var(--text-on-ink);
  margin: 0;
}

.signals__map { position: relative; display: grid; }

/* The list STRETCHES to the map, and the four rows are distributed across
   it with space-evenly. Centring them instead left the blocks huddled in
   the middle third with a long bare run of spine underneath. */
.signals__list {
  position: relative;   /* paints above the spine */
  z-index: 1;
  list-style: none;
  margin: 0; padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-content: space-evenly;
}

/* Rows are set explicitly. Left to auto-placement the four blocks pair up
   into two rows of two; one block per row is what the alternation needs.
   Even row-gap is what evens the spacing out — the mock's own gaps run
   135 / 115 / 195px, which is drawing, not rhythm. */
.signal:nth-child(1) { grid-row: 1; grid-column: 1; }
.signal:nth-child(2) { grid-row: 2; grid-column: 2; }
.signal:nth-child(3) { grid-row: 3; grid-column: 1; }
.signal:nth-child(4) { grid-row: 4; grid-column: 2; }

/* Both columns hang off the spine at the same distance, text left-aligned
   in each — which is what the drawing shows, and what keeps the two
   columns reading as one thread rather than two lists.

   Each block is a card on an alabaster ground. Four small type blocks
   floating directly on a full-bleed orange field read as debris next to
   the spine; giving them an edge makes them objects the line connects.
   The padding is what keeps the type off that edge. */
.signal {
  width: min(20.5rem, 100%);
  background: var(--surface-signal);
  border-radius: 4px;
  padding: 1.15rem 1.35rem 1.25rem;
}
.signal:nth-child(odd)  { justify-self: end;   margin-right: clamp(1.25rem, 4vw, 4rem); }
.signal:nth-child(even) { justify-self: start; margin-left:  clamp(1.25rem, 4vw, 4rem); }

.signal__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-signal-name);
  line-height: 1.25;
  /* Ember, not brand orange. On this alabaster card the brand #EF4510
     measures 3.58:1 and fails AA at 17px; ember is 4.98:1 and passes.
     At this size on alabaster the two are hard to tell apart. One token
     (--surface-signal's partner, --text-signal-name) reverts it. */
  color: var(--text-signal-name);
  margin: 0 0 0.25rem;
}
.signal__ask {
  font-size: var(--fs-signal);
  line-height: 1.45;
  color: var(--text-primary);   /* 16.4:1 on the card */
  text-wrap: pretty;
}

/* --- The spine ------------------------------------------------------
   A 2px rail scaled on Y, which is a compositor transform. An SVG
   stroke-dashoffset draw would repaint the same picture every frame.
   It renders FULLY DRAWN by default: app.js parks it at scaleY(0), so
   with JavaScript off or motion reduced the line is simply there. */
.spine {
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  transform: translateX(-50%);
  display: grid;
  grid-template-rows: auto 1fr auto;
  justify-items: center;
  z-index: 0;
  pointer-events: none;
}
.spine__cap {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--spine);
}
.spine__rail { width: var(--spine-w); align-self: stretch; }
.spine__draw {
  display: block;
  width: 100%; height: 100%;
  background: var(--spine);
  transform-origin: top center;
}

/* --- Touch and narrow: one centred column, spine behind the text -----
   The accent bar is dropped here. Centred text with a left-hand bar over
   a centred line reads as a mistake, and the spine is already doing the
   job of tying the four together.

   The card's own opaque ground is what interrupts the spine, so the line
   passes behind each one rather than through the letterforms. ---------- */
@media (max-width: 860px) {
  .signals__list { grid-template-columns: 1fr; row-gap: clamp(1.75rem, 6svh, 3.5rem); }
  .signal:nth-child(1),
  .signal:nth-child(2),
  .signal:nth-child(3),
  .signal:nth-child(4) {
    grid-column: 1;
    justify-self: center;
    margin-left: 0;
    margin-right: 0;
  }
  /* The card's own opaque ground is what interrupts the spine here, so the
     separate knockout the first pass needed is gone with it. */
  .signal {
    width: min(20rem, 100%);
    text-align: center;
    padding: 1.05rem 1.25rem 1.15rem;
  }
  .signals__list { padding-block: clamp(1.75rem, 5svh, 3rem); }
  /* Just over two screens. At its natural height the whole section fitted
     inside one 812px viewport, so all four blocks were on screen at once
     and there was nothing left for the scroll to reveal. Two screens gives
     each block about half a viewport of its own. */
  .signals__screen {
    min-height: 215svh;
    padding-bottom: clamp(3rem, 10svh, 6rem);
  }
}

/* =====================================================================
   SCREEN 3 — LEAD FORM
   ===================================================================== */
/* Back to one viewport. The 140svh the first pass used existed only to give
   the ground crossfade somewhere to happen; with flat grounds it was just
   40svh of empty alabaster around the form. */
.leadform {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(3.5rem, 10svh, 6.5rem) var(--gutter);
}

.leadform__inner {
  width: 100%;
  max-width: 62rem;
  display: grid;
  justify-items: center;
  gap: clamp(1rem, 2.5svh, 1.75rem);
  text-align: center;
}

.leadform__title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--fs-display);
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--text-primary);
  text-wrap: balance;
  margin: 0;
}

.leadform__lede {
  font-size: var(--fs-lede);
  line-height: 1.55;
  max-width: 58rem;
  color: var(--text-primary);
  text-wrap: pretty;
}
.leadform__lede strong { font-weight: 600; }

/* The kindling mark. Sized in em so it tracks the heading's own clamp()
   rather than needing a breakpoint of its own, and nudged down a little
   because the flame's tip reads higher than the cap line it sits beside. */
.nobr { white-space: nowrap; }
.flame {
  width: 0.86em;
  height: 0.86em;
  vertical-align: -0.1em;
  margin-left: 0.22em;
  overflow: visible;
}

/* The one pure white in the system. Alabaster on alabaster would make the
   card vanish, which is the whole reason --surface-card exists. */
.card {
  width: 100%;
  max-width: 29.5rem;
  margin-top: clamp(0.5rem, 2svh, 1.5rem);
  background: var(--surface-card);
  border: 1px solid var(--rule-card);
  padding: clamp(1.5rem, 4vw, 2.25rem);
  text-align: left;
}

.score-form { display: grid; gap: 1.05rem; }
.card__head { display: grid; gap: 0.4rem; }
.card__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.25rem;
  line-height: 1.25;
  color: var(--text-primary);
  margin: 0;
}
.card__sub { font-size: 0.9rem; line-height: 1.5; color: var(--text-secondary); }

.card__fine { font-size: 0.78rem; line-height: 1.5; color: var(--text-secondary); }

.card__confirm { display: grid; gap: 1rem; padding: 0.75rem 0; }
.card__confirm-msg {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.125rem;
  line-height: 1.45;
  color: var(--text-primary);
}

/* --- Fields ---------------------------------------------------------
   Restyled for a light card; the dark-panel variant went with the modal.
   Placeholders are Graphite (5.74:1), not the mock's lighter grey, which
   measures 2.38:1 and is unreadable for anyone who needs the example. */
.field { display: grid; gap: 0.4rem; }
.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0.85rem; }

.field__label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.req { color: var(--accent); }

.field__input {
  font-family: var(--font-text);
  font-size: var(--fs-body);
  color: var(--text-primary);
  background: var(--field-fill);
  border: 1px solid var(--rule-card);
  border-radius: 4px;
  padding: 0.8rem 0.875rem;
  min-height: var(--tap);
  width: 100%;
  transition: border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}
.field__input::placeholder { color: var(--text-secondary); opacity: 1; }
.field__input:hover { border-color: rgba(27, 26, 23, 0.32); }
.field__input:focus-visible {
  outline: none;
  border-color: var(--ink);
  background: var(--surface-card);
  box-shadow: 0 0 0 2px rgba(239, 69, 16, 0.28);
}
/* :invalid fires before anything has been typed, which marks an untouched
   form as wrong. :user-invalid waits until the field has been left. */
.field__input:user-invalid { border-color: var(--ember); }

.form__error {
  font-family: var(--font-text);
  font-size: 0.875rem;
  color: var(--ember);
  border-left: 3px solid var(--ember);
  padding: 0.55rem 0.75rem;
  background: rgba(184, 72, 10, 0.08);
}

/* --- Filled CTA — brand board v1.3 §05 ------------------------------
   Poppins 600 / 16px / 5px radius / 17px-20px padding / title case, with
   the alabaster label the board specifies. That label measures 3.58:1 on
   brand orange and is a DOCUMENTED EXCEPTION, not an oversight: see
   tokens.css and MASTER §2.2. Hover is 4.96:1 and passes. */
.btn {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  line-height: 1;
  letter-spacing: 0.005em;
  min-height: var(--tap);
  padding: 1.0625rem 1.25rem;
  border: 0;
  border-radius: var(--btn-radius);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.btn--fill { width: 100%; background: var(--btn-fill); color: var(--btn-label); }
.btn--fill:hover { background: var(--btn-fill-hover); }
.btn--fill:disabled { background: var(--btn-fill-disabled); color: var(--btn-label-disabled); cursor: progress; }
.btn__arrow { font-size: 0.95em; }

@media (max-width: 420px) {
  .field-pair { grid-template-columns: 1fr; }
}

/* =====================================================================
   SCREEN 4 — FOOTER
   ===================================================================== */
/* A band, as drawn — no min-height at all. Both earlier attempts (50svh,
   then a full screen) were sized to give the ground crossfade room to
   finish, and both left most of a viewport of empty orange under two small
   objects. Its own padding is the whole of it now. */
.sitefoot {
  display: grid;
  place-items: center;
  padding: clamp(3.5rem, 10svh, 6.5rem) var(--gutter);
}
.sitefoot__inner {
  display: grid;
  justify-items: center;
  gap: clamp(1.75rem, 4svh, 2.75rem);
}
/* The supplied alt lockup carries its own #EF4510 backing plate, which is
   this screen's exact ground — so it reads as bare alabaster letterforms
   and the plate is invisible. */
.sitefoot__logo {
  width: 100%;
  max-width: var(--cta-w);
  height: auto;
  display: block;
}

/* --- Social marks ---------------------------------------------------- */
.socials {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0; padding: 0;
}
.socials li { display: flex; }
.socials__mark {
  width: var(--tap); height: var(--tap);
  border: 1px solid var(--rule-on-orange);
  border-radius: 50%;
  display: grid; place-items: center;
  color: var(--text-on-ink);
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.socials__mark:hover {
  background: var(--ink);
  border-color: var(--ink);
}
.socials__mark svg { width: 20px; height: 20px; display: block; }
a.socials__mark { text-decoration: none; }
a.socials__mark:focus-visible { outline: 2px solid var(--text-on-ink); outline-offset: 3px; }

/* =====================================================================
   REDUCED MOTION
   No pin, no scrub, no looping cue. The two screens become ordinary
   stacked sections that simply scroll. A real path, not an afterthought.
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  .scrollcue__wheel, .scrollcue__arrow { animation: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* =====================================================================
   REVEAL  (app.js toggles .is-in)

   Ported from paris-str at ITS values, because that site is the reference
   for how content should arrive here: 64px of travel, a slight scale down
   and a 6px blur that clears, over 0.85–1.05s. Its own source calls the
   effect "deliberately pronounced", and that is the point. The first pass
   here used a 26px fade with no scale and no blur, which reads as a
   element correcting itself rather than as an entrance.

   CSS owns the travel and the blur; app.js only decides WHEN, and adds a
   stagger so items crossing the line together arrive one after another.

   Guarded the same three ways as the intro: the media query below (reduced
   motion), the <noscript> override in the head (JS off), and the failsafe
   animation (JS on, app.js never ran). Nothing is ever stranded.
   ===================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .reveal {
    opacity: 0;
    transform: translateY(64px) scale(0.965);
    filter: blur(6px);
    transition:
      opacity   0.85s var(--ease),
      transform 1.05s var(--ease),
      filter    0.85s var(--ease);
    transition-delay: var(--d, 0ms);
    will-change: transform, opacity;
    animation: failsafe-reveal 0s linear 4s forwards;
  }
  .reveal.is-in { opacity: 1; transform: none; filter: none; }
}

/* app.js is driving; stand the failsafe down. */
.reveals-on .reveal { animation: none !important; }

/* app.js is deliberately NOT animating. Reveal now. */
.motion-off .reveal {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
}

/* =====================================================================
   TRANSITION STAGE
   Gated on `prefers-reduced-motion: no-preference` in CSS, not on a JS
   class. That means the reduced-motion visitor never reaches a single
   line of it, and no inline <script> guard is needed — which keeps the
   Content-Security-Policy at a strict `script-src 'self'`.

   Three independent safety nets stop content from ever being stranded
   invisible: the media query (reduced motion), the <noscript> override
   in the document head (JS off), and the failsafe animation below
   (JS on but app.js failed to load or threw).
   ===================================================================== */
@keyframes failsafe-reveal { to { opacity: 1; } }

@media (prefers-reduced-motion: no-preference) {
  /* The lockup enters from a larger scale, so it starts hidden. If app.js
     never runs at all, CSS reveals it after 2s. */
  .intro__lockup > h1,
  .intro__tagline,
  .scrollcue {
    opacity: 0;
    animation: failsafe-reveal 0s linear 2s forwards;
  }
}

/* A CSS animation in its `forwards` fill state outranks inline styles in the
   cascade, so the failsafe above would stomp on anything GSAP writes once it
   fired. These two classes hand ownership over explicitly rather than relying
   on the two never overlapping:
     .motion-ready — app.js is driving; stand the failsafe down.
     .motion-off   — app.js is deliberately NOT animating (reduced motion, or
                     GSAP failed to load); reveal now instead of waiting 2s. */
.motion-ready .intro__lockup > h1,
.motion-ready .intro__tagline,
.motion-ready .scrollcue { animation: none !important; }

.motion-off .intro__lockup > h1,
.motion-off .intro__tagline,
.motion-off .scrollcue { opacity: 1 !important; }

/* The orange flood. Sits above the wordmark, and as it reaches full opacity
   the stage IS the Signals screen's colour field — so the pin releases into
   that section with no visible seam.

   It exists because scaling the ampersand alone cannot guarantee a solid
   field: the glyph has counters, and at extreme scale whichever part of the
   glyph sits under the transform origin is what fills the screen. The flood
   is the same #EF4510, so the crossfade is imperceptible — what the eye
   reads is the ampersand's counters closing up. */
.flood {
  position: absolute;
  inset: 0;
  background: var(--bg-orange);
  opacity: 0;
  pointer-events: none;
  z-index: 2;
}

.intro__lockup { position: relative; z-index: 1; }

/* Close the gap between the transition ending and the Signals screen
   arriving.

   `pinSpacing` reserves 250vh and drops the next section AFTER it, which
   left a full viewport of scrolling through finished orange before you
   reached anything. Pulling it up by exactly one viewport makes it land
   flush: the instant the pin reaches its end, the Signals screen occupies
   the screen precisely.

   The z-index is the one the ground layers need (above `.bg`), and it must
   not go higher: the Signals screen has to stay BEHIND the pinned stage
   (which is `position: fixed` while pinned, so it paints above static
   content) — otherwise it would creep up over the zooming ampersand during
   the transition. The stage fades out at 92% progress to reveal it, and both
   are the same orange by then, so the handoff cannot be seen.

   Scoped to `.stage-active` so the reduced-motion, no-JS and touch paths,
   which have no pin and no spacer, keep ordinary stacked sections. */
.stage-active .signals { margin-top: -100svh; }

/* --- The Signals screen holds still with `position: sticky`, NOT a second
   ScrollTrigger pin.

   A second pin would mean a second pin-spacer, and the spacer is what cost
   us a day in September: ScrollTrigger copies the pinned element's z-index
   onto it, it spans the whole section, and it defaults to
   `pointer-events: auto` — so it silently ate every click below it. Sticky
   has no spacer at all. ScrollTrigger is then only a reader of progress
   across the wrapper; it moves nothing itself, so there is nothing for a
   refresh to recompute wrongly.

   Scoped to `.stage-active`, so touch and the static paths get a section at
   its natural height instead. */
.stage-active .signals { min-height: 300svh; }
.stage-active .signals__screen {
  position: sticky;
  top: 0;
  height: 100svh;
  min-height: 0;
}

/* The stage must sit ABOVE the Signals screen for the whole transition, and the
   z-index has to be explicit — relying on "fixed elements paint above static
   ones" is not safe here. ScrollTrigger pins with a transform rather than
   `position: fixed` in some configurations (touch, and when the scroller has
   a non-visible overflow — this page sets `overflow-x: clip` on body). A
   transformed element is not a positioned one, so paint order falls back to
   DOM order and the section below, being the later sibling, came out on top:
   the dials rose over the zooming ampersand.

   The stage still fades out at 0.92 to hand over, so this only governs who
   covers whom on the way there. */
.stage-active .intro { z-index: 2; }

/* ...but ScrollTrigger COPIES that z-index onto the .pin-spacer it wraps the
   stage in. The spacer is 250vh tall, spans the section below, sits above it,
   and defaults to `pointer-events: auto` — so it silently swallowed every
   hover and click below it: the CTA, the LinkedIn link, all of it. Fading
   the stage out did not help, because the blocker was the spacer, not the
   stage.

   Nothing inside the spacer is ever interactive (wordmark, tagline, scroll
   cue, flood are all inert), so it can opt out of hit-testing entirely.
   Scrolling is unaffected — wheel and touch do not care about
   `pointer-events`. */
.stage-active .pin-spacer { pointer-events: none; }

/* =====================================================================
   LENIS — required stylesheet
   Copied verbatim from node_modules/lenis/dist/lenis.css. Lenis does NOT
   inject these itself (verified against the 1.3 bundle), and without them
   the smooth-scroll container sizes wrongly and `lenis.stop()` — which is
   what used to hold the page still under the open modal — does nothing.
   Nothing calls `stop()` now, but the sizing rules still matter.

   Inlined here rather than linked as a fourth stylesheet: it is ~20 lines,
   and on a page this small an extra request costs more than the bytes.
   Re-copy it if `npm run vendor` pulls a new Lenis major.
   ===================================================================== */
html.lenis,
html.lenis body { height: auto; }

.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }

.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch],
.lenis [data-lenis-prevent-vertical],
.lenis [data-lenis-prevent-horizontal] { overscroll-behavior: contain; }

.lenis.lenis-smooth iframe { pointer-events: none; }

.lenis.lenis-autoToggle {
  transition-property: overflow;
  transition-duration: 1ms;
  transition-behavior: allow-discrete;
}

/* =====================================================================
   TOUCH STAGE — the one-shot dive
   No pin, no spacer, no scroll-position maths. The intro is a fixed
   overlay covering the visual viewport, which is what makes this immune
   to the three mobile bugs: a fixed inset:0 element always matches the
   real viewport, so a retracting URL bar cannot open a gap the way
   `min-height: 100svh` did.
   ===================================================================== */
.stage-mobile .intro {
  position: fixed;
  inset: 0;
  z-index: 50;
  min-height: 0;   /* inset:0 governs the size; svh must not fight it */
}

/* Locked until the dive has played, so the page below cannot be reached by
   simply scrolling past the intro. */
.stage-locked,
.stage-locked body { overflow: hidden; }

/* Dive complete: the overlay is gone and the page IS the Signals screen. */
.stage-done .intro { display: none; }

/* Paint the page orange once the dive is done. Sections are
   `min-height: 100svh`, which is deliberately the SHORT viewport — so when
   the URL bar retracts there is a sliver below. Making the body orange means
   that sliver matches the field above it instead of exposing alabaster,
   which is exactly the strip that showed while scrolling.

   The fixed ground layers cover the viewport proper, so this now governs
   only the overscroll region. */
.stage-done body { background: var(--bg-orange); }

