/* ==========================================================================
   Radio Silence — section layouts: hero, feature sections, capture stage,
   privacy, finale, footer
   ========================================================================== */

/* ----- Top bar ----- */

.topbar {
  position: absolute; /* no-JS fallback: bar scrolls away as before */
  top: 0; left: 0; right: 0;
  z-index: 10;
  transition: padding 0.5s var(--ease-out-quint);
}

/* Pinned bar only when JS can toggle .is-scrolled; no-JS visitors keep
   the absolute scroll-away bar above. */
@media (scripting: enabled) {
  .topbar { position: fixed; }
}

/* Scrolled: the bar insets from the viewport and the .wrap box becomes
   an Apple-style rounded blurred pill (painted by .wrap::before). */
.topbar.is-scrolled { padding: 10px 12px 0; }

.topbar .wrap {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 28px;
  padding-bottom: 28px;
  transition: padding 0.5s var(--ease-out-quint);
}

.topbar.is-scrolled .wrap {
  padding-top: 13px;
  padding-bottom: 13px;
}

/* Pill paint lives on a pseudo-element — backdrop-filter must NEVER go on
   .topbar or .wrap themselves: it would turn them into the containing
   block for the fixed full-screen [open] menu overlay below, confining it
   to the bar's box. A pseudo has no descendants, so it's safe. */
.topbar .wrap::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1; /* above page content, below wordmark/menu/badge + overlay */
  border-radius: 999px;
  background: rgba(10, 10, 10, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.08);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease-out-quint);
}

.topbar.is-scrolled .wrap::before {
  opacity: 1;
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
}

.wordmark {
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0.38em;
  color: var(--rs-white);
  text-decoration: none;
  /* Keep "RADIO SILENCE" on one line — otherwise the wider [ CLOSE ] label
     (vs [ MENU ]) shrinks the flex row enough to wrap the wordmark at ~375px. */
  white-space: nowrap;
}

/* Site nav at every breakpoint: [ MENU ] sits right of center next to
   [ BETA ] and opens a full-screen overlay. Native <details> — no JS. */
.topbar__menu { position: relative; margin-left: auto; margin-right: 16px; }

/* The bar's own content must float above the overlay it opens. */
.wordmark, .topbar__menu-btn { position: relative; z-index: 1; }

body:has(.topbar__menu[open]) { overflow: hidden; }

/* The [ MENU ] / [ CLOSE ] button (the <summary>). */
.topbar__menu-btn {
  list-style: none; /* drop the native disclosure triangle */
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--rs-logbook-meta);
  white-space: nowrap;
  transition: color 0.25s;
}

.topbar__menu-btn::-webkit-details-marker { display: none; }
.topbar__menu-btn:hover { color: var(--rs-amber); }

.topbar__menu-label-open { display: none; }
.topbar__menu[open] .topbar__menu-label-closed { display: none; }
.topbar__menu[open] .topbar__menu-label-open { display: inline; }

/* Collapsed by default; only paints when the <details> is open. Explicit
   (rather than relying on the UA's closed-details hiding, which some
   engines apply via content-visibility that our layout can defeat). */
.topbar__menu-list { display: none; }

.topbar__menu[open] .topbar__menu-list {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
  padding: 140px 32px 0;
  background: rgba(0, 0, 0, 0.94);
  backdrop-filter: blur(20px);
  transition: opacity 0.35s ease;
  transition-behavior: allow-discrete;
}

.topbar__menu[open] .topbar__menu-list a {
  font-family: var(--font-mono);
  font-size: 20px;
  letter-spacing: 0.14em;
  color: var(--rs-logbook-meta);
  text-decoration: none;
  transition: color 0.25s, opacity 0.35s ease, transform 0.35s ease;

  /* Fade/slide in on open. */
  opacity: 1;
  transform: translateY(0);
}

.topbar__menu[open] .topbar__menu-list a:hover { color: var(--rs-amber); }

.topbar__menu[open] .topbar__menu-list a:nth-child(1) { transition-delay: 0.06s; }
.topbar__menu[open] .topbar__menu-list a:nth-child(2) { transition-delay: 0.12s; }
.topbar__menu[open] .topbar__menu-list a:nth-child(3) { transition-delay: 0.18s; }

@starting-style {
  .topbar__menu[open] .topbar__menu-list { opacity: 0; }
  .topbar__menu[open] .topbar__menu-list a {
    opacity: 0;
    transform: translateY(10px);
  }
}

/* Desktop: same overlay, links scale up and left-align to the .wrap content
   column (536 = var(--page-max)/2 - 24px padding) under the wordmark. */
@media (min-width: 900px) {
  .topbar__menu[open] .topbar__menu-list {
    padding: 180px 24px 0 max(24px, calc(50vw - 536px));
  }

  .topbar__menu[open] .topbar__menu-list a { font-size: 28px; }
}

/* ----- Tool pages: desktop sidebar switcher -----
   The three /tools/ pages center their content in a narrow ch-based column,
   leaving the rest of a wide viewport empty. This surfaces the other tools
   in that gutter so visitors don't have to open [ MENU ] or scroll to the
   footer to discover them. Below the breakpoint, [ MENU ] + the footer
   links (both already list all three tools) are the only nav — this stays
   hidden rather than fighting for space. The current-page ▸ marker mirrors
   the app's station-list convention (Color.rsTacticalOrange, U+25B8) for
   visual continuity between the site and the app. */
.tools-side-nav {
  display: none;
  position: fixed;
  /* Grows past the 64px floor as the viewport widens, so the gutter's
     spare width goes partly to breathing room instead of leaving the
     sidebar glued to the browser edge on very wide screens. */
  left: max(64px, calc(50vw - 576px));
  top: 120px;
  flex-direction: column;
  gap: 16px;
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0.08em;
  z-index: 5;
}

.tools-side-nav__link {
  display: flex;
  align-items: baseline;
  color: var(--rs-meta);
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.25s, border-color 0.25s, background-color 0.25s, box-shadow 0.25s;
}

.tools-side-nav__link:hover { color: var(--rs-amber); }

.tools-side-nav__link.is-current { color: var(--rs-orange); }

.tools-side-nav__mark {
  display: inline-block;
  width: 14px;
  flex: none;
}

@media (min-width: 1280px) {
  .tools-side-nav { display: flex; }
}

/* ----- Tool pages: mobile/tablet switcher -----
   Below the sidebar breakpoint, the same <nav> markup becomes a horizontal,
   scrollable tab strip right under the topbar — reusing the .tray segment
   look already established by the FREQ/WAVELENGTH-style toggles elsewhere,
   so it reads as the same control language rather than a new pattern.
   [ MENU ] and the footer still list all three tools too; this just adds
   an always-visible option instead of requiring a tap to discover them. */
@media (max-width: 1279px) {
  .tools-side-nav {
    display: flex;
    position: static;
    flex-direction: row;
    align-items: center;
    /* `safe` centers only while everything fits; once the row overflows
       (narrow phones) it falls back to start-aligned so the current pill
       never gets centered half off-screen with no way to scroll back to
       it. Unsupported browsers just ignore the whole declaration and get
       the same safe start-aligned fallback. */
    justify-content: safe center;
    gap: 8px;
    width: auto;
    max-width: var(--page-max);
    margin: 0 auto;
    padding: 88px 24px 0;
    font-size: 12px;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .tools-side-nav::-webkit-scrollbar { display: none; }

  .tools-side-nav__link {
    flex: none;
    padding: 10px 14px;
    border: 1px solid var(--rs-header-divider);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
  }

  .tools-side-nav__link.is-current {
    border-color: var(--rs-orange);
    background: rgba(255, 107, 26, 0.06);
    box-shadow: 0 0 10px rgba(255, 107, 26, 0.18);
  }

  .tools-side-nav__mark { display: none; }
}

/* ----- Hero (Framer layout: copy above, device below) ----- */

.hero {
  position: relative;
  text-align: center;
  /* The intro's zoomed opening pose paints past the section edge on narrow
     screens; clip (not hidden — no scroll container side effects, and body's
     overflow-x:hidden is unreliable on iOS Safari) keeps it inside. */
  overflow-x: clip;
}

/* Mobile / no-JS / reduced motion: .hero__pin is a plain flow div and the
   hero renders exactly as it always has. */
.hero__pin {
  display: flex;
  flex-direction: column;
  /* `safe` centering: when the copy+device stack is taller than the pin
     (100svh in the desktop dive below — e.g. a 1280×720 laptop, where the
     stack overruns the viewport), fall back to start-alignment instead of
     centering, so the kicker/headline never clip above the fold. On a tall
     enough viewport this is identical to plain `center`. */
  justify-content: safe center;
  padding: clamp(120px, 16vh, 180px) 0 clamp(64px, 9vh, 110px);
}

/* Desktop zoom-in dive (reverse of Apple's iPhone hero scrub): the hero
   becomes a scroll runway with a sticky pin — same mechanism as the RECORD
   .stage below — and herodive.js scrubs the device scale with progress.
   The extra 130vh is the dive's scroll distance. */
@media (min-width: 900px) {
  .hero { height: 230vh; }

  .hero__pin {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
  }

  .hero__device .device { will-change: transform; }
}

@media (prefers-reduced-motion: reduce) {
  .hero { height: auto; }
  .hero__pin { position: static; height: auto; overflow: visible; }
}

.no-motion .hero { height: auto; }
.no-motion .hero__pin { position: static; height: auto; overflow: visible; }

/* .wrap's `margin: 0 auto` overrides flex stretch alignment on the cross
   axis (auto margins win over align-items per spec), so without an
   explicit width it shrinks-to-fit the hero device's content and can
   overflow the viewport on narrow screens. Force it back to full width;
   max-width + the existing auto margins still center it on wide screens. */
.hero .wrap { width: 100%; }

.hero__kicker {
  min-height: 1.4em; /* reserve space for the typing effect */
}

.hero__headline {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(36px, 7.4vw, 92px);
  line-height: 1.02;
  letter-spacing: -0.025em;
  margin: 20px auto 22px;
  max-width: 12em;
}

.hero__sub {
  margin: 0 auto;
  max-width: 36em;
}

.hero__ctas {
  display: flex;
  gap: 16px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: clamp(28px, 4vh, 44px);
}

.hero__device {
  margin-top: clamp(48px, 8vh, 96px);
}

/* Apple-style opening pose: the phone arrives alone, tilted back (top edge
   away, like the iPhone 17 Pro hero shot) and so large it fills the screen.
   It holds that pose for 0.5s (fill: both applies `from` during the delay),
   then settles flat over 2.6s with a heavy ease (slow start, long glide) —
   mass in motion. One-shot on load. Lives on its own wrapper so it composes
   with the entrance rise (outer .hero__device) and the scroll dive's inline
   transforms (inner .device) without ever sharing an element. */
.hero__intro3d {
  animation: hero-tilt-settle 2.6s cubic-bezier(0.55, 0, 0.15, 1) 0.5s both;
  will-change: transform;
}

@keyframes hero-tilt-settle {
  from { transform: perspective(1400px) translateY(-14%) rotateX(55deg) scale(2.6); }
  to   { transform: perspective(1400px) translateY(0) rotateX(0deg) scale(1); }
}

/* Nothing but the phone during the settle: topbar, hero copy/CTAs, and the
   scroll cue stay dark until the phone lands (3.1s = 0.5s hold + 2.6s
   settle), then fade in slowly. fill-mode MUST be `backwards`, never
   forwards/both — a forwards fill on opacity would permanently outrank the
   inline opacity herodive.js writes to .hero__copy-block during the scroll
   dive and break the copy fade-out. (The 3.1s is mirrored by the typing
   delay in main.js.) */
.topbar,
.hero__copy-block {
  animation: hero-ui-in 1.4s var(--ease-out-quint) 3.1s backwards;
}

.hero__cue {
  animation:
    hero-ui-in 1.4s var(--ease-out-quint) 3.1s backwards,
    cue-pulse 2s ease-in-out 4.5s infinite;
}

@keyframes hero-ui-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__intro3d, .topbar, .hero__copy-block { animation: none; }
  .hero__cue { animation: none; }
  /* Pill morph snaps instead of gliding. This media query is the only
     guard on tool pages — they don't load main.js, so .no-motion is
     never set there. */
  .topbar, .topbar .wrap, .topbar .wrap::before { transition: none; }
}

.no-motion .hero__intro3d,
.no-motion .topbar,
.no-motion .hero__copy-block { animation: none; }
.no-motion .hero__cue { animation: none; }
.no-motion .topbar,
.no-motion .topbar .wrap,
.no-motion .topbar .wrap::before { transition: none; }

/* Base styles only — the animation list (delayed hero-ui-in fade + the
   cue-pulse loop) lives in the intro block above, gated behind the phone's
   settle. */
.hero__cue {
  text-align: center;
  margin-top: 40px;
}

.hero__cue svg {
  display: inline-block;
  width: 26px;
  height: 26px;
  fill: none;
  stroke: var(--rs-dim);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Starts at full opacity so it hands off seamlessly from the hero-ui-in
   fade (which ends at 1 exactly when this begins), then dips. */
@keyframes cue-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

/* Waveform the dive reveals as the device dissolves — a standalone canvas
   port of the same silk-thread horizon that lives inside the tuner video,
   positioned over the video's waveform so it reads as the one element that
   survives to black. Raised only by herodive.js on desktop; invisible and
   idle everywhere else. */
.hero__wf {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 220px;
  opacity: 0;
  pointer-events: none;
  z-index: 2;
}

.hero__wf canvas { width: 100%; height: 100%; }

/* ----- Feature sections: text + visual, alternating ----- */

.feature {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(40px, 6vw, 80px);
  align-items: center;
}

.feature__copy,
.feature__visual { min-width: 0; }

@media (min-width: 900px) {
  .feature { grid-template-columns: 5fr 6fr; }
  /* visual leads (Framer order): visual left, copy right */
  .feature--flip .feature__copy { order: 2; }
  .feature--flip .feature__visual { order: 1; }
  .feature--flip { grid-template-columns: 6fr 5fr; }
}

/* Mobile (stacked, single column): in pill-driven feature--flip sections
   (LISTEN), show the device visual right after the sub copy, before the
   pill list, instead of after it — the phone demo reads better once you've
   seen it, before picking a technique to read about. Scoped via :has(.pills)
   so NETWORK's plain feature--flip (no pills) keeps copy-then-visual.
   feature__copy is flattened so its children (kicker/headline/sub/pills)
   become siblings of feature__visual in the grid, letting `order` place the
   visual between them; kicker/headline/sub keep their default order (0),
   which still sorts them ahead of both. */
@media (max-width: 899px) {
  .feature--flip:has(.pills) .feature__copy { display: contents; }
  .feature--flip:has(.pills) .feature__visual { order: 1; }
  .feature--flip:has(.pills) .pills { order: 2; }
}

.feature--stack {
  grid-template-columns: 1fr;
  text-align: center;
}

.feature--stack .feature__copy { max-width: 46em; margin: 0 auto; }
.feature--stack .sub { margin: 0 auto; }

/* ----- SCOPE showcase (centerpiece + instrument tray) ----- */

.scope { position: relative; }

/* Desktop zoom-out dive (Apple iPhone-hero pattern, reverse of the hero
   zoom-in above): copy+device start pinned; scopedive.js scrubs the device
   from a full-bleed cover scale down to 1 as the user scrolls, then fades
   the copy in. Gated on height as well as width — the copy+device stack is
   taller than the hero's, and a 1366×768-class laptop viewport can't fit it
   even with .scope__controls (tray + captions) already moved to normal flow
   below the pin (see index.html). */
@media (min-width: 900px) and (min-height: 760px) {
  .scope {
    padding: 0;
    content-visibility: visible;
  }

  /* The scroll runway owns the height (not .scope) so the trailing
     .scope__controls flows AFTER the full sticky travel — otherwise it
     climbs up into the pinned viewport and paints over the copy/device. */
  .scope__runway {
    height: 200vh;
    position: relative;
  }

  .scope__pin {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    /* Tighter than the shared --section-pad (meant for normal, un-pinned
       sections) — this frame is already dense with copy + an 880px device,
       so a smaller reserved margin keeps the settled view tight instead of
       airy. */
    padding: clamp(24px, 3vh, 48px) 0;
  }

  /* margin-top: auto absorbs all the slack on tall viewports (where 100svh
     is much taller than the copy+device stack), so the block still reads as
     roughly centered; margin-bottom is capped instead of also auto, so the
     gap before .scope__controls stays a normal section-sized breath instead
     of growing open-ended with viewport height. Also: an explicit width
     stops this flex item from collapsing to fit-content — .wrap's own
     margin:0 auto (base.css) gives it 'auto' cross-axis margins, which
     opts a flex item OUT of the default stretch sizing, so without this it
     shrinks to the copy's 46em cap and takes the 880px device down with it. */
  .scope__pin > .wrap {
    width: 100%;
    margin-top: auto;
    margin-bottom: clamp(16px, 3svh, 56px);
  }

  .scope__visual .device { will-change: transform; }

  /* Copy paints above the device so the headline can overlay the full-bleed
     waterfall at the start of the dive (the device is a later DOM sibling and
     would otherwise cover it). Only the headline is visible that early; the
     rest of the copy is faded out until the phone has settled. */
  .scope .feature__copy { position: relative; z-index: 2; }

  /* Legibility for the headline while it sits over the busy waterfall —
     imperceptible once it settles over the black background. */
  .scope .headline { text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55), 0 0 34px rgba(0, 0, 0, 0.5); }
}

@media (prefers-reduced-motion: reduce) {
  .scope { height: auto; padding: var(--section-pad) 0; content-visibility: auto; }
  .scope__runway { height: auto; }
  .scope__pin { position: static; height: auto; overflow: visible; padding: 0; }
}

.no-motion .scope { height: auto; padding: var(--section-pad) 0; content-visibility: auto; }
.no-motion .scope__runway { height: auto; }
.no-motion .scope__pin { position: static; height: auto; overflow: visible; padding: 0; }

/* Hero-scale centerpiece: wider than the standard two-column device */
.scope .device--landscape { width: min(880px, 100%); }

.scope .feature__visual { margin-top: clamp(24px, 3.5vw, 40px); }

/* Console status line — the scroll-initiated toggle moment. The state
   bracket lives in ::after content so the OFF→ON flip is pure CSS, driven
   by the reveal observer adding .is-in. */
.scope__status {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--rs-meta);
  margin: 28px 0 0;
}

.scope__state { margin-left: 10px; }

.scope__state::after {
  content: "[ OFF ]";
  color: var(--rs-dim);
}

.scope__status.is-in .scope__state::after {
  content: "[ ON ]";
  color: var(--rs-orange);
  text-shadow: var(--rs-bloom-orange);
  animation: scope-flick 0.45s steps(1) 0.3s backwards;
}

/* Relay-closing flicker: two brief dropouts, then steady */
@keyframes scope-flick {
  0% { opacity: 0; }
  18% { opacity: 1; }
  34% { opacity: 0.2; }
  52% { opacity: 1; }
  68% { opacity: 0.35; }
  100% { opacity: 1; }
}

/* Tray + crossfading caption below the device. min-width: 0 keeps the
   grid item from sizing to the tray's content width on narrow screens
   (same treatment as .feature__copy/.feature__visual), so the tray's
   max-width + internal overflow-x can take over. */
.scope__controls {
  margin-top: clamp(32px, 4vw, 44px);
  min-width: 0;
}

/* Phones: the four-button trays (DECODE chapters, SCOPE views) can't fit one
   row, so lay them out 2x2 instead of a clipped horizontal scroll strip.
   Scoped to these two sections; the 3-button capture-stack tray stays a single
   row (.tray--capture can't be the hook — capture-stack shares it). The
   active-segment progress underline and .is-on styling are per-segment, so they
   carry over unchanged. 519px is the site's established tray phone breakpoint. */
@media (max-width: 519px) {
  .decode .tray,
  .scope .tray {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;              /* same inner gap the flex tray uses */
    width: 100%;
    max-width: 380px;     /* keep 2-up proportions tidy */
    margin-inline: auto;
    overflow: visible;    /* override overflow-x: auto — no scroll needed */
  }
}

.scope__captions {
  position: relative;
  min-height: 4.2em;
  max-width: 40em;
  margin: 22px auto 0;
}

.scope__caption {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  color: var(--rs-logbook-meta);
  font-size: 15px;
  line-height: 1.6;
  transition: opacity 0.4s ease;
}

.scope__caption.is-on { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .scope__status.is-in .scope__state::after { animation: none; }
  .scope__caption { transition: none; }
}

.no-motion .scope__status.is-in .scope__state::after { animation: none; }
.no-motion .scope__caption { transition: none; }

/* ----- DECODE — CW/RTTY decoder chapter loop -----
   Layout rides the standard feature--stack column; this block adds the
   chapter controls, the caption crossfade, and the morse bed: two faint
   rails behind the section whose dash pattern keys out CQ in actual
   morse (-.-. --.-), drifting slowly in opposite directions. */

.decode {
  position: relative;
  overflow: hidden;
}

.decode__controls {
  margin-top: clamp(28px, 4vw, 40px);
  min-width: 0;
}

.decode__captions {
  position: relative;
  max-width: 40em;
  margin: 22px auto 0;
}

.decode__caption {
  margin: 0 0 12px;
  color: var(--rs-logbook-meta);
  font-size: 15px;
  line-height: 1.6;
}

/* Captions crossfade only once explorer.js marks the section .is-live and
   motion is allowed (same contract as the capture-stack steps): collapsed
   onto one cell, fading on .is-on. No JS / reduced motion: they simply
   stack, all readable. */
html:not(.no-motion) .decode.is-live .decode__captions { min-height: 4.2em; }

html:not(.no-motion) .decode.is-live .decode__caption {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  transition: opacity 0.4s ease;
}

html:not(.no-motion) .decode.is-live .decode__caption.is-on { opacity: 1; }

.decode__roadmap {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--rs-meta);
  text-align: center;
  margin: 28px 0 0;
}

/* Backdrop beds. Two strips feed horizontally THROUGH the device: each bed is
   full-bleed (100vw) and sits behind the phone (z-index 0 vs the device's 1),
   so each strip is hidden by the phone body and emerges in the side margins —
   reads as tape/rail threading through the reader. Anchored to .decode__stage
   (which wraps only the device) so the strips' vertical % lands on the phone
   regardless of the copy height above. Each strip is one tile wider than its
   box and loops a whole-tile translate for a seamless, mechanical feed.
   Edge-faded by a mask so it doesn't hard-cut at the viewport edges.

   Two beds are stacked and crossfade on the active chapter's mode (the
   section's data-mode, reflected by explorer.js): --baudot (baudot-tape.svg,
   ITA2-flavored punched tape) is the RTTY motif; --morse (decode-morse.svg,
   CQ keyed in dots/dashes) is the CW motif. */
.decode__stage {
  position: relative;
}

.decode__stage .device {
  position: relative;
  z-index: 1;
}

.decode__bed {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 14%, #000 86%, transparent);
  opacity: 0;
  transition: opacity 0.6s ease;
}

/* Mode crossfade: only the active mode's bed is shown. */
.decode[data-mode="rtty"] .decode__bed--baudot,
.decode[data-mode="cw"] .decode__bed--morse { opacity: 1; }

.decode__tape {
  position: absolute;
  left: 0;
  right: -192px;
  height: 34px;
  background-repeat: repeat-x;
  opacity: 0.85;
  animation: decode-feed 22s linear infinite;
}

.decode__tape--a { top: 24%; }
/* Phase-offset the lower strip so the two rows don't read as mirror copies. */
.decode__tape--b { top: 58%; }

/* RTTY bed: 192px baudot tile, whole-tile feed loop. */
.decode__bed--baudot .decode__tape {
  background-image: url("../assets/media/baudot-tape.svg");
  background-size: 192px 34px;
}
.decode__bed--baudot .decode__tape--b { background-position-x: 96px; }

/* CW bed: 272px morse (CQ) tile, its own whole-tile feed loop. */
.decode__bed--morse .decode__tape {
  right: -272px;
  background-image: url("../assets/media/decode-morse.svg");
  background-size: 272px 34px;
  animation-name: decode-feed-morse;
  animation-duration: 30s;
}
.decode__bed--morse .decode__tape--b { background-position-x: 136px; }

@keyframes decode-feed {
  to { transform: translateX(-192px); }
}

@keyframes decode-feed-morse {
  to { transform: translateX(-272px); }
}

@media (prefers-reduced-motion: reduce) {
  .decode__tape { animation: none; }
  .decode__caption { transition: none; }
  .decode__bed { transition: none; }
}

.no-motion .decode__tape { animation: none; }
.no-motion .decode__caption { transition: none; }
.no-motion .decode__bed { transition: none; }

/* ----- NETWORK — dotted world map with receiver pings -----
   A faint equirectangular dot-grid map (netmap.svg) sits behind the whole
   section; orange pings mark real KiwiSDR node locations. When the section
   scrolls in (reveal.js adds .is-in to .netmap) each ping's dot blooms on
   in a top-of-list-to-bottom stagger (--i), then keeps emitting a slow
   expanding ring forever — the network quietly "breathing". Transform +
   opacity only: no filters, no layout, nothing that costs raster time. */

.network { position: relative; }
.network .wrap { position: relative; z-index: 1; }

/* Extra bottom room so the ping-dense band of the map (Europe/US, its
   vertical middle) gets open black space below the copy/device columns
   instead of colliding with the text. */
@media (min-width: 900px) {
  .network { padding-bottom: calc(var(--section-pad) + 170px); }
}

.netmap {
  position: absolute;
  /* Centered with auto margins, NOT left:50% + translateX(-50%): the reveal
     system's [data-reveal].is-in { transform: none } (0,2,0) would clobber
     any transform set here (0,1,0) the moment the map reveals. */
  left: 0;
  right: 0;
  margin-inline: auto;
  bottom: calc(var(--section-pad) * 0.35);
  width: min(1200px, 94vw);
  aspect-ratio: 1000 / 372;
  z-index: 0;
  pointer-events: none;
}

.netmap__dots {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Fade the grid off toward the right, where the copy column sits, and at
     the far edges — full strength behind the device, tapering (not vanishing)
     under the copy so the map stays readable as a map. Static mask on a
     static element: applied once, no per-frame cost. */
  -webkit-mask-image: radial-gradient(120% 120% at 34% 50%,
      #000 40%, rgba(0, 0, 0, 0.68) 72%, rgba(0, 0, 0, 0.38) 100%);
  mask-image: radial-gradient(120% 120% at 34% 50%,
      #000 40%, rgba(0, 0, 0, 0.68) 72%, rgba(0, 0, 0, 0.38) 100%);
}

.netmap__ping {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
}

/* The node itself: an orange dot that blooms on during the intro and keeps a
   persistent two-layer bloom so it reads as a lit beacon. */
.netmap__ping::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--rs-orange);
  box-shadow:
    0 0 6px rgba(255, 107, 26, 0.9),
    0 0 14px rgba(255, 107, 26, 0.5);
  opacity: 0;
  transform: scale(0.4);
}

.netmap.is-in .netmap__ping::before {
  animation: netmap-dot 0.5s var(--ease-out-quint) forwards;
  /* North→south curtain: 38 nodes bloom over ~1.7s (0.045s/node) */
  animation-delay: calc(0.35s + var(--i) * 0.045s);
}

/* The repeating sonar ring each node emits once it is on the air */
.netmap__ping::after {
  content: "";
  position: absolute;
  inset: -13px;
  border-radius: 50%;
  border: 2px solid var(--rs-orange);
  opacity: 0;
  transform: scale(0.3);
}

/* De-synced sonar rings across all 38 nodes: each ring's period varies
   (--dj → 5.4–6.6s) so they drift and never resonate, and its phase is
   spread by a golden-ratio offset (--ph) that starts after the intro curtain
   (~2.6s). With the low duty cycle below only ~8–10 rings pulse at once —
   an "observatory" calm rather than a marching wave. */
.netmap.is-in .netmap__ping::after {
  animation: netmap-ring calc(5.4s + var(--dj) * 1.2s) linear infinite;
  animation-delay: calc(2.6s + var(--ph) * 6s);
}

@keyframes netmap-dot {
  from { opacity: 0; transform: scale(0.4); }
  60%  { opacity: 1; transform: scale(1.5); }
  to   { opacity: 1; transform: scale(1); }
}

/* Ring expands and fades over the first ~22% of its cycle, then rests the
   rest — short, sparse pulses so a dense field stays calm. */
@keyframes netmap-ring {
  0%   { opacity: 0;    transform: scale(0.3); }
  4%   { opacity: 0.75; }
  22%  { opacity: 0;    transform: scale(1.5); }
  100% { opacity: 0;    transform: scale(1.5); }
}

/* Below the two-column breakpoint the map would sit behind stacked copy —
   more noise than signal. Desktop-only, like the dives. */
@media (max-width: 899px) {
  .netmap { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .netmap.is-in .netmap__ping::before { animation: none; opacity: 1; transform: scale(1); }
  .netmap.is-in .netmap__ping::after { animation: none; }
}

.no-motion .netmap.is-in .netmap__ping::before { animation: none; opacity: 1; transform: scale(1); }
.no-motion .netmap.is-in .netmap__ping::after { animation: none; }

/* ----- FAVORITES — choreographed entrance + dial scale -----
   Copy: masked line-rise headline (share-style stretch) and a delayed spot
   sub, overlapping a weighty ease-out-expo media settle. Below the device a
   faint HF dial scale (.favdial, markup in index.html) draws in: baseline
   sweeps left→right, ticks curtain on, then the six saved frequencies bloom
   as orange markers and settle into de-synced carrier pulses (the netmap's
   sibling — real data, transform + opacity only).
   The :not(.is-in) scope on the media start pose is load-bearing — an
   unscoped transform here (0,2,0, later file) would beat
   [data-reveal].is-in { transform: none } and freeze the start pose. */

.favorites { position: relative; }
.favorites .wrap { position: relative; z-index: 1; }

/* Open black space below the device for the dial strip (netmap pattern). */
@media (min-width: 900px) {
  .favorites { padding-bottom: calc(var(--section-pad) + 120px); }
}

/* Headline: share-style stretched masked line rise. */
.favorites .headline[data-split="lines"] .li {
  transition-duration: 1.3s, 1.3s;
  transition-delay: calc(0.15s + var(--i, 0) * 150ms);
}

.favorites [data-reveal="media"]:not(.is-in) {
  transform: translateY(56px) scale(0.94);
}

.favorites [data-reveal="media"] {
  transition-duration: 1.5s, 1.5s;
  transition-delay: 0.25s, 0.25s;
  transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); /* ease-out-expo */
}

@media (prefers-reduced-motion: reduce) {
  .favorites [data-reveal="media"] { transform: none; transition: none; }
}

.no-motion .favorites [data-reveal="media"] { transform: none; transition: none; }

/* Dial strip: centered with auto margins, NOT translateX (see .netmap). */
.favdial {
  position: absolute;
  left: 0;
  right: 0;
  margin-inline: auto;
  bottom: calc(var(--section-pad) * 0.4 - 20px);
  width: min(1200px, 94vw);
  height: 140px; /* baseline stays put (bottom lowered 20px); strip grows upward for the dogleg leaders */
  z-index: 0;
  pointer-events: none;
  font-family: var(--font-mono);
  /* Tight edge dissolve so the strip reads as a slice of a longer dial.
     Stops at 3%/97% (not wider): the 15.7 MHz marker cluster sits at ~96%
     and must stay at full brightness. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 3%, #000 97%, transparent);
}

/* Baseline draws left→right on reveal. */
.favdial__line {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  background: rgba(255, 255, 255, 0.28);
  transform: scaleX(0);
  transform-origin: left center;
}

.favdial.is-in .favdial__line {
  transform: scaleX(1);
  transition: transform 1.2s cubic-bezier(0.16, 1, 0.3, 1) 0.1s;
}

/* Ticks bloom behind the drawn line in a left→right curtain (--i). */
.favdial__tick {
  position: absolute;
  left: var(--x);
  top: 50%;
  width: 2px;
  height: 8px;
  margin: -4px 0 0 -1px;
  background: var(--rs-dim);
  opacity: 0;
  transform: translateY(6px);
}

.favdial__tick--major { height: 14px; margin-top: -7px; }

.favdial.is-in .favdial__tick {
  opacity: 0.55;
  transform: none;
  transition:
    opacity 0.7s var(--ease-out-quint),
    transform 0.7s var(--ease-out-quint);
  transition-delay: calc(0.3s + var(--i) * 22ms);
}

/* MHz labels under the major ticks. translate(-50%) centering is safe here:
   only .favdial itself carries [data-reveal]; children never get .is-in. */
.favdial__label {
  position: absolute;
  left: var(--x);
  top: calc(50% + 12px);
  transform: translate(-50%, 6px);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--rs-dim);
  opacity: 0;
}

.favdial.is-in .favdial__label {
  opacity: 0.8;
  transform: translate(-50%, 0);
  transition:
    opacity 0.9s var(--ease-out-quint),
    transform 0.9s var(--ease-out-quint);
  transition-delay: calc(0.34s + var(--i) * 22ms);
}

/* Saved-frequency markers: netmap-dot bloom, left→right by --i. Full
   brightness like the netmap pings — the white cursor+readout already
   marks which one is live, so the dots themselves don't need to be dimmed. */
.favdial__mark {
  position: absolute;
  left: var(--x);
  top: 50%;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
}

.favdial__mark::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--rs-orange);
  box-shadow:
    0 0 6px rgba(255, 107, 26, 0.9),
    0 0 14px rgba(255, 107, 26, 0.5);
  opacity: 0;
  transform: scale(0.4);
}

.favdial.is-in .favdial__mark::before {
  animation: favdial-dot 0.5s var(--ease-out-quint) forwards;
  animation-delay: calc(1s + var(--i) * 0.09s);
}

/* Ambient carrier pulse once lit — de-synced periods (--dj) and
   golden-ratio phases (--ph), same observatory calm as the netmap. Ring
   geometry and peak opacity now match .netmap__ping::after exactly. */
.favdial__mark::after {
  content: "";
  position: absolute;
  inset: -13px;
  border-radius: 50%;
  border: 2px solid var(--rs-orange);
  opacity: 0;
  transform: scale(0.3);
}

.favdial.is-in .favdial__mark::after {
  animation: favdial-ring calc(5.6s + var(--dj) * 1.2s) linear infinite;
  animation-delay: calc(2.4s + var(--ph) * 6s);
}

@keyframes favdial-dot {
  from { opacity: 0; transform: scale(0.4); }
  60%  { opacity: 1; transform: scale(1.6); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes favdial-ring {
  0%   { opacity: 0;    transform: scale(0.3); }
  4%   { opacity: 0.75; }
  22%  { opacity: 0;    transform: scale(1.5); }
  100% { opacity: 0;    transform: scale(1.5); }
}

/* ----- Upgrade: bands (substrate) + station callouts + live cursor/readout ----- */

/* Broadcast-band shading — faint neutral regions behind the ticks; fades in
   first (0.2s) as the instrument substrate. */
.favdial__band {
  position: absolute;
  left: var(--x1);
  top: 50%;
  width: var(--w);
  height: 30px;
  margin-top: -15px;
  background: rgba(255, 255, 255, 0.08);
  border-inline: 2px solid rgba(255, 255, 255, 0.2);
  opacity: 0;
}
.favdial__band::after {           /* faint band name, behind the ticks */
  content: attr(data-band);
  position: absolute;
  left: 3px;
  top: 1px;
  font-size: 8px;
  letter-spacing: 0.1em;
  color: var(--rs-dim);
}
.favdial__band--edge::after { left: auto; right: 3px; } /* 31m label off the masked left edge */

.favdial.is-in .favdial__band {
  opacity: 1;
  transition: opacity 0.8s var(--ease-out-quint) 0.2s;
}

/* Station callouts — 45° dogleg leaders. Full-strip-width container so every %
   (dot --x, label --lx/--lxr, horizontal-leg width) resolves against the strip,
   locking leg endpoints to the same %s as the dots at any width. Only .favdial
   itself carries [data-reveal]/.is-in, so transforms on these children are safe. */
.favdial__call {
  --rise: 30px;
  --dir: 1;                         /* +1 up-right (default) */
  position: absolute;
  left: 0;
  right: 0;
  top: calc(50% - 64px);
  height: 64px;
  font-size: 11px;
  line-height: 1.25;
  letter-spacing: 0.06em;
  white-space: nowrap;
}
.favdial__call::before {            /* diagonal leg: 1px bar, up 45° from the dot */
  content: "";
  position: absolute;
  left: var(--x);
  bottom: 0;
  width: 1px;
  height: calc(var(--rise) * 1.41421356);
  margin-left: -0.5px;
  background: rgba(255, 107, 26, 0.6);
  transform-origin: bottom center;
  transform: rotate(calc(var(--dir) * 45deg)) scaleY(0);
}
.favdial__call::after {             /* horizontal leg: elbow → label near edge */
  content: "";
  position: absolute;
  bottom: var(--rise);
  height: 1px;
  left: calc(var(--x) + var(--rise));
  width: calc(var(--lx) - var(--x) - var(--rise));
  background: rgba(255, 107, 26, 0.6);
  transform-origin: left center;
  transform: scaleX(0);
}
.favdial__call b,
.favdial__call i { position: absolute; left: var(--lx); opacity: 0; }
.favdial__call b { bottom: calc(var(--rise) + 1.3em + 2px); color: var(--rs-tick); font-weight: 400; }
.favdial__call i { bottom: calc(var(--rise) + 2px);          color: var(--rs-meta); font-style: normal; }

/* left-bending variant (15.7 MHz cluster): mirror the diagonal, right-anchor label */
.favdial__call--left { --dir: -1; }
.favdial__call--left::after {
  left: var(--lxr);
  width: calc(var(--x) - var(--rise) - var(--lxr));
  transform-origin: right center;
}
.favdial__call--left b,
.favdial__call--left i { left: auto; right: calc(100% - var(--lxr)); text-align: right; }

/* draw-on: diagonal → horizontal → label, keyed to marker --i */
.favdial.is-in .favdial__call::before {
  transform: rotate(calc(var(--dir) * 45deg)) scaleY(1);
  transition: transform 0.30s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(1.35s + var(--i) * 0.09s);
}
.favdial.is-in .favdial__call::after {
  transform: scaleX(1);
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(1.65s + var(--i) * 0.09s);
}
.favdial.is-in .favdial__call b,
.favdial.is-in .favdial__call i {
  opacity: 1;
  transition: opacity 0.25s linear;
  transition-delay: calc(1.90s + var(--i) * 0.09s);
}

/* Live tuning cursor — white needle + white bloom (matches the app's dial
   needle). Draws DOWN from the top as the climax at 1.9s. */
.favdial__cursor {
  position: absolute;
  left: var(--x);
  top: 50%;
  width: 1px;
  height: 44px;
  margin: -22px 0 0 -0.5px;
  background: var(--rs-white);
  box-shadow: var(--rs-bloom);
  opacity: 0;
  transform: scaleY(0);
  transform-origin: top center;
}
.favdial.is-in .favdial__cursor {
  opacity: 1;
  transform: scaleY(1);
  transition:
    transform 0.7s cubic-bezier(0.16, 1, 0.3, 1) 1.9s,
    opacity 0.3s linear 1.9s;
}

/* Live readout — right-anchored above the needle (Tier A), lands with it. */
.favdial__readout {
  position: absolute;
  left: calc(var(--x) - 4px);
  top: 2px;
  text-align: right;
  line-height: 1.3;
  letter-spacing: 0.04em;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-100%, -6px);
}
.favdial__readout b { display: block; font-size: 12px; font-weight: 400; color: var(--rs-white); }
.favdial__readout i { display: block; font-size: 10px; font-style: normal; color: var(--rs-meta); }
.favdial.is-in .favdial__readout {
  opacity: 1;
  transform: translate(-100%, 0);
  transition:
    opacity 0.3s linear 2.0s,
    transform 0.6s var(--ease-out-quint) 2.0s;
}

/* Desktop-only, like the netmap. */
@media (max-width: 899px) {
  .favdial { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .favdial.is-in .favdial__line,
  .favdial.is-in .favdial__tick,
  .favdial.is-in .favdial__label { transition: none; }
  .favdial.is-in .favdial__mark::before { animation: none; opacity: 1; transform: scale(1); }
  .favdial.is-in .favdial__mark::after { animation: none; }
  .favdial.is-in .favdial__band    { transition: none; opacity: 1; }
  .favdial.is-in .favdial__call::before { transition: none; transform: rotate(calc(var(--dir) * 45deg)) scaleY(1); }
  .favdial.is-in .favdial__call::after  { transition: none; transform: scaleX(1); }
  .favdial.is-in .favdial__call b,
  .favdial.is-in .favdial__call i        { transition: none; opacity: 1; }
  .favdial.is-in .favdial__cursor  { transition: none; opacity: 1; transform: scaleY(1); }
  .favdial.is-in .favdial__readout { transition: none; opacity: 1; transform: translate(-100%, 0); }
}

.no-motion .favdial.is-in .favdial__line,
.no-motion .favdial.is-in .favdial__tick,
.no-motion .favdial.is-in .favdial__label { transition: none; }
.no-motion .favdial.is-in .favdial__mark::before { animation: none; opacity: 1; transform: scale(1); }
.no-motion .favdial.is-in .favdial__mark::after { animation: none; }
.no-motion .favdial.is-in .favdial__band    { transition: none; opacity: 1; }
.no-motion .favdial.is-in .favdial__call::before { transition: none; transform: rotate(calc(var(--dir) * 45deg)) scaleY(1); }
.no-motion .favdial.is-in .favdial__call::after  { transition: none; transform: scaleX(1); }
.no-motion .favdial.is-in .favdial__call b,
.no-motion .favdial.is-in .favdial__call i        { transition: none; opacity: 1; }
.no-motion .favdial.is-in .favdial__cursor  { transition: none; opacity: 1; transform: scaleY(1); }
.no-motion .favdial.is-in .favdial__readout { transition: none; opacity: 1; transform: translate(-100%, 0); }

/* ----- SHARE — slower headline entrance -----
   Same masked line-rise as the stock [data-split="lines"] (base.css), just
   eased out longer and with more air between lines. */
.share .headline[data-split="lines"] .li {
  transition-duration: 1.3s, 1.3s;
  transition-delay: calc(0.18s + var(--i, 0) * 150ms);
}

/* ----- LOGBOOK — bespoke copy entrance -----
   The phone beside this copy plays back a recorded catch, so the copy gets
   the section's own signature: the kicker relay-flicks on, the headline
   rises as masked lines (data-split="lines", primitives in base.css), and a
   small waveform rule — drawn by main.js onto [data-lb-rule], echoing the
   playback bars on the phone — sweeps in beneath it. */

/* Kicker opts out of the stock reveal rise; the flash owns opacity.
   (Same relay character as the finale's STANDING BY; deliberately its own
   keyframe so this section doesn't depend on the finale block.) */
.logbook .kicker {
  transform: none;
  transition: none;
}

.logbook .kicker.is-in {
  animation: lb-flick 0.55s steps(1) both;
}

@keyframes lb-flick {
  0%   { opacity: 0;    text-shadow: none; }
  16%  { opacity: 1;    text-shadow: var(--rs-bloom-orange); }
  30%  { opacity: 0.12; text-shadow: none; }
  48%  { opacity: 1;    text-shadow: var(--rs-bloom-orange); }
  64%  { opacity: 0.35; text-shadow: none; }
  100% { opacity: 1;    text-shadow: none; }
}

/* The waveform rule canvas: blank until main.js draws it, so it needs no
   reveal state of its own — just reserved space. */
.lb-rule {
  display: block;
  width: 210px;
  height: 18px;
  margin: 2px 0 20px;
}

@media (prefers-reduced-motion: reduce) {
  .logbook .kicker.is-in { animation: none; opacity: 1; }
}

.no-motion .logbook .kicker.is-in { animation: none; opacity: 1; }

/* ----- Capture stage (pinned scroll-scrub) ----- */

.stage { height: 300vh; position: relative; }

.stage__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.stage__grid {
  display: grid;
  grid-template-columns: 4fr 7fr;
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
  width: 100%;
}

/* Chapter indicator rail — instrument markings, not carousel dots */
.stage__rail {
  position: absolute;
  right: clamp(16px, 3vw, 40px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 20px;
  z-index: 3;
}

.rail__step {
  background: none;
  border: none;
  padding: 6px 4px;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--rs-dim);
  transition: color 0.3s, text-shadow 0.3s;
}

.rail__step:hover { color: var(--rs-meta); }

.rail__step:focus-visible {
  outline: 2px solid var(--rs-amber);
  outline-offset: 2px;
}

.stage[data-phase="0"] .rail__step:nth-child(1),
.stage[data-phase="1"] .rail__step:nth-child(2),
.stage[data-phase="2"] .rail__step:nth-child(3) {
  color: var(--rs-orange);
  text-shadow: var(--rs-bloom-orange);
}

.capsteps { position: relative; min-height: 240px; }

.capstep {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.5s ease, transform 0.5s var(--ease-out-quint);
}

.capstep .capstep__label {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--rs-orange);
}

.capstep h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(22px, 2.6vw, 30px);
  margin: 12px 0 12px;
  letter-spacing: -0.01em;
}

.capstep p { color: var(--rs-logbook-meta); margin: 0; max-width: 30em; }

.stage[data-phase="0"] .capstep[data-step="0"],
.stage[data-phase="1"] .capstep[data-step="1"],
.stage[data-phase="2"] .capstep[data-step="2"] {
  opacity: 1;
  transform: none;
}

/* Interactive chapter fallback (narrow screens / reduced motion / no JS).
   Default layout is fully stacked — that IS the no-JS state. Once
   explorer.js marks the section .is-live (and motion is allowed), the
   capsteps collapse into a crossfading overlay driven by .is-on, and the
   tray + auto-advance take over the chapter progression. */
.capture-stack { display: none; }

.capture-stack .device { margin-top: 16px; }

.capture-stack .tray {
  display: flex;
  margin-top: 20px;
}

.capture-stack .tray__seg { flex: 1; justify-content: center; }

.capture-stack__steps {
  margin-top: 28px;
  position: relative;
}

.capture-stack .capstep {
  position: static;
  opacity: 1;
  transform: none;
  margin-bottom: 40px;
}

html:not(.no-motion) .capture-stack.is-live .capture-stack__steps {
  min-height: 12em; /* fits the tallest step (SAVE) at 375px */
}

html:not(.no-motion) .capture-stack.is-live .capstep {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}

html:not(.no-motion) .capture-stack.is-live .capstep.is-on {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .capture-stack.is-live .capstep { transition: none; }
}

@media (max-width: 899px), (prefers-reduced-motion: reduce) {
  .stage { display: none; }
  .capture-stack { display: block; }
}

.no-motion .stage { display: none; }
.no-motion .capture-stack { display: block; }

/* ----- Privacy section ----- */

.privacy { max-width: 760px; margin: 0 auto; }

/* ----- Final CTA ----- */

.finale {
  text-align: center;
  padding-bottom: 40px;
}

.finale__wf {
  width: min(720px, 92vw);
  height: 120px;
  margin: 0 auto 8px;
}

.finale__wf canvas { width: 100%; height: 100%; }

.finale__headline {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(36px, 6vw, 72px);
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin: 24px 0;
}

.finale__small {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--rs-meta);
  margin-top: 28px;
}

/* STANDING BY powers on like a relay closing — a sharp flicker to full with a
   brief orange bloom, in deliberate contrast to the calm fades around it.
   Same character as the scope section's "[ ON ]" (scope-flick). */
.finale .kicker {
  transform: none;      /* cancel the default reveal rise */
  transition: none;     /* the flash owns opacity, not a transition */
}

.finale .kicker.is-in {
  animation: standby-flash 0.55s steps(1) both;
  animation-delay: var(--d, 0ms);
}

@keyframes standby-flash {
  0%   { opacity: 0;    text-shadow: none; }
  16%  { opacity: 1;    text-shadow: var(--rs-bloom-orange); }
  30%  { opacity: 0.12; text-shadow: none; }
  48%  { opacity: 1;    text-shadow: var(--rs-bloom-orange); }
  64%  { opacity: 0.35; text-shadow: none; }
  100% { opacity: 1;    text-shadow: none; }
}

/* Soften the surrounding finale fades so the flash is the only sharp motion. */
.finale [data-reveal="fade"] { transition-duration: 1.1s; }

@media (prefers-reduced-motion: reduce) {
  .finale .kicker.is-in { animation: none; opacity: 1; }
}

/* ----- Footer ----- */

.footer {
  border-top: 1px solid var(--rs-header-divider);
  padding: 48px 0 40px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--rs-meta);
}

.footer__links {
  display: flex;
  gap: 28px;
  flex-wrap: wrap;
  justify-content: center;
  margin-bottom: 24px;
}

.footer__links a {
  color: var(--rs-logbook-meta);
  text-decoration: none;
  transition: color 0.25s;
}

.footer__links a:hover { color: var(--rs-amber); }

.footer__line {
  text-align: center;
  font-size: 11px;
  line-height: 2;
  color: var(--rs-dim);
}

.footer__eot {
  text-align: center;
  margin-top: 26px;
  font-size: 10px;
  letter-spacing: 0.2em;
  color: #2e2e2e;
}

/* Reuses .prompt-line's terminal styling (components.css) — just centers it
   and tightens the top margin to sit in the footer's rhythm instead of a
   privacy-card's. */
.footer__prompt {
  text-align: center;
  margin-top: 20px;
}
