/* ============================================================
   ACT I — THE DESCENT : the map, the drop, eight waypoints
   Extends style.css. 2.5D only — transform/opacity, no WebGL.
   ============================================================ */

body.act-one { background: #0B0E0D; }

/* ---------- MOBILE VIEWPORT LOCK (Item 1: fixed bar / inner-scroll) ----------
   Stops the DOCUMENT ROOT from scrolling so iOS keeps the toolbar shown, then
   moves scrolling into a locked-height inner container (#scroller). Activated
   only when act-one.js resolves a real #scroller on a mobile device (it adds
   .locked-scroll to <html>), so desktop is untouched.

   The class is gated on the ROOT element (html.locked-scroll), NOT body: a
   `.locked-scroll body` descendant combinator matches nothing here.

   LOAD-BEARING: this changes WHO scrolls, not HOW TALL the stages are. Do not
   add height units here — the 100lvh stages and the lvh probe stay as-is, so
   the film progress denominator (rect.height − viewportH()) is unchanged and
   the descent→cache junction still lands byte-aligned.

   GUARD (do not violate): no transform / filter / will-change:transform /
   perspective / contain on any ancestor of #nav, .rail-left, .route-marker, or
   #scroller — any one reparents the position:fixed chrome to the locked box and
   it drifts. (backdrop-filter on .nav itself is fine.) */
html.locked-scroll,
html.locked-scroll body {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
html.locked-scroll #scroller {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: none;
}

/* ---------- ROUTE MARKER (persistent corner mini-map) ---------- */

.route-marker {
  position: fixed;
  right: clamp(0.9rem, 2vw, 1.6rem);
  /* hug the frame edge on ultra-wide; collapses to the clamp at ≤2560 (B3/B5) */
  right: calc(var(--frame-gutter) + clamp(0.9rem, 2vw, 1.6rem));
  bottom: clamp(0.9rem, 2vh, 1.6rem);
  z-index: 45;
  display: flex;
  flex-direction: column;
  /* right-align every item so each item's right edge holds a constant distance
     from the screen edge as values change (no left-edge shimmer) */
  align-items: flex-end;
  /* pinned width (≥ the longest waypoint label) so the map centres stably and the
     chips' right edge never shifts as the location text changes (hud-rail-map Fix 4) */
  width: 10.75rem;
  gap: 0.4rem;
  pointer-events: none;
}
/* topographic mini-map: full-width (aligns with the chip boxes below), a
   translucent night-ops plate with contour lines + a pulsing "you are here" dot */
.route-mini {
  width: 100%;
  height: auto;
  aspect-ratio: 344 / 200;
  display: block;
  border: 1px solid rgba(231, 226, 211, 0.14);
  border-radius: 4px;
  opacity: 0.94;            /* a tiny bit see-through — the film reads faintly behind */
}
.topo-bg { fill: rgba(9, 11, 10, 0.9); }
.topo-line { fill: none; stroke: var(--bone); stroke-width: 0.85; }
.route-line-mini { fill: none; stroke: var(--amber); stroke-opacity: 0.45; stroke-width: 1.4; }
.route-pin {
  fill: rgba(231, 226, 211, 0.30);
  transition: fill 0.3s ease;
}
.route-pin.is-lit { fill: #E8A33D; }
/* pulsing "you are here" marker on the current waypoint (positioned by updateHUD) */
.route-here-dot { fill: #F4C070; r: 4.8; }
.route-here-ring {
  fill: none; stroke: #F4C070;
  animation: here-pulse 1.5s ease-out infinite;
}
@keyframes here-pulse {
  0%   { r: 5;  opacity: 1;   stroke-width: 3.2; }
  70%  { r: 24; opacity: 0;   stroke-width: 1;   }
  100% { r: 24; opacity: 0;   stroke-width: 1;   }
}
.route-count {
  width: 100%;
  text-align: right;
  font-size: 0.6rem;
  letter-spacing: 0.14em;
  color: var(--bone-dim);
  background: rgba(11, 14, 13, 0.6);
  padding: 0.2rem 0.5rem;
  border: 1px solid rgba(231, 226, 211, 0.14);
}
.route-count [data-wpt-now] { color: var(--amber); }
.route-alt {
  width: 100%;
  text-align: right;
  font-size: 0.58rem;
  letter-spacing: 0.18em;
  color: var(--bone-dim);
  background: rgba(11, 14, 13, 0.6);
  padding: 0.15rem 0.5rem;
  border: 1px solid rgba(231, 226, 211, 0.14);
  border-top: 0;
}
.route-alt [data-alt-now] { color: var(--bone); }
.route-loc {
  width: 100%;
  text-align: right;
  font-size: 0.56rem;
  letter-spacing: 0.22em;
  color: var(--amber);
  background: rgba(11, 14, 13, 0.6);
  padding: 0.15rem 0.5rem;
  border: 1px solid rgba(231, 226, 211, 0.14);
  border-top: 0;
}

/* ---------- LEFT RAIL (expedition ruler) ---------- */

.rail-left {
  position: fixed;
  right: clamp(0.9rem, 2vw, 1.6rem);
  /* hug the frame edge on ultra-wide; collapses to the clamp at ≤2560 (B3/B5) */
  right: calc(var(--frame-gutter) + clamp(0.9rem, 2vw, 1.6rem));
  top: 44%;
  transform: translateY(-50%);
  z-index: 45;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.8rem;
  pointer-events: none;
}
.rail-count {
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  color: var(--bone-dim);
}
.rail-count [data-sec-now] { color: var(--amber); }
.rail-ticks {
  display: flex;
  flex-direction: column;
  gap: 13px;
  align-items: flex-end;
}
.rail-tick {
  width: 10px;
  height: 1px;
  background: rgba(231, 226, 211, 0.26);
  transition: width 0.35s ease, background-color 0.35s ease;
}
.rail-tick.is-major { width: 16px; background: rgba(231, 226, 211, 0.42); }
.rail-tick.is-active { width: 22px; background: var(--amber); }
/* short-viewport guard: the taller ruler + the mini-map share the right column;
   tighten the gap so they can't overlap in a half-height window (hud-rail-map Fix 1) */
@media (max-height: 620px) { .rail-ticks { gap: 8px; } }
@media (max-width: 900px) {
  .rail-left { display: none; }
}

/* ---------- HERO: THE MAP + THE DROP ---------- */

.act-hero {
  position: relative;
  height: 240vh;
}
.act-hero-sticky {
  position: sticky;
  top: 0;
  /* visual layer = largest viewport (lvh, static): the canvas always covers the
     reclaimed strip when the toolbar retracts, so there is no bottom gap. When
     the toolbar is shown, the bottom sits harmlessly beneath it (igloo-class
     overdraw). Must stay in unit-lockstep with L353/397/400/428. */
  height: 100lvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.drop-stage {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.drop-map {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform-origin: 12% 89%;
  will-change: transform, opacity;
}
.drop-map picture,
.drop-map img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.drop-map::after {
  /* readability vignette over the plate, under the HUD */
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 90% 70% at 50% 46%, rgba(11, 14, 13, 0) 40%, rgba(11, 14, 13, 0.5) 100%),
    linear-gradient(rgba(11, 14, 13, 0.22), rgba(11, 14, 13, 0.22));
  pointer-events: none;
}
.drop-map-ui {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.route-line {
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: draw-route 2.2s ease-out 0.3s forwards;
}
@keyframes draw-route {
  to { stroke-dashoffset: 0; }
}
.beacon {
  fill: #E8A33D;
  opacity: 0;
  animation: beacon-on 0.5s ease-out forwards;
}
.beacon[data-beacon="1"] { animation-delay: 2.0s; }
.beacon[data-beacon="2"] { animation-delay: 2.15s; }
.beacon[data-beacon="3"] { animation-delay: 2.3s; }
.beacon[data-beacon="4"] { animation-delay: 2.45s; }
.beacon[data-beacon="5"] { animation-delay: 2.6s; }
.beacon[data-beacon="6"] { animation-delay: 2.75s; }
.beacon[data-beacon="7"] { animation-delay: 2.9s; }
.beacon[data-beacon="8"] { animation-delay: 3.05s; }
@keyframes beacon-on {
  0% { opacity: 0; r: 2; }
  60% { opacity: 1; r: 9; }
  100% { opacity: 0.85; r: 6; }
}
.map-label { opacity: 0; animation: label-in 0.6s ease-out 3.3s forwards; }

.drop-clouds {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.cloud-layer {
  position: absolute;
  left: -10%;
  width: 120%;
  height: 60%;
  opacity: 0;
  will-change: transform, opacity;
  background: radial-gradient(ellipse 60% 100% at 50% 50%, rgba(231, 226, 211, 0.16), transparent 70%);
  filter: blur(6px);
}
.cloud-1 { top: 10%; }
.cloud-2 { top: 40%; }
.cloud-3 { top: 65%; }

.drop-breakthrough {
  position: absolute;
  inset: 0;
  z-index: 2;
  opacity: 0;
  will-change: opacity;
}
.drop-breakthrough picture,
.drop-breakthrough img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.hero-copy {
  position: relative;
  z-index: 4;
  text-align: center;
  padding: 0 var(--pad-x);
  will-change: opacity, transform;
}
.drop-map img,
.drop-breakthrough img,
.wpt-photo img,
.hero-photo img {
  -webkit-user-drag: none;
  user-select: none;
}
.drop-embers {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ---------- THE DESCENT FILM (scroll-scrubbed) ---------- */

.drop-canvas,
.drop-rest {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: none;
  z-index: 2;
}
/* copy-seam-jump: +30lvh SEAM SLACK on every film stage. The successor film's
   widened negative margin swallows it, so every pin point, every fade beat and
   the page length stay exactly where they were. The JS maps film progress over
   the PRE-SLACK runway (SEAM_SLACK_FRAC in act-one.js — must stay in lockstep
   with this 30lvh), so the scrub rate is untouched; the film simply holds its
   junction frame through the slack band, hidden under the next stage. The band
   exists so the stage handover (is-pinned flip) has margin on BOTH sides of the
   seam: the outgoing stage is still stuck for 0.3 viewport after the incoming
   stage has appeared. NO rest margins, NO dead zones, NO scrub-rate change —
   those were the v5 mistakes that degraded the feel. */
.act-hero.film { height: calc(300vh + 30lvh); }
.act-hero.film .drop-canvas { display: block; }
.act-hero.film .drop-rest.show { display: block; }
.act-hero.film .drop-map,
.act-hero.film .drop-clouds,
.act-hero.film .drop-breakthrough { display: none; }

.drop-scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  will-change: opacity;
  background:
    radial-gradient(ellipse 74% 58% at 50% 46%, rgba(5, 7, 6, 0.5) 0%, rgba(5, 7, 6, 0.22) 55%, rgba(5, 7, 6, 0.55) 100%),
    linear-gradient(rgba(5, 7, 6, 0.22), rgba(5, 7, 6, 0.34));
}

/* ---------- COPY LEGIBILITY VEIL ----------
   A directional scrim anchored to where copy lives (left column, top-left
   eyebrow/name, bottom-left territory) that fades to clear over the center-
   right so the subject/fire/moon stay bright. Its opacity tracks copy
   presence (driven in JS) so the cinematic mid-dive is never dimmed. */
.copy-veil {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  will-change: opacity;
  background:
    /* AREA ONE — copy block, upper-left; fades before the fire (lower-center) and moon (upper-right) */
    radial-gradient(ellipse 60% 46% at 11% 18%,
      rgba(4, 6, 5, 0.88) 0%,
      rgba(4, 6, 5, 0.4) 50%,
      rgba(4, 6, 5, 0) 74%),
    /* AREA TWO — territory line, bottom-LEFT radial; darker for legibility, fades before
       centre to protect the fire (hud-rail-map Fix 5a — strengthened per feedback) */
    radial-gradient(ellipse 66% 50% at 7% 104%,
      rgba(4, 6, 5, 0.94) 0%,
      rgba(4, 6, 5, 0.58) 44%,
      rgba(4, 6, 5, 0) 72%),
    /* AREA THREE — thin right-edge strip behind the ruler + mini-map; soft inward fade so
       there's no visible edge, and it rides the same copy-tracked opacity as One/Two */
    linear-gradient(to left,
      rgba(4, 6, 5, 0.62) 0%,
      rgba(4, 6, 5, 0.34) 4%,
      rgba(4, 6, 5, 0) 13%);
}
.wpt-film .copy-veil { z-index: 1; }
body.act-one .act-hero h1 {
  text-shadow: 0 2px 26px rgba(0, 0, 0, 0.7), 0 1px 5px rgba(0, 0, 0, 0.55);
}
.hero-copy .eyebrow,
.hero-copy .subline,
.hero-copy .drop-subline2 {
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.75), 0 1px 3px rgba(0, 0, 0, 0.5);
}
.hero-copy-end {
  position: absolute;
  z-index: 4;
  left: var(--pad-x);
  /* The one keeper from the July 28 seam attempts. Absolutely positioned with only
     `left`, this block sat flush against the right edge while the scene's own in-flow
     .wpt-content respects the sticky's padding-right — measured 355px vs 335px at
     375px, so the SAME text wrapped differently either side of a seam, and Isaiah
     reported the copy touching the screen edge on his phone. Mobile-only in effect
     (desktop already hits the 40rem cap) and it touches nothing about scroll feel. */
  right: var(--pad-x);
  top: clamp(5.2rem, 11vh, 7.5rem);
  max-width: 40rem;
  opacity: 0;
  transform: translateY(18px);
  will-change: opacity, transform;
  pointer-events: none;
}
/* .end-name shares the .wpt-name type rule below — one rule, both blocks, so the
   film-to-film copy handoff swaps between pixel-identical headings */
.hero-copy-end .wpt-body p,
.hero-copy-end .wpt-territory {
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.75), 0 1px 3px rgba(0, 0, 0, 0.5);
}
body.act-one .eyebrow { margin-bottom: clamp(1.2rem, 3vh, 2rem); }
body.act-one .act-hero h1 {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(3rem, 12vw, 7.5rem);
  line-height: 0.98;
  letter-spacing: 0.015em;
}
.drop-subline2 {
  margin-top: clamp(1.2rem, 3vh, 2rem);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--bone-dim);
  max-width: 30rem;
  margin-inline: auto;
}
.act-hero .hero-foot {
  z-index: 3;
  /* The stage is lvh; anchor the scroll cue to the SMALL viewport so it never
     hides under the toolbar when it's shown. (100lvh - 100svh) = 0 when the
     toolbar is retracted, so it settles to its natural 2rem then. */
  bottom: calc(2rem + (100lvh - 100svh));
}

/* ---------- WAYPOINT SCENES ---------- */

.wpt-scene {
  position: relative;
  min-height: 100lvh; /* lvh visual layer — see .act-hero-sticky */
  display: flex;
  align-items: flex-start;
  padding: clamp(5.2rem, 11vh, 7.5rem) var(--pad-x) 5rem;
  overflow: hidden;
}
.wpt-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
}
.wpt-bg svg {
  width: 100%;
  height: 100%;
  display: block;
}
.wpt-photo,
.wpt-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--crop, 50% 50%);
  display: block;
}
.wpt-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom, rgba(11, 14, 13, 0.8) 0%, rgba(11, 14, 13, 0.34) 45%, rgba(11, 14, 13, 0.08) 78%, rgba(11, 14, 13, 0.22) 100%),
    linear-gradient(100deg, rgba(11, 14, 13, 0.58) 0%, rgba(11, 14, 13, 0.2) 40%, rgba(11, 14, 13, 0) 62%);
}
/* plain scenes only (NOT the Cache film): darker bottom so TERRITORY reads clean.
   Neutral — no left bias (WPT-07 vein + WPT-08 fire sit bottom-left). Scoped
   :not(.wpt-film) so the film's held --bg-scrim never darkens the Cache dive
   (hud-rail-map Fix 5b — the red-team landmine). */
.wpt-scene:not(.wpt-film) .wpt-bg::after {
  background:
    linear-gradient(to bottom, rgba(11, 14, 13, 0.8) 0%, rgba(11, 14, 13, 0.34) 45%, rgba(11, 14, 13, 0.2) 78%, rgba(11, 14, 13, 0.46) 100%),
    linear-gradient(100deg, rgba(11, 14, 13, 0.58) 0%, rgba(11, 14, 13, 0.2) 40%, rgba(11, 14, 13, 0) 62%);
}

/* ---------- FILM SCENES (scene-to-scene scroll-scrub) ---------- */

.wpt-film {
  display: block;
  padding: 0;
  /* .wpt-scene's overflow:hidden would break the sticky stage — the sticky clips itself */
  overflow: visible;
}
.wpt-film.film {
  /* +30lvh seam slack, matching .act-hero.film — see the comment there */
  min-height: calc(300vh + 30lvh);
  /* start one viewport early: the stage pins (hidden) under the hero's final frame,
     so the identical basecamp frame takes over with no visible scroll-away.
     -100lvh matches the hero stage height (now lvh) so the pre-pin handoff lands
     byte-aligned — a -100svh offset here would flash the junction seam. The extra
     -30lvh swallows the previous stage's seam slack, so every pin point sits at
     exactly the same scroll position as before the slack existed. */
  margin-top: calc(-100lvh - 30lvh);
}
.wpt-film.film .wpt-film-sticky { visibility: hidden; }
.wpt-film.film.is-pinned .wpt-film-sticky { visibility: visible; }

/* carried-over basecamp copy: only exists in film mode, driven by the scrub */
.film-handoff-copy,
.film-handoff-territory { display: none; }
.wpt-film.film .film-handoff-copy,
.wpt-film.film .film-handoff-territory { display: block; }

/* scrim crossfade over the handoff: the hero's end scrim carries in at 0.45
   and trades places with this scene's own gradient, so luminance never jumps */
.film-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: none;
  will-change: opacity;
  background:
    radial-gradient(ellipse 74% 58% at 50% 46%, rgba(5, 7, 6, 0.5) 0%, rgba(5, 7, 6, 0.22) 55%, rgba(5, 7, 6, 0.55) 100%),
    linear-gradient(rgba(5, 7, 6, 0.22), rgba(5, 7, 6, 0.34));
}
.wpt-film.film .film-scrim { display: block; }
/* Film scenes carry only a light base wash so the cinematic frame pops — copy
   legibility is owned by the copy-veil (Areas One/Two/Three), not a full-scene
   scrim. The --bg-scrim ramp is preserved (keeps the settle from flashing). */
.wpt-film.film .wpt-bg::after { opacity: calc(var(--bg-scrim, 1) * 0.2); }
.wpt-film-sticky {
  position: sticky;
  top: 0;
  min-height: 100lvh; /* lvh visual layer — see .act-hero-sticky */
  display: flex;
  align-items: flex-start;
  padding: clamp(5.2rem, 11vh, 7.5rem) var(--pad-x) 5rem;
  overflow: hidden;
}
.film-canvas,
.film-rest {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: none;
}
.wpt-film.film .film-canvas { display: block; }
.wpt-film.film .film-rest.show { display: block; }
/* junction still sits above the canvas but below the scrims, so the
   dissolve never changes scene brightness */
.wpt-film .film-rest-start { will-change: opacity; }
.wpt-film.film .wpt-photo { display: none; }
.wpt-film.film .wpt-content,
.wpt-film.film .wpt-territory {
  opacity: 0;
  will-change: opacity, transform;
}

.scene-transition {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
}
.transition-cloud.play {
  animation: wipe-cloud 1s ease-out forwards;
}
@keyframes wipe-cloud {
  0% { opacity: 0.9; background: radial-gradient(ellipse 140% 60% at 50% 50%, rgba(231, 226, 211, 0.5), transparent 70%); }
  100% { opacity: 0; }
}
.transition-ripple.play {
  animation: wipe-ripple 1.1s ease-out forwards;
}
@keyframes wipe-ripple {
  0% { opacity: 0.85; background: radial-gradient(circle at 50% 70%, rgba(201, 210, 214, 0.4) 0%, transparent 55%); }
  100% { opacity: 0; }
}
.transition-flicker.play {
  animation: wipe-flicker 0.7s steps(5) forwards;
}
@keyframes wipe-flicker {
  0% { opacity: 0.7; background: #0B0E0D; }
  20% { opacity: 0.2; }
  40% { opacity: 0.6; }
  60% { opacity: 0.1; }
  100% { opacity: 0; }
}

.wpt-content {
  position: relative;
  z-index: 2;
  max-width: 40rem;
}
.wpt-eyebrow {
  font-size: 0.68rem;
  letter-spacing: 0.24em;
  color: var(--amber);
  margin-bottom: 1rem;
}
/* ONE heading rule for both the scene's own copy (.wpt-name) and the carried-over
   handoff duplicate (.end-name): same type AND same shadow, so the block swap at a
   film-to-film seam re-renders identical glyphs. A future media query tweaking one
   without the other can't desynchronise them (copy-seam-jump Cause 1 + mobile). */
.wpt-name,
.end-name {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(2.2rem, 6.5vw, 4rem);
  line-height: 1.02;
  letter-spacing: 0.015em;
  text-transform: uppercase;
  margin-bottom: 1.6rem;
  text-shadow: 0 2px 20px rgba(0, 0, 0, 0.65);
}
.wpt-body p {
  font-size: 1.02rem;
  line-height: 1.75;
  color: #DAD6C8;
  /* matches .hero-copy-end .wpt-body p exactly — the handoff swap must not add or drop a shadow */
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.75), 0 1px 3px rgba(0, 0, 0, 0.5);
}
.wpt-body p + p { margin-top: 1.1em; }
.wpt-body em { color: var(--bone); font-style: italic; }
.wpt-territory {
  position: absolute;
  left: var(--pad-x);
  bottom: clamp(2.4rem, 6vh, 4rem);
  z-index: 2;
  padding-top: 1.2rem;
  border-top: 1px solid rgba(231, 226, 211, 0.22);
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  color: var(--bone);
  max-width: 34rem;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65), 0 2px 16px rgba(0, 0, 0, 0.85);
}
/* anchor the territory readout to the small viewport (stages are lvh) — pre-lvh
   browsers drop this and keep the clamp above */
.wpt-territory {
  bottom: calc(clamp(2.4rem, 6vh, 4rem) + (100lvh - 100svh));
}
.hero-territory-end {
  z-index: 4;
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
}

/* ---------- MOBILE ---------- */

@media (max-width: 640px) {
  .route-marker { display: none; }
  .wpt-scene { align-items: flex-start; padding-bottom: 3rem; }
  .wpt-name,
  .end-name { font-size: clamp(1.9rem, 9vw, 2.8rem); }
  /* portrait crops: bias each plate toward its light source so the subject survives the 9:16 window */
  #wpt-01 .wpt-photo img { object-position: 50% 62%; }
  #wpt-02 .wpt-photo img { object-position: 46% 55%; }
  #wpt-03 .wpt-photo img { object-position: 50% 55%; }
  #wpt-04 .wpt-photo img { object-position: 52% 50%; }
  #wpt-05 .wpt-photo img { object-position: 50% 42%; }
  #wpt-06 .wpt-photo img { object-position: 50% 55%; }
  #wpt-07 .wpt-photo img { object-position: 48% 55%; }
  #wpt-08 .wpt-photo img { object-position: 50% 58%; }
  .drop-map picture img { object-position: 38% 60%; }
  .wpt-bg::after {
    background:
      linear-gradient(to bottom, rgba(11, 14, 13, 0.88) 0%, rgba(11, 14, 13, 0.45) 52%, rgba(11, 14, 13, 0.18) 100%);
  }
}

/* ---------- REDUCED MOTION ---------- */

@media (prefers-reduced-motion: reduce) {
  .act-hero { height: auto; }
  .act-hero-sticky { position: relative; height: 100svh; }
  .drop-clouds { display: none; }
  .route-line { stroke-dashoffset: 0; animation: none; }
  .beacon { opacity: 0.85; animation: none; }
  .map-label { opacity: 1; animation: none; }
  .drop-map { transform: none !important; opacity: 1 !important; }
  .drop-breakthrough { opacity: 0 !important; }
  .scene-transition { display: none; }
  .drop-embers { display: none; }
  .hero-copy { opacity: 1 !important; transform: none !important; }
  .hero-copy-end { position: relative; inset: auto; margin: 2rem var(--pad-x); opacity: 1 !important; transform: none !important; }
}

/* ---------- cold open (placeholder: hold + hairline + dissolve) ----------
   Spec: plans/load-strategy.md §4 "THE COLD OPEN, SPECIFIED".
   The ~3s reveal film replaces the dissolve later; this keeps the beat. */
.cold-open {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #0B0E0D;                    /* same field as body.act-one */
  opacity: 1;
  transition: opacity 700ms ease;
}
.cold-open.done {
  opacity: 0;
  pointer-events: none;
}

/* FAILSAFE: if cold-open.js never runs (404, blocked, parse error) nothing would
   ever remove this overlay and the page would sit black forever. This frees it
   with no JS at all. The script always wins in practice -- it removes the element
   ~1s in -- so this only ever fires when the script is missing. */
.cold-open {
  animation: co-failsafe 800ms ease 12s forwards;
}
@keyframes co-failsafe {
  to { opacity: 0; visibility: hidden; pointer-events: none; }
}
/* cold-open.js adds .co-alive the moment it runs. The ENTER gate makes the hold
   deliberately unbounded, so the 12s failsafe would fade the gate away and
   strand the film; the script owning its own lifecycle is what makes that safe.
   The no-JS path never gets this class, so the failsafe still covers it. */
.cold-open.co-alive { animation: none; }
.cold-open.co-skippable { cursor: pointer; }

/* ---------- THE ENTER GATE ----------
   Appears only once the films are genuinely resident, so it can never be used
   to cut a load short. Its click is also the browser's permission for sound:
   without it the transition sting cannot legally play. */
.cold-open .co-enter {
  position: absolute;
  left: 50%;
  bottom: clamp(4.5rem, 14vh, 8rem);
  transform: translateX(-50%);
  z-index: 6;
  padding: 0.85em 2.6em;
  background: transparent;
  border: 1px solid rgba(232, 163, 61, 0.55);
  border-radius: 2px;
  color: var(--amber);
  font-size: 0.66rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  animation: co-enter-in 700ms ease 120ms forwards;
  transition: color 260ms ease, border-color 260ms ease;
  /* 🔴 The browser's own focus ring drew a SECOND rectangle around our border.
     cold-open.js focuses this button programmatically the moment it appears
     (it is the only way forward, so keyboard readers must land on it), which
     means every visitor saw the focused look on load, not just keyboard ones.
     Isaiah, Jul 31: "this screenshot has like an extra outline or something."
     The ring is removed here and replaced by the border brightening below, so
     focus stays visible without ever drawing a second edge. */
  outline: none;
}
/* The lit state lives on ::after rather than on the button, so the pulse can
   fade the whole look (fill + brighter border + glow) with ONE animated
   property. inset:0 lands its border EXACTLY on the button's own hairline so
   the two stack into one brighter edge -- at -1px they sit side by side and
   read as the double edge this whole change exists to remove. z-index:-1 keeps
   the fill behind the word. */
.cold-open .co-enter::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border: 1px solid var(--amber);
  border-radius: 2px;
  background: rgba(232, 163, 61, 0.12);
  /* Diffuse on purpose. A tight, high-alpha shadow reads as another ring,
     which is the exact thing being fixed above. */
  box-shadow: 0 0 16px rgba(232, 163, 61, 0.20);
  opacity: 0;
  pointer-events: none;
  /* ~~1730ms, keyframed off Isaiah's screen recording where he worked the
     effect by hand with the mouse: three cycles at 1.7 / 1.8 / 1.7s, lit 32%
     of each, rest longer than the bloom.~~ STRUCK, Jul 31, by the man who made
     the recording: "you took literally the exact pulse pattern of my own manual
     doing it. I want a soft predictable pulsing glow, not based on my manual
     pattern that looks really inconsistent."
     The lesson is that the recording was evidence of WHAT he wanted, not a
     waveform to reproduce. His hand could only make a square wave -- move,
     stop, hold, move back -- and faithfully copying it copied the limits of a
     mouse. What reads as soft is the opposite: perfectly symmetric, no hold at
     either end, no dead time between swells. One stop up, one stop down.
     Slower than his hand (2.4s vs 1.73s) because an even swell reads faster
     than a stop-start one at the same period. This duration is the only number
     to touch if he wants it calmer or more urgent. */
  /* easeInOutSine, not `ease-in-out`. The CSS default is flat at both ends and
     measured 14% of the cycle pinned within 2% of full, which is a hold by
     another name -- the exact fault being fixed. This curve is the closest CSS
     gets to a true sine, so the glow turns at the top instead of resting there. */
  animation: co-enter-pulse 2400ms cubic-bezier(0.37, 0, 0.63, 1) 900ms infinite;
}
/* A real hover answers immediately and holds, rather than fighting the loop. */
.cold-open .co-enter:hover::after {
  animation: none;
  opacity: 1;
}
.cold-open .co-enter:hover,
.cold-open .co-enter:focus-visible {
  color: var(--amber-hi, #F4C070);
}
/* Focus brightens the ONE border the button already has and lets the pulse keep
   running. Pinning it lit here is what made the load-screen state look stuck. */
.cold-open .co-enter:focus-visible {
  border-color: var(--amber);
}
@keyframes co-enter-in {
  from { opacity: 0; transform: translateX(-50%) translateY(8px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Two stops, nothing else. Any extra stop reintroduces a hold, and a hold is
   what the eye reads as an irregular pulse rather than a breath. */
@keyframes co-enter-pulse {
  0%, 100% { opacity: 0; }
  50%      { opacity: 1; }
}
/* the hairline has done its job once the gate is up */
.cold-open.co-ready .co-rule { opacity: 0.25; transition: opacity 400ms ease; }
.cold-open.co-filming .co-enter,
.cold-open.done .co-enter { display: none; }

/* the background plate: Isaiah's tactical map display. AVIF first, WebP fallback,
   the near-black field underneath so there is never a white flash while it lands. */
.cold-open .co-bg {
  position: absolute;
  inset: 0;
  display: block;
}
.cold-open .co-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 600ms ease;
}
.cold-open .co-bg img.co-in { opacity: 1; }

/* The transition film. Sits over the plate, under the hairline. Only ever shown
   once it is FULLY buffered -- a stuttering 4s is worse than no film at all. */
.cold-open .co-film {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity 260ms ease;
  pointer-events: none;
}
.cold-open .co-film.co-playing { opacity: 1; }

/* The handoff shade: Isaiah's own design for the seam (2026-07-30). Measured off
   his screen recording: the hero settles under a ~0.5 scrim stack (brightness
   ratio 0.504, uniform across a 3x3 grid), so the dissolve alone carried a 50%
   brightness step and read as a jump. This ramps the SAME darkening in over the
   film's tail, so both sides of the dissolve match and the dissolve only has to
   hide the pixel offset. 0.5 is the measured target; the CSS var exists so
   Isaiah can tune it by eye.
   Tuned off his second recording (2026-07-30, 5.24PM): the ink is #050706, the
   hero scrim's own color, NOT #000 — under a pure-black shade the film's cool
   cast survived (U 131.2 vs the hero's 130.3) and read as a colour step. And the
   ramp starts earlier (2.2s out, 2s long) per Isaiah: sooner and gentler. */
.cold-open .co-shade {
  position: absolute;
  inset: 0;
  background: #050706;
  opacity: 0;
  pointer-events: none;
  transition: opacity 2000ms linear;
}
.cold-open .co-shade.co-shade-in { opacity: var(--co-shade-max, 0.5); }

/* ---------- screen-framing the cold open (Isaiah, 2026-07-31) ----------
   The bug he hit on a widescreen monitor: the loading plate and the transition
   film ran edge to edge, then the site itself appeared capped at --frame-max,
   so the first thing the page did was jump narrower.

   Everything cinematic on this page lives inside <main>, which style.css:89
   caps at --frame-max and centres. The cold open is the one exception: it is
   position:fixed outside <main> (it has to be, it covers the page), so the cap
   never reached it. Fix: give its visual layers the SAME frame, using the
   --frame-gutter variable style.css:66 already derives for this purpose.

   The overlay itself stays full-bleed on purpose — its #0B0E0D field is what
   fills the margins, exactly as the page background does either side of <main>,
   so the gutters match before and after the handoff.

   ONE WIDTH BASIS, and mixing two is what caused the handoff shift Isaiah
   filmed on 2026-07-31. The first version of this rule set `left` from
   --frame-gutter (derived from 100vw, which INCLUDES the classic scrollbar) and
   `width` from 100% of .cold-open (a fixed element, whose box EXCLUDES it).
   Those agree only while the scroll lock is suppressing the scrollbar. The
   moment the lock releases and the bar returns, .cold-open narrows by the
   scrollbar width, `left` does not move, the right edge pulls in, and
   object-fit:cover re-fits the picture — measured off his recording as a
   translate PLUS a horizontal scale anchored at the left edge (dx -1 / -2 / -3
   across left / centre / right thirds, dy 0, brightness flat).

   So: centre inside .cold-open's own box and clamp the width, using nothing but
   that box. left/right:0 + margin-inline:auto is the standard centring for an
   absolutely positioned element, and it is the same shape as <main>'s
   `max-width + margin-inline:auto` (style.css:89), so the two stay locked
   together. Clamping at --frame-max is what kills the shift outright: above the
   cap the width is a constant, so a scrollbar appearing changes the centring by
   the same amount it changes <main>'s, and nothing moves relative to anything.

   `width: auto` would be the obvious way to do this and it is WRONG, which cost
   an earlier measured round: .co-film is a <video>, a REPLACED element, and a
   replaced element with left+right set and width:auto falls back to its
   INTRINSIC size — the 300x150 default, since the video has no src until
   cold-open.js assigns one. It measured 300px wide on every viewport. */
.cold-open .co-bg,
.cold-open .co-film,
.cold-open .co-shade {
  left: 0;
  right: 0;
  margin-inline: auto;
  width: min(100%, var(--frame-max));
}

/* The frame's bone edge-lines (style.css:786-790) sit at z 44 and this overlay
   is z 9999, so during the cold open they were hidden and then appeared at the
   handoff — the same inconsistency, one layer down. Draw them on the overlay so
   the frame reads identically from the first paint. Non-binding at ≤2560, where
   the gutter is 0 and both lines land at -1px, off-screen. */
.cold-open::before,
.cold-open::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--bone-faint);
  pointer-events: none;
}
.cold-open::before { left:  calc((100% - min(100%, var(--frame-max))) / 2 + var(--frame-line-inset)); }
.cold-open::after  { right: calc((100% - min(100%, var(--frame-max))) / 2 + var(--frame-line-inset)); }

/* the hairline is meaningless once the film is rolling: the load it reports is
   over, and it floated over the film in Isaiah's recording. Fade it the moment
   playback starts. */
.cold-open.co-filming .co-rule { opacity: 0; transition: opacity 240ms ease; }

/* The dissolve OFF the film is load-bearing, not decorative: the render lands
   NEAR descent's f_001 but never on it (measured 7.58 against a gate of 6), so
   cutting would pop. 700ms per plans/load-transition-film.md §2; lengthened to
   1000ms 2026-07-30 (the specced next lever) after Isaiah still saw the step. */
.cold-open.co-handoff { transition: opacity 1000ms ease; }
.cold-open.co-handoff .co-rule { opacity: 0; transition: opacity 240ms ease; }

/* The loading indicator: bottom centre, over the plate. No number, no words --
   a bar that fills carries the same information at a tenth of the anxiety, and
   invented copy is banned. Three layers, so it reads as ALIVE even when the
   network stalls and the fill is not moving:
     .co-rule   the track, barely there
     > i        the fill, driven by real buffered progress in cold-open.js
     ::after    a slow sweep that never stops, so a stalled fill still breathes
   160px wide because 40px vanished against the map plate. */
.cold-open .co-rule {
  position: absolute;
  left: 50%;
  bottom: 8.5vh;
  transform: translateX(-50%);
  width: 160px;
  height: 1px;
  background: rgba(233, 228, 216, 0.16);
  overflow: hidden;
}
.cold-open .co-rule > i {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg,
    rgba(233, 228, 216, 0.55) 0%,
    rgba(233, 228, 216, 0.92) 82%,
    rgba(224, 138, 47, 0.95) 100%);          /* torch amber on the leading edge */
  transform: scaleX(0);
  transform-origin: left center;
  will-change: transform;
  transition: transform 220ms linear;
  box-shadow: 0 0 6px rgba(233, 228, 216, 0.35);
}
.cold-open .co-rule::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent 0%, rgba(233, 228, 216, 0.5) 50%, transparent 100%);
  transform: translateX(-100%);
  animation: co-sweep 1900ms ease-in-out infinite;
}
@keyframes co-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
/* Motion is the only thing here that is decorative. Under reduced motion the
   whole overlay is already display:none, so no extra guard is needed. */

/* scroll lock while the field is held, both roots */
html.cold-open-hold,
html.cold-open-hold body { overflow: hidden !important; touch-action: none; }
html.cold-open-hold #scroller { overflow: hidden !important; touch-action: none; }

/* no JS, no gate: the <noscript> block in act-one/index.html hides it outright */
@media (prefers-reduced-motion: reduce) {
  .cold-open { display: none; }
}

/* ============================================================================
   THE MONO REGISTER  —  plans/decode-and-scrub-audio.md §0.6, §0.11, §10 H3
   ============================================================================
   The decode needs a MONOSPACE face and this is why: the line is full length
   from frame one, so in a proportional face every churning glyph is a different
   width, the line reflows on every frame and the whole block jitters. That is
   the difference between a decode and a mess.

   IBM Plex Mono is already self-hosted and already preloaded for the mono HUD
   register, so the whole body moving onto it costs ZERO new font bytes.

   Mono runs wider per character than Inter (~0.60em advance against ~0.50em),
   so the size comes down and the leading opens to keep the same colour on the
   page and the same measure inside the existing 40rem cap. Re-check at 375px:
   `.hero-copy-end` and `.wpt-content` wrap differently either side of a seam if
   this drifts, which is a bug Isaiah has already caught once on his phone. */

/* ONE rule, both blocks. `.hero-copy-end .wpt-body p` adds only a text-shadow,
   so the film-to-film copy handoff keeps swapping between pixel-identical
   paragraphs. Changing the face in two places is how that gets broken. */
.wpt-body p {
  font-family: var(--mono);
  font-size: 0.92rem;
  line-height: 1.85;
  letter-spacing: -0.005em;
}
@media (max-width: 480px) {
  /* 375px is the reference width. Mono's wider advance pushes the longest line
     past the sticky's padding here before it reaches the 40rem cap, so the
     phone gets its own step down rather than a wrap that differs across a seam. */
  .wpt-body p { font-size: 0.86rem; line-height: 1.8; }
}

/* §0.11: the hero `.subline` joins the mono register. It was the only line in
   `.hero-copy` without the `mono` class, which read as an odd one out the
   moment everything around it moved. */
.hero-copy .subline {
  font-family: var(--mono);
  font-size: clamp(0.88rem, 2.2vw, 1.02rem);
  line-height: 1.8;
  letter-spacing: 0.01em;
}
