/**
 * =============================================================================
 * PSYCHO PATHS — THE OVERLAY
 * =============================================================================
 * Everything here floats over a live WebGL canvas, and that single fact drives
 * every decision below.
 *
 *   NOTHING IS OPAQUE THAT DOES NOT HAVE TO BE. A solid panel over the scene
 *   is a hole punched in the set. The panels are glass — a heavy backdrop blur
 *   with a low-alpha ink fill — so the graveyard stays legible *through* the
 *   reading surface and the page reads as one composited image rather than as
 *   cards stacked on a wallpaper.
 *
 *   CONTRAST IS EARNED LOCALLY, NEVER GLOBALLY. The camera moves, so the
 *   luminance behind any given paragraph changes as you scroll. Type is not
 *   trusted to sit on the raw scene: it sits on glass, or on a gradient scrim,
 *   or it is large enough that a text-shadow carries it. There is no state in
 *   which body copy is one thin serif against an unknown background.
 *
 *   HARD CORNERS THROUGHOUT. A rounded rectangle is a software affordance and
 *   this is a Victorian playbill. The only radius in the file is on the focus
 *   ring, which is a browser affordance and should look like one.
 *
 * PALETTE. The brief fixes three: #050505, #8B0F1D, #D9A441. Everything
 * between them is walked a few degrees warm — true greys under a gold key
 * light read as concrete, and a house lit by candles has no true greys in it.
 * =============================================================================
 */

:root {
  --void:        #050505;
  --charcoal:    #0C0B0A;
  --soot:        #151312;
  --iron:        #2B2724;
  --dust:        #7A736A;
  --parchment:   #F2EFE7;

  --crimson:     #8B0F1D;
  --crimson-hot: #C41425;

  /* Gold at ~39° against crimson's ~353° is ANALOGOUS — close enough on the
     wheel to read as one warm family, far enough to be an unmistakably
     different signal. Contrast on void is 9.1:1. */
  --ember:       #D9A441;
  --ember-dim:   #8A6A2C;

  --glass:       rgba(12, 11, 10, 0.58);
  --glass-line:  rgba(217, 164, 65, 0.16);

  --font-display: "Bodoni Moda", Didot, Georgia, serif;
  --font-body:    "EB Garamond", Garamond, Georgia, serif;

  --ease-io:  cubic-bezier(0.65, 0, 0.35, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  --gutter: clamp(1.25rem, 5vw, 5rem);
  --act-pad: clamp(6rem, 14vh, 11rem);
}

*, *::before, *::after { box-sizing: border-box; }

body {
  font-family: var(--font-body);
  font-size: clamp(1.0625rem, 1.15vw, 1.1875rem);
  line-height: 1.66;
  color: var(--parchment);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--crimson); color: var(--parchment); }

img { max-width: 100%; display: block; }

a { color: inherit; }

/* One visible focus treatment for the whole document. Never removed — this
   page is a form behind a 3D scene and keyboard users must be able to find
   the fields. */
:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip-link {
  position: fixed; top: -100px; left: 1rem; z-index: 200;
  background: var(--crimson); color: var(--parchment);
  padding: 0.75rem 1.25rem; text-decoration: none;
  font-family: var(--font-body); letter-spacing: 0.2em; text-transform: uppercase;
  font-size: 0.8125rem;
}
.skip-link:focus { top: 1rem; }

/* ==========================================================================
   TYPE SCALE
   Tracking is the thing to get right. A grotesque wants to be pulled tight; a
   Didone's whole character is the air around its hairlines, and negative
   tracking closes a display size into a smear.
   ========================================================================= */
.h1 {
  font-family: var(--font-display);
  font-weight: 900;
  /* 6vw put "The house needs a company." on four lines of 94px type, which is
     a headline that has stopped being a headline. The section headings here
     are sentences, not single words — they need a size that can carry a clause
     without eating the act. The wordmark is the only thing allowed to be huge. */
  font-size: clamp(2rem, 4.8vw, 4.25rem);
  line-height: 0.98;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  margin: 0 0 0.5em;
  text-wrap: balance;
}

/* THE WORDMARK.
   The drop-shadow lives on the PARENT and the mask on the CHILD, and that
   split is required rather than tidy. CSS applies filter BEFORE mask, so a
   drop-shadow on the masked element itself would be computed from the
   element's full rectangle and then clipped away by its own mask — a shadow of
   a box you cannot see. Applied one level up, the filter takes the child's
   already-masked output and shadows the actual letterforms. */
.wordmark {
  margin: 0.1em 0 0.5em;
  filter:
    drop-shadow(0 2px 3px rgba(0, 0, 0, 0.9))
    drop-shadow(0 6px 26px rgba(0, 0, 0, 0.7));
}

/* THE MARK'S REVEAL IS A CSS TRANSITION, NOT A TWEEN, and that is on purpose.
   Everything else on the page is revealed by GSAP, which is fine for a
   paragraph: if the tween never runs the worst case is some text that was
   already going to be there. The wordmark is the hero, and a per-frame tween
   holds its element at the FROM state until the ticker advances it — so a
   throttled tab, a stalled rAF, or a CDN that failed to serve GSAP leaves the
   masthead invisible with no way back.

   A class toggle plus a transition cannot fail that way. The end state is
   declarative: once .is-in is set the mark is visible whether the transition
   played over eighty frames, over three, or not at all. */
.wordmark-mark {
  display: block;
  /* SIZED AGAINST THE SET, NOT THE VIEWPORT. Run full-width the mark covers
     the gates exactly — the one thing the opening shot exists to show. The vh
     term keeps it honest on a short laptop window, where a pure-vw size is
     still enormous and pushes the CTAs off the bottom. */
  width: min(62vw, 46vh, 40rem);
  /* From the file's own viewBox, so the mark can never be stretched. */
  aspect-ratio: 250.4877 / 113.24448;
  background-color: var(--parchment);
  -webkit-mask: url("../media/logo.svg") no-repeat center / contain;
  mask: url("../media/logo.svg") no-repeat center / contain;

  opacity: 0;
  clip-path: inset(0 0 100% 0);
  transform: translateY(18px);
  transition:
    opacity 0.8s var(--ease-out),
    clip-path 1.3s var(--ease-out),
    transform 1.3s var(--ease-out);
}
.wordmark-mark.is-in {
  opacity: 1;
  clip-path: inset(0 0 0 0);
  transform: none;
}

/* Visually hidden, still announced. The <h1> carries real text under the
   painted mark so the page has a heading and a search result. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The engraved brass plate: small caps at wide tracking. This is the label
   voice of the whole site and replaces what would otherwise be a monospace. */
.plate {
  font-family: var(--font-body);
  font-size: clamp(0.75rem, 0.85vw, 0.8125rem);
  line-height: 1.4;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ember);
  margin: 0 0 1.25rem;
}
.plate.dim { color: var(--dust); }
.dim { color: var(--dust); }
.gold { color: var(--ember); }
.crimson { color: var(--crimson-hot); }

.standfirst {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.1vw, 1.9rem);
  line-height: 1.24;
  max-width: 26ch;
  margin: 0 0 0.75rem;
  text-shadow: 0 0.05em 0.35em rgba(0, 0, 0, 0.8);
}

.whisper {
  font-style: italic;
  color: var(--dust);
  margin: 0 0 2rem;
  text-shadow: 0 0.05em 0.3em rgba(0, 0, 0, 0.8);
}

.prose { max-width: 62ch; margin: 0 0 1.25rem; color: #cfc8bd; }
.prose strong { color: var(--parchment); font-weight: 600; }
.prose.small { font-size: 0.95em; color: var(--dust); }

/* ==========================================================================
   LAYOUT
   Each act is a full-height stage the camera has a waypoint for. The content
   sits inside it; the scene shows around it.
   ========================================================================= */
#content { position: relative; z-index: 2; }

.act {
  position: relative;
  min-height: 100svh;
  padding: var(--act-pad) var(--gutter);
  display: flex;
  align-items: center;
}
.act-inner { width: 100%; max-width: 1240px; margin: 0 auto; }
.act-narrow { max-width: 760px; }
.act-head { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* ARRIVAL. Bottom-aligned: the gates fill the upper frame and the type sits
   under them rather than over them. */
/* The hero is bottom-aligned so the type hangs under the gate arch. Its top
   padding is small because the section is already 100svh — the content block
   sets its own height and the padding only has to clear the fixed chapter
   mark. A full --act-pad here overflowed a 700px-tall window. */
/* The bottom padding is what lifts the whole block — address, wordmark,
   standfirst, buttons — up off the floor of the frame. The scroll cue is
   absolutely positioned and stays where it is, which is the point: the type
   rises and the cue keeps holding the bottom edge. */
#arrival { align-items: flex-end; padding-top: 5rem; padding-bottom: clamp(4.5rem, 13vh, 9.5rem); }
.arrival-inner { max-width: 1240px; }

/* A foot gradient so the CTAs hold contrast whatever the scene is doing
   behind them. Pinned to the section, not the viewport, so it travels off
   with the hero instead of shading the whole page. */
/* THE HERO SCRIM FADES OUT AT BOTH ENDS, and the bottom end is the point.
   It used to run to rgba(5,5,5,0.92) at 0% — full strength exactly at the
   section's bottom edge, with the next section starting at nothing. A gradient
   can be perfectly smooth within itself and still terminate in a hard step,
   and that step was a dark band lying across the whole page at the seam
   between the hero and the invitation.

   Fading to zero at both ends means the scrim has no edge anywhere: it swells
   behind the type and is gone before it reaches either boundary. The bottom
   few percent give up their darkening to #veil, which is fixed to the viewport
   and covers that band on every section rather than only this one.

   ITS PEAK IS DELIBERATELY LOW. This gradient is 78% of the section tall,
   which means the gates are behind it as much as the type is, and at 0.86 it
   was not protecting the wordmark so much as turning the one shot worth
   seeing into a silhouette of itself. The type carries its own text-shadow —
   that is what a scrim of this size should be *supplementing*, not replacing.
   0.60 at the peak leaves the ironwork readable and still lands the standfirst
   on something. */
#arrival::after {
  content: "";
  position: absolute; inset: auto 0 0 0; height: 78%;
  background: linear-gradient(to top,
    rgba(5, 5, 5, 0)    0%,
    rgba(5, 5, 5, 0.44) 10%,
    rgba(5, 5, 5, 0.60) 30%,
    rgba(5, 5, 5, 0.38) 58%,
    rgba(5, 5, 5, 0)    100%);
  pointer-events: none; z-index: -1;
}

.scroll-cue {
  position: absolute; left: var(--gutter); bottom: 2.5rem;
  display: flex; align-items: center; gap: 1rem;
  font-size: 0.75rem; letter-spacing: 0.26em; text-transform: uppercase;
  color: var(--dust);
}
.scroll-cue-line {
  display: block; width: 64px; height: 1px;
  background: linear-gradient(to right, var(--ember), transparent);
  transform-origin: left;
  animation: cue 2.6s var(--ease-io) infinite;
}
@keyframes cue { 0%, 100% { transform: scaleX(0.35); opacity: 0.5; } 50% { transform: scaleX(1); opacity: 1; } }

/* ==========================================================================
   GLASS
   backdrop-filter is the whole idea: the panel is a lens over the graveyard,
   not a card on top of it.
   ========================================================================= */
.panel {
  position: relative;
  padding: clamp(1.75rem, 4vw, 3.25rem);
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(22px) saturate(115%);
  -webkit-backdrop-filter: blur(22px) saturate(115%);
  /* A gold hairline inside the top edge reads as a bevel catching the
     lantern, which is what stops the glass looking like a flat overlay. */
  box-shadow:
    inset 0 1px 0 rgba(217, 164, 65, 0.14),
    0 30px 90px -30px rgba(0, 0, 0, 0.9);
}
.panel-wide { max-width: 900px; }

/* The tab that straddles the panel's top border, like a filing label. */
.panel-tab {
  position: absolute; top: -0.72rem; left: clamp(1.25rem, 3vw, 2rem);
  padding: 0 0.75rem;
  background: var(--void);
  font-size: 0.6875rem; letter-spacing: 0.26em; text-transform: uppercase;
  color: var(--dust);
  margin: 0;
}

/* No backdrop-filter (older Firefox, and anything with it disabled): the
   panel must not become a transparent smear over a moving scene, so it falls
   back to a near-solid ink fill. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .panel { background: rgba(8, 7, 6, 0.94); }
}

.pull {
  margin: 2rem 0;
  padding-left: clamp(1.25rem, 3vw, 2rem);
  border-left: 1px solid var(--crimson);
}
.pull p {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 1.9vw, 1.6rem);
  line-height: 1.34; font-style: italic; margin: 0 0 0.75rem;
}
.pull cite {
  font-style: normal; font-size: 0.75rem;
  letter-spacing: 0.26em; text-transform: uppercase; color: var(--dust);
}

/* Specification list — the brass-plate voice as a data table. */
.spec { margin: 2.5rem 0 0; padding: 0; display: grid; gap: 1px; background: var(--iron); border: 1px solid var(--iron); }
.spec > div { background: var(--charcoal); padding: 1.1rem 1.25rem; margin: 0; }
.spec dt { font-size: 0.6875rem; letter-spacing: 0.26em; text-transform: uppercase; color: var(--dust); margin-bottom: 0.4rem; }
.spec dd { margin: 0; font-family: var(--font-display); font-size: 1.0625rem; color: var(--parchment); }
@media (min-width: 720px) {
  .spec { grid-template-columns: repeat(3, 1fr); }
  .spec-4 { grid-template-columns: repeat(4, 1fr); }
}

.inclusive { border-left: 1px solid var(--ember-dim); padding-left: 1.25rem; margin: 2rem 0 3rem; }

/* ==========================================================================
   THE SIX DOORS
   ========================================================================= */
.doors { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; background: var(--iron); border: 1px solid var(--iron); }
@media (min-width: 700px)  { .doors { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .doors { grid-template-columns: repeat(3, 1fr); } }

.door {
  position: relative;
  background: var(--glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  padding: clamp(1.75rem, 3.5vw, 2.75rem);
  min-height: 190px;
  display: flex; flex-direction: column; justify-content: space-between; gap: 1.5rem;
  transition: background 0.5s var(--ease-out);
}
.door:hover { background: rgba(24, 12, 12, 0.66); }
.door-num {
  font-family: var(--font-display); font-size: 2.25rem; font-weight: 400;
  color: var(--crimson-hot); line-height: 1;
}
.door p { margin: 0; font-family: var(--font-display); font-size: 1.15rem; line-height: 1.3; }

/* ==========================================================================
   BUTTONS
   ========================================================================= */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 1.05rem 2.1rem;
  font-family: var(--font-body);
  font-size: 0.8125rem; letter-spacing: 0.26em; text-transform: uppercase;
  text-decoration: none; cursor: pointer;
  border: 1px solid transparent;
  transition: background 0.4s var(--ease-out), color 0.4s var(--ease-out),
              border-color 0.4s var(--ease-out);
}
.btn-primary { background: var(--crimson); color: var(--parchment); border-color: var(--crimson); }
.btn-primary:hover { background: var(--crimson-hot); border-color: var(--crimson-hot); }
.btn-ghost { background: transparent; color: var(--parchment); border-color: var(--iron); }
.btn-ghost:hover { border-color: var(--ember); color: var(--ember); }
.btn-block { width: 100%; margin-top: 1.5rem; }
.btn[disabled], .btn[aria-busy="true"] { opacity: 0.6; cursor: wait; }

.cta-row { display: flex; flex-wrap: wrap; gap: 1rem; }

.link-arrow {
  display: inline-block; margin-top: 0.75rem;
  font-size: 0.8125rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--ember); text-decoration: none;
  border-bottom: 1px solid var(--ember-dim); padding-bottom: 0.3rem;
  transition: border-color 0.35s var(--ease-out);
}
.link-arrow::after { content: " →"; }
.link-arrow:hover { border-color: var(--ember); }

/* ==========================================================================
   FORMS
   ========================================================================= */
form { display: flex; flex-direction: column; gap: 1.5rem; }

/* The honeypot. Off-screen rather than display:none — some bots skip hidden
   inputs, and the point of the trap is that they fill it in. */
.trap {
  position: absolute !important; left: -9999px !important;
  width: 1px; height: 1px; opacity: 0;
}

.field-row { display: flex; flex-direction: column; gap: 0.5rem; }
.field-grid { display: grid; gap: 1.5rem; }
@media (min-width: 640px) { .field-grid { grid-template-columns: 1fr 1fr; } }

label {
  font-size: 0.6875rem; letter-spacing: 0.26em; text-transform: uppercase;
  color: var(--dust);
}

.field {
  width: 100%;
  padding: 0.95rem 1rem;
  background: rgba(5, 5, 5, 0.55);
  border: 1px solid var(--iron);
  color: var(--parchment);
  font-family: var(--font-body); font-size: 1.0625rem;
  transition: border-color 0.3s var(--ease-out), background 0.3s var(--ease-out);
}
.field::placeholder { color: rgba(122, 115, 106, 0.6); }
.field:hover { border-color: #453e39; }
.field:focus { border-color: var(--ember); background: rgba(5, 5, 5, 0.8); outline: none; }
.field[aria-invalid="true"] { border-color: var(--crimson-hot); }

/* The select needs its own chevron: `appearance: none` is required to stop
   the OS drawing a light-mode control into a dark panel, and that removes the
   arrow along with it. */
.field-select { position: relative; }
.field-select select {
  appearance: none; -webkit-appearance: none;
  padding-right: 2.75rem; cursor: pointer;
}
.field-select::after {
  content: ""; position: absolute; right: 1.1rem; top: 50%;
  width: 8px; height: 8px; margin-top: -6px;
  border-right: 1px solid var(--dust); border-bottom: 1px solid var(--dust);
  transform: rotate(45deg); pointer-events: none;
}
/* The dropdown itself is an OS surface and inherits nothing from the panel. */
.field-select option { background: #0C0B0A; color: var(--parchment); }

.field-error, .form-error {
  margin: 0;
  font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--crimson-hot); line-height: 1.5;
}
.form-error a { color: var(--ember); text-underline-offset: 4px; }
[hidden] { display: none !important; }

/* Consent checkbox. The native box is kept in the accessibility tree and
   visually replaced, so the label still toggles it and focus still lands. */
.consent { display: flex; align-items: flex-start; gap: 1rem; cursor: pointer;
  font-size: 1rem; letter-spacing: 0; text-transform: none; color: #cfc8bd;
  line-height: 1.6; }
.consent input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.consent-box {
  flex: 0 0 auto; width: 18px; height: 18px; margin-top: 0.28rem;
  border: 1px solid var(--iron); background: rgba(5, 5, 5, 0.6);
  transition: background 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
}
.consent input:checked + .consent-box { background: var(--crimson); border-color: var(--crimson); }
.consent input:checked + .consent-box::after {
  content: "✓"; display: block; color: var(--parchment);
  font-size: 12px; line-height: 16px; text-align: center;
}
.consent input:focus-visible + .consent-box { outline: 2px solid var(--ember); outline-offset: 2px; }

/* ── Receipts ───────────────────────────────────────────────────────────── */
.receipt-head { display: flex; align-items: flex-start; gap: 1.25rem; margin-bottom: 2rem; }
.receipt-mark {
  flex: 0 0 auto; width: 48px; height: 48px;
  border: 1px solid var(--crimson); color: var(--crimson-hot);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-size: 1.5rem;
  transform: rotate(-8deg);
}
.receipt-head h3 {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  font-size: clamp(1.25rem, 1.9vw, 1.75rem); line-height: 1.1; margin: 0 0 0.5rem;
}
.receipt-head .plate { margin: 0; }

.receipt { width: 100%; border-collapse: collapse; border: 1px solid var(--iron); margin-bottom: 2rem; }
.receipt th, .receipt td {
  padding: 1rem; text-align: left; vertical-align: top;
  font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase;
  border-bottom: 1px solid var(--iron);
}
.receipt tr:last-child th, .receipt tr:last-child td { border-bottom: 0; }
.receipt th { color: var(--dust); font-weight: 400; width: 42%; border-right: 1px solid var(--iron); }
.receipt td { color: var(--parchment); font-variant-numeric: tabular-nums; }

/* ==========================================================================
   FIXED CHROME
   ========================================================================= */
#veil {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  /* Edge darkening only. The film grain and the main vignette are rendered
     into the WebGL composite pass; this exists so the fixed chrome below has
     something to sit on wherever the camera is. */
  background:
    linear-gradient(to bottom, rgba(5, 5, 5, 0.62) 0%, rgba(5, 5, 5, 0) 22%),
    linear-gradient(to top,    rgba(5, 5, 5, 0.55) 0%, rgba(5, 5, 5, 0) 20%);
}

#progress-rail {
  position: fixed; top: 0; left: 0; right: 0; height: 1px;
  z-index: 60; pointer-events: none; background: rgba(43, 39, 36, 0.6);
}
#progress-rail span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(to right, var(--crimson), var(--ember));
  transform-origin: left;
}

#chapter-mark {
  position: fixed; left: var(--gutter); top: 2rem; z-index: 60;
  display: flex; align-items: center; gap: 0.85rem;
  font-size: 0.6875rem; letter-spacing: 0.26em; text-transform: uppercase;
  color: var(--dust); pointer-events: none; user-select: none;
}
#chapter-mark [data-chapter-num] { color: var(--ember); font-variant-numeric: tabular-nums; }
#chapter-mark .rule { width: 28px; height: 1px; background: var(--iron); }

/* The chapter rail. Hidden on narrow and coarse-pointer devices: it is a
   hover affordance and a thumb cannot use it. */
#rail { display: none; }
@media (min-width: 1100px) and (pointer: fine) {
  #rail {
    display: block; position: fixed; right: var(--gutter); top: 50%;
    transform: translateY(-50%); z-index: 60;
  }
  #rail ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1.1rem; align-items: flex-end; }
  #rail a {
    display: flex; align-items: center; gap: 0.75rem;
    font-size: 0.6875rem; letter-spacing: 0.24em; text-transform: uppercase;
    color: var(--dust); text-decoration: none;
    transition: color 0.4s var(--ease-out);
  }
  #rail a::after {
    content: ""; width: 22px; height: 1px; background: var(--iron);
    transition: width 0.5s var(--ease-out), background 0.4s var(--ease-out);
  }
  #rail a:hover { color: var(--parchment); }
  #rail a.is-active { color: var(--ember); }
  #rail a.is-active::after { width: 46px; background: var(--ember); }

  /* The rail is fixed to the right gutter and the footer's fourth column runs
     under it — at 1568px the advisory copy and "The Casting Call" overlapped
     outright. Rather than shove the whole footer left and throw its centred
     sign-off off-axis, the rail retires: it navigates the journey, and the
     footer is where the journey has ended. main.js adds .rail-hidden once the
     footer is the active chapter. */
  #rail { transition: opacity 0.6s var(--ease-out), visibility 0.6s; }
  #rail.rail-hidden { opacity: 0; visibility: hidden; pointer-events: none; }
}

/* ==========================================================================
   THE LOADING GATE
   ========================================================================= */
#threshold {
  display: flex; align-items: center; justify-content: center;
  padding: var(--gutter);
  transition: opacity 1.1s var(--ease-io), visibility 1.1s;
}
#threshold.is-open { opacity: 0; visibility: hidden; pointer-events: none; }
.threshold-inner { text-align: center; max-width: 520px; width: 100%; }
.threshold-word {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  font-size: clamp(1.5rem, 4vw, 2.75rem); line-height: 1.1;
  margin: 0 0 2.5rem;
}
.threshold-bar { height: 1px; background: var(--iron); margin-bottom: 1rem; overflow: hidden; }
.threshold-bar span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(to right, var(--crimson), var(--ember));
  transition: width 0.4s var(--ease-out);
}
#threshold .plate { margin: 0 0 1rem; }
#threshold .plate.dim { margin: 0; font-variant-numeric: tabular-nums; }

/* ==========================================================================
   FOOTER
   ========================================================================= */
#colophon {
  position: relative; z-index: 2;
  padding: clamp(5rem, 12vh, 9rem) var(--gutter) 3rem;
  /* The one place a scrim is needed at full strength: the last camera frame
     is the lit doorway, which is the brightest thing in the whole scene. */
  background: linear-gradient(to bottom, rgba(5, 5, 5, 0.55), rgba(5, 5, 5, 0.94) 40%);
  border-top: 1px solid var(--iron);
}
.foot-grid { display: grid; gap: 3rem; margin-bottom: 5rem; }
@media (min-width: 700px)  { .foot-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .foot-grid { grid-template-columns: repeat(4, 1fr); } }
.foot-lead { font-family: var(--font-display); font-size: 1.25rem; line-height: 1.3; margin: 0 0 1rem; }
.foot-sign {
  font-family: var(--font-display); font-weight: 900; text-transform: uppercase;
  font-size: clamp(1.75rem, 6vw, 4.5rem); line-height: 1;
  text-align: center; color: var(--parchment); margin: 0 0 4rem;
  opacity: 0.9;
}
.foot-rule {
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;
  padding-top: 2rem; border-top: 1px solid var(--iron);
}
.foot-rule .plate { margin: 0; }

/* THE TWO NOTICE LINKS. Dust rather than ember, and that is the whole styling
   decision: on this site ember is light and it means "this is live, press it".
   A privacy notice is a thing that must be FINDABLE, not a thing anybody should
   be drawn towards — dressing it as a call to action would put the same visual
   weight on it as the ticket button two sections up. It lifts to parchment on
   hover, which is enough to say it is a link, and the separator is decorative
   so a screen reader reads two links rather than "Privacy dot Refunds". */
.foot-legal { display: flex; gap: 0.6rem; align-items: baseline; }
.foot-legal a {
  color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--iron);
  padding-bottom: 1px;
  transition: color 0.18s var(--ease-out), border-color 0.18s var(--ease-out);
}
.foot-legal a:hover { color: var(--parchment); border-bottom-color: var(--dust); }

/* --------------------------------------------------------------------------
   THE FOOTER DISCLOSURE
   A <details>, so it opens with no JavaScript and closes with one press. The
   marker is replaced with a typed one because the native triangle is drawn by
   the operating system in the operating system's own colour, which on a
   near-black footer is either invisible or a bright blue arrowhead — the one
   piece of another vendor's design language on the page.

   THE SUMMARY IS A TARGET, NOT A WORD. 44px of height and a real focus ring:
   it is the only interactive element in the colophon that is not a link, and
   it is read on a phone by somebody who has scrolled to the bottom.
   ----------------------------------------------------------------------- */
.foot-note { margin: 0 0 1.5rem; }
.foot-note > summary {
  display: flex; align-items: center; gap: 0.55rem;
  min-height: 44px; cursor: pointer; list-style: none;
  font-family: var(--font-body); font-size: 0.8rem; letter-spacing: 0.02em;
  color: var(--dust); transition: color 0.2s ease;
}
.foot-note > summary::-webkit-details-marker { display: none; }
.foot-note > summary::before {
  content: "+"; display: inline-block; width: 1ch;
  font-family: var(--font-body); color: var(--ember); transition: transform 0.2s ease;
}
.foot-note[open] > summary::before { content: "\2013"; }
.foot-note > summary:hover { color: var(--parchment); }
.foot-note > summary:focus-visible {
  outline: 2px solid var(--ember); outline-offset: 3px;
}
.foot-note-body {
  max-width: 62ch; padding: 0.25rem 0 0 1.55rem;
  border-left: 1px solid var(--iron); margin-left: 0.1rem;
}
.foot-note-body .prose { margin-bottom: 0.75rem; }
.foot-note-body .prose:last-child { margin-bottom: 0; }

/* ==========================================================================
   REVEALS
   The `data-reveal` elements start hidden and are released by main.js. If
   JavaScript never runs, the .no-js rule below hands them all back — content
   must never be gated on an animation.
   ========================================================================= */
[data-reveal] { opacity: 0; }
.js-ready [data-reveal] { will-change: opacity, transform; }
html.no-js [data-reveal], html.no-js .wordmark-mark { opacity: 1 !important; transform: none !important; clip-path: none !important; }

/* Reduced motion: everything arrives, nothing travels. The reveal, the cue
   animation and the scene's own easing all collapse to a state change. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal], .wordmark-mark { opacity: 1 !important; transform: none !important; clip-path: none !important; }
}

/* No WebGL. The scene never came up, so the page provides its own ground and
   the panels stop pretending to be lenses over something. */
html.no-webgl body { background: var(--void); }
html.no-webgl .panel, html.no-webgl .door {
  background: rgba(10, 9, 8, 0.92);
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
html.no-webgl #veil { display: none; }

/* ==========================================================================
   CINEMATIC SCROLL
   --------------------------------------------------------------------------
   Everything here is scoped behind `html.cinematic`, which js/main.js adds
   only when its CINEMATIC_SCROLL flag is on and the visitor has not asked for
   reduced motion. Flip that one constant and this whole block stops applying —
   there is no second switch and nothing to unpick.

   TWO BEHAVIOURS:

   1. THE FADE, at every size. While the page is moving the overlay steps back
      so the walk is unobstructed; when it settles, the section returns. The
      scene is never touched — it is the thing being revealed.

   2. SNAPPING, one section per gesture, on any window big enough to hold a
      section. See the media query further down for why it is gated on size
      rather than applied everywhere.

   WHY THE LIFT MOVED OFF #content — this was the glitch.
   A snap target's snap position is read from its TRANSFORMED border box, and
   `#content` is the ancestor of all six of them. Putting `translateY(10px)` on
   it moved every snap point down 10px the instant a scroll began and back up
   10px the instant it stopped, so the snap engine was re-targeting mid-flight,
   twice per gesture, against a destination that would not hold still. The lift
   now lives on `.act-inner`, which is INSIDE the snap target: a descendant's
   transform is invisible to the snap engine, and the fade looks identical.
   ========================================================================= */
html.cinematic {
  /* Snapping and smooth scrolling fight: the browser animates toward the
     target and then snaps it again on arrival, which reads as a bounce. */
  scroll-behavior: auto;
}

/* --------------------------------------------------------------------------
   THE FADE IS DELIBERATELY ASYMMETRIC — leaving is not the same gesture as
   arriving, and giving both the same curve is what made the first version feel
   like a dimmer switch rather than a cut.

   OUT is a cut: 0.18s on an ease-IN curve, so the copy is gone almost before
   the page has begun to move, and gone to ZERO rather than to a faint 0.06 —
   six percent of a white serif over a dark scene is still legible enough to
   read as a ghost dragged across the graveyard, which is the one thing this
   effect must not do.

   IN is an entrance: it waits 0.22s after the page has settled, then takes
   0.95s on the house ease-out, with the panels a further 0.16s behind the text
   so a section assembles rather than arriving whole. The pause matters as much
   as the duration — it lets the eye register the new shot before any type
   arrives on top of it.

   The direction is chosen by WHICH RULE CARRIES THE TRANSITION. A transition
   declared on the base rule governs the return to that state; one declared on
   the .is-scrolling rule governs the trip into it. Two rules, two curves, no
   JavaScript.
   ----------------------------------------------------------------------- */

/* IN — the return. Late, and unhurried. */
html.cinematic #content,
html.cinematic #colophon {
  transition: opacity 0.95s var(--ease-out) 0.22s;
}
html.cinematic .act-inner,
html.cinematic .scroll-cue {
  /* The travel, on elements the snap engine does not measure. No blur: a
     backdrop blur on a full-page layer over a live canvas costs a full-screen
     pass every frame, and this fires during the one moment the frame budget is
     already going on the camera. */
  transition: transform 1.15s var(--ease-out) 0.22s;
}

/* OUT — the cut. `ease-in`, so it accelerates away instead of lingering. */
html.cinematic.is-scrolling #content,
html.cinematic.is-scrolling #colophon {
  transition: opacity 0.18s cubic-bezier(0.4, 0, 1, 1);
  opacity: 0;
  /* Invisible, so it must not keep swallowing clicks — but it returns on a
     190ms idle, so this is only ever set while the page is genuinely in motion
     and nothing could be aimed at anyway. */
  pointer-events: none;
}
html.cinematic.is-scrolling .act-inner,
html.cinematic.is-scrolling .scroll-cue {
  transition: transform 0.22s cubic-bezier(0.4, 0, 1, 1);
  transform: translateY(12px);
}

/* The chapter mark and rail are the exception, and stay symmetric. They are
   the two things that say WHERE you are — the one question a reader has
   mid-scroll — so they dim rather than leave, and they come back immediately
   rather than waiting their turn behind the copy. */
html.cinematic #chapter-mark,
html.cinematic #rail { transition: opacity 0.42s var(--ease-out); }
html.cinematic.is-scrolling #chapter-mark { opacity: 0.75; }
html.cinematic.is-scrolling #rail { opacity: 0.5; }

/* The hero scrim goes with the type it exists to protect, on the same two
   curves. Leaving it up while that type has gone is a dark rectangle sitting
   on the gates for no reason. */
html.cinematic #arrival::after { transition: opacity 0.95s var(--ease-out) 0.22s; }
html.cinematic.is-scrolling #arrival::after {
  transition: opacity 0.18s cubic-bezier(0.4, 0, 1, 1);
  opacity: 0;
}

/* Panels arrive after the text around them, so a section assembles rather than
   appearing all at once — a longer wait and a longer travel, but the same cut
   on the way out. All three are descendants of a snap target, so none of them
   can move a snap point. */
html.cinematic .panel,
html.cinematic .doors,
html.cinematic .spec {
  transition:
    opacity 1.05s var(--ease-out) 0.38s,
    transform 1.25s var(--ease-out) 0.38s;
}
html.cinematic.is-scrolling .panel,
html.cinematic.is-scrolling .doors,
html.cinematic.is-scrolling .spec {
  transition:
    opacity 0.18s cubic-bezier(0.4, 0, 1, 1),
    transform 0.22s cubic-bezier(0.4, 0, 1, 1);
  transform: translateY(22px) scale(0.994);
}

/* ==========================================================================
   ONE SECTION, ONE VIEWPORT, ONE GESTURE
   --------------------------------------------------------------------------
   Mandatory snapping only works if a section actually fits the window, so
   everything in this query does two jobs at once: it fixes each act to exactly
   one viewport, and it re-lays-out the two acts that could not possibly fit at
   that height — the registry and the casting call — into two columns, copy
   beside form, instead of copy above form.

   WHY IT IS GATED ON SIZE. Below this, the acts stack into columns far taller
   than the window and no amount of tightening fixes that; mandatory snap there
   would put a submit button somewhere the page refuses to rest. So a phone
   keeps the ordinary document it has always had, and keeps the fade. The
   snapping is an enhancement for windows that can hold it, not a mode the
   layout depends on.

   A SIDE EFFECT WORTH HAVING: six sections at exactly one viewport each makes
   the document exactly five viewports of scroll, and scene.js interpolates six
   camera waypoints across that same 0–1. Every snap point now lands precisely
   on a waypoint, so the walk comes to rest on a composed shot every time
   rather than wherever the copy happened to end.
   ========================================================================= */
@media (min-width: 1120px) and (min-height: 700px) {

  html.cinematic {
    scroll-snap-type: y mandatory;
    /* The acts no longer need padding to establish their height — they are a
       viewport tall by decree — so it goes back to being breathing room. */
    --act-pad: clamp(3rem, 7vh, 5.5rem);
  }

  html.cinematic .act,
  html.cinematic #colophon {
    height: 100svh;
    scroll-snap-align: start;
    /* `always` is the whole request: it forbids passing over a snap point, so
       one gesture advances exactly one section and a fast flick cannot skip
       three. Rail and skip links would be caught by it too, so main.js lifts
       snapping for the length of a programmatic jump. */
    scroll-snap-stop: always;

    /* THE SAFETY NET. Everything below is sized to fit, but "fits" depends on
       the font that actually loaded and the text that is actually in it. If a
       section ever does outgrow its viewport, mandatory snap would refuse to
       rest anywhere the overflow could be read — so the section scrolls
       internally instead, and chains out to the page at its ends. It costs
       nothing when the content fits, which is the normal case. */
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--iron) transparent;
  }

  /* ------------------------------------------------------------------------
     THE ACTS READ SMALLER THAN THE HERO
     ------------------------------------------------------------------------
     Fitting each act to one viewport is not the same as filling one, and the
     first pass conflated them: every section was tuned until it *just* fit,
     which left five screens of type running wall to wall with no air anywhere.
     A shot that touches all four edges has no composition in it.

     So everything from the second act to the last comes down about 12% and the
     freed space becomes margin. The hero is deliberately excluded — it is the
     title card, it is supposed to be enormous, and the wordmark is sized
     against the gates rather than against the type.

     `[data-act]:not([data-act="0"])` is the whole selector: the acts already
     carry their index for the camera, so "the second section to the last" is a
     fact the markup already states rather than a list of five ids to keep in
     sync with the HTML.
     --------------------------------------------------------------------- */
  html.cinematic [data-act]:not([data-act="0"]) {
    /* Body copy, and everything that inherits it. */
    font-size: clamp(1rem, 1.06vw, 1.125rem);
  }

  html.cinematic [data-act]:not([data-act="0"]) .act-inner { max-width: 1100px; }
  html.cinematic [data-act]:not([data-act="0"]) .panel-wide { max-width: 820px; }

  html.cinematic [data-act]:not([data-act="0"]) .h1 {
    font-size: clamp(1.85rem, 4.05vw, 3.5rem);
  }
  html.cinematic [data-act]:not([data-act="0"]) .plate {
    font-size: clamp(0.6875rem, 0.78vw, 0.75rem);
    margin-bottom: 1rem;
  }
  html.cinematic [data-act]:not([data-act="0"]) .pull p {
    font-size: clamp(1.05rem, 1.6vw, 1.35rem);
  }
  html.cinematic [data-act]:not([data-act="0"]) .spec dd { font-size: 0.95rem; }
  html.cinematic [data-act]:not([data-act="0"]) .door-num { font-size: 1.9rem; }
  html.cinematic [data-act]:not([data-act="0"]) .door p { font-size: 1rem; }

  /* Form controls come down with everything else, but the PADDING does not:
     the target stays the size a pointer needs even when the label on it is a
     point smaller. */
  html.cinematic [data-act]:not([data-act="0"]) .field { font-size: 1rem; }
  html.cinematic [data-act]:not([data-act="0"]) .btn { font-size: 0.75rem; }
  html.cinematic [data-act]:not([data-act="0"]) .receipt-head h3 {
    font-size: clamp(1.15rem, 1.6vw, 1.5rem);
  }

  html.cinematic .foot-sign { font-size: clamp(1.5rem, 4.9vw, 3.5rem); }
  html.cinematic .foot-lead { font-size: 1.125rem; }

  /* `safe` centring. A centred flex item that outgrows its container overflows
     equally in both directions, which puts the TOP of the content above the
     scroll origin where it can never be reached. `safe` gives up the centring
     at exactly the moment centring would start costing content. */
  html.cinematic .act { align-items: safe center; }
  html.cinematic #arrival { align-items: safe flex-end; }

  html.cinematic #colophon {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-top: clamp(3rem, 7vh, 5.5rem);
    padding-bottom: clamp(2rem, 4vh, 3rem);
  }
  html.cinematic .foot-grid { margin-bottom: clamp(2rem, 5vh, 3.5rem); }
  html.cinematic .foot-sign { margin-bottom: clamp(1.5rem, 4vh, 2.75rem); }

  /* ---- 02 · THE INVITATION --------------------------------------------
     Single column, simply tighter. The panel is a card and reads as one; the
     savings come out of the gaps between its parts, which were generous enough
     to give up sixty pixels without the composition noticing. */
  html.cinematic #host .panel { padding: clamp(1.5rem, 3vw, 2.5rem); }
  html.cinematic .pull { margin: 1.5rem 0; }
  html.cinematic .spec { margin-top: 1.75rem; }
  html.cinematic #host .prose p { margin-bottom: 0.9rem; }

  /* ---- 03 · SIX DOORS -------------------------------------------------- */
  html.cinematic #paths .act-head { margin-bottom: clamp(1.75rem, 4vh, 2.75rem); }
  html.cinematic .door { padding: clamp(1.35rem, 2.4vw, 2rem); min-height: 0; gap: 1rem; }

  /* ---- 04 & 05 · THE TWO FORMS ----------------------------------------
     Copy beside form. Stacked, the casting call was 1359px of content in an
     820px window — not a spacing problem, a composition problem. Side by side,
     the tallest column is the panel and the act sits comfortably inside one
     viewport with room to spare. */
  html.cinematic #registry .act-inner,
  html.cinematic #audition .act-inner {
    max-width: 1180px;
    display: grid;
    /* Exactly two items — .act-aside and .panel — so the tracks are the whole
       layout and no row can appear to fall out of. */
    grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
    column-gap: clamp(2rem, 4.5vw, 4.5rem);
    align-items: center;
  }

  html.cinematic #registry .act-head,
  html.cinematic #audition .act-head { margin-bottom: 0; }

  /* A trailing margin at the foot of a vertically centred column is not
     spacing, it is twenty pixels of nothing that the section has to be tall
     enough to hold. */
  html.cinematic .act-head > *:last-child,
  html.cinematic .act-aside > *:last-child { margin-bottom: 0; }

  /* The four-up spec goes 2×2 in half the width, and the "17 tracks · 24
     positions" line stops wrapping to three. */
  html.cinematic #audition .spec-4 { grid-template-columns: repeat(2, 1fr); margin-top: 1.75rem; }
  html.cinematic #audition .inclusive { margin: 1.5rem 0 0; }

  /* The forms themselves lose a little air. Five fields at 1.5rem apart is a
     lot of air, and it is the cheapest forty pixels in the file. */
  html.cinematic #registry form,
  html.cinematic #audition form,
  html.cinematic #registry .field-grid,
  html.cinematic #audition .field-grid { gap: 1.15rem; }
  html.cinematic #registry .panel,
  html.cinematic #audition .panel { padding: clamp(1.5rem, 2.6vw, 2.25rem); }
  html.cinematic #registry .btn-block,
  html.cinematic #audition .btn-block { margin-top: 0.5rem; }
}

/* --------------------------------------------------------------------------
   THE SHORT LAPTOP — 1280×720 and its neighbours.
   Measured there, the invitation ran 61px over and the casting call 53px, and
   1366×768 is not much kinder. That is the most common window this site will
   ever open in, so it gets its own pass rather than being handed to the
   internal-scroll safety net: a section you have to scroll inside is a section
   that has stopped being one shot.

   All of it comes out of padding and gaps. Nothing here changes a type size —
   the composition is the same one, set tighter.
   ----------------------------------------------------------------------- */
@media (min-width: 1120px) and (min-height: 700px) and (max-height: 810px) {
  html.cinematic { --act-pad: clamp(2rem, 5vh, 4rem); }

  html.cinematic #host .panel,
  html.cinematic #registry .panel,
  html.cinematic #audition .panel { padding: 1.5rem 1.75rem; }

  html.cinematic .pull { margin: 1.1rem 0; }
  html.cinematic .spec { margin-top: 1.35rem; }
  html.cinematic .spec > div { padding: 0.8rem 1.1rem; }
  html.cinematic #host .prose p { margin-bottom: 0.7rem; }

  html.cinematic #registry form,
  html.cinematic #audition form,
  html.cinematic #registry .field-grid,
  html.cinematic #audition .field-grid { gap: 1rem; }
  html.cinematic #registry .field,
  html.cinematic #audition .field { padding: 0.8rem 1rem; }

  html.cinematic #audition .spec-4 { margin-top: 1rem; }
  html.cinematic #audition .inclusive { margin-top: 0.8rem; }
}

/* ==========================================================================
   ONE SECTION, ONE VIEWPORT, ONE GESTURE — ON A PHONE
   --------------------------------------------------------------------------
   The same promise as the desktop query above, kept the same way: every act is
   fixed to exactly one viewport, mandatory snapping rests only on act
   boundaries, and `scroll-snap-stop: always` forbids passing one, so a flick
   advances exactly one section and six gestures reach the bottom.

   An earlier pass used PROXIMITY snapping here on the grounds that the acts do
   not fit a phone. They did not fit because nothing had been done to make them
   fit — the whole layout was still the one composed for a desktop, merely
   narrower. This block does for a 375px screen what the block above does for a
   1440px one, and it is the same three moves in the same order:

     1. THE TYPE COMES DOWN, acts two to six only. The hero is the title card
        and keeps its scale.
     2. THE GRIDS RE-FLOW. Six doors go two-up instead of six-up — three rows
        instead of six is 575px back on its own. The casting spec goes 2x2. The
        colophon goes two columns.
     3. THE AIR COMES OUT of panel padding, form gaps and heading margins,
        which is where a desktop composition keeps most of its slack.

   WHAT IS NOT DONE HERE: nothing is hidden, and no tap target shrinks. Form
   controls keep their padding even as their labels come down a point, because
   a target that fits the layout and not the thumb has solved the wrong
   problem. Copy is set smaller, never removed — a phone visitor gets the same
   words as a desktop one.

   THE SAFETY NET IS THE SAME ONE. Everything below is measured to fit, but
   "fits" depends on the font that actually loaded, the text actually in it,
   and how much chrome the browser is showing this second. If an act ever does
   outgrow its viewport it scrolls internally and chains out to the page at its
   ends, rather than mandatory snapping refusing to rest anywhere the overflow
   could be read. It costs nothing when the content fits.
   ========================================================================= */
@media (max-width: 1119px), (max-height: 699px) {

  html.cinematic { scroll-snap-type: y mandatory; }

  html.cinematic .act,
  html.cinematic #colophon {
    height: 100svh;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    overflow-y: auto;
    overscroll-behavior-y: auto;   /* chain out at the ends. See safety net. */
  }

  /* `safe` centring: a centred flex item that outgrows its container overflows
     equally both ways, which puts the TOP of the content above the scroll
     origin where it can never be reached. `safe` gives up the centring at
     exactly the moment centring would start costing content. */
  html.cinematic .act { align-items: safe center; }
  html.cinematic #arrival { align-items: safe flex-end; }

  /* Top padding is clearance for #chapter-mark, which is FIXED and ends 50px
     down the screen; it is set by that and not by taste. The bottom is only
     breathing room and gives up most of itself. */
  html.cinematic .act {
    padding-top: clamp(4.5rem, 10.5vh, 6rem);
    padding-bottom: clamp(1.5rem, 3.5vh, 2.5rem);
  }

  /* ---- TYPE, ACTS TWO TO SIX ------------------------------------------
     `[data-act]:not([data-act="0"])` — the acts already carry their index for
     the camera, so "the second section to the last" is a fact the markup
     states rather than a list of ids to keep in sync. */
  /* THE BODY SIZE IS TIED TO THE VIEWPORT'S HEIGHT, not to a fixed rem. Phones
     differ by 180px of screen between an SE and a Pro Max while their WIDTHS
     barely move, so a section that fits one and not the other is a height
     problem — and a fixed type size cannot answer it. 1.78vh lands on 15px at
     844 and walks down from there, with the lower clamp at 13px because that
     is where this face stops being comfortable and no layout is worth going
     past it. */
  html.cinematic [data-act]:not([data-act="0"]) {
    font-size: clamp(0.8125rem, 1.78vh, 0.9375rem);
  }
  html.cinematic [data-act]:not([data-act="0"]) .h1 { font-size: clamp(1.5rem, 7.4vw, 2.25rem); }
  html.cinematic [data-act]:not([data-act="0"]) .plate { font-size: 0.625rem; margin-bottom: 0.6rem; }
  html.cinematic [data-act]:not([data-act="0"]) .pull p { font-size: 1.02rem; }
  html.cinematic [data-act]:not([data-act="0"]) .prose p { margin-bottom: 0.65rem; }
  html.cinematic [data-act]:not([data-act="0"]) .spec dd { font-size: 0.85rem; }
  html.cinematic [data-act]:not([data-act="0"]) .spec dt { font-size: 0.58rem; }
  html.cinematic [data-act]:not([data-act="0"]) .act-head { margin-bottom: 1.15rem; }

  /* Labels come down; PADDING DOES NOT. The target stays the size a thumb
     needs even when the word on it is a point smaller. */
  html.cinematic [data-act]:not([data-act="0"]) .field { font-size: 0.9375rem; }
  html.cinematic [data-act]:not([data-act="0"]) .btn { font-size: 0.6875rem; }

  html.cinematic .act-head > *:last-child,
  html.cinematic .act-aside > *:last-child { margin-bottom: 0; }

  /* ---- 02 · THE INVITATION -------------------------------------------- */
  html.cinematic #host .panel { padding: 1.15rem 1.1rem; }
  html.cinematic #host .pull { margin: 0.9rem 0; }
  html.cinematic #host .pull p { font-size: 0.98rem; }
  html.cinematic #host .spec { margin-top: 0.9rem; }
  html.cinematic #host .panel-tab { display: none; }   /* the card names itself */

  /* ---- 03 · SIX DOORS --------------------------------------------------
     Two-up. Six stacked cards were 1147px in a 682px budget and no amount of
     padding would have closed that; three rows instead of six is 575px back
     before a single value is tightened. */
  html.cinematic #paths .doors { grid-template-columns: 1fr 1fr; }
  html.cinematic #paths .door {
    padding: 0.85rem 0.8rem; min-height: 0; gap: 0.5rem;
  }
  html.cinematic #paths .door p { font-size: 0.8125rem; line-height: 1.4; }
  html.cinematic #paths .door-num { font-size: 1.1rem; }

  /* ---- 04 · THE REGISTRY & 05 · THE CASTING CALL ----------------------- */
  html.cinematic #registry form,
  html.cinematic #audition form,
  html.cinematic #registry .field-grid,
  html.cinematic #audition .field-grid { gap: 0.55rem 0.7rem; }
  html.cinematic #registry .panel,
  html.cinematic #audition .panel { padding: 1rem; }
  html.cinematic #registry .btn-block,
  html.cinematic #audition .btn-block { margin-top: 0.25rem; }

  /* THE CONTROL KEEPS A 46px TARGET. That is the floor, not a starting point —
     below it a thumb starts missing, and a form that fits the screen and not
     the hand has solved the wrong problem. Everything saved on these rows
     comes out of the LABEL above the box and the gap below it, never the box. */
  html.cinematic #registry .field,
  html.cinematic #audition .field { padding-top: 0.7rem; padding-bottom: 0.7rem; }
  html.cinematic #registry .field-row > label,
  html.cinematic #audition .field-row > label { margin-bottom: 0.3rem; font-size: 0.72rem; }

  /* Name and email are the long ones and stay full width; the short answers
     pair up rather than each taking a row of its own. .field-grid only went
     two-up at 640px, so on a phone it was stacking two half-width questions
     into two full rows — 89px per form, spent on nothing. */
  html.cinematic #registry .field-grid,
  html.cinematic #audition .field-grid { grid-template-columns: 1fr 1fr; }

  /* Four columns, not 2x2. The values are a numeral and a word each, so a
     quarter of the width is enough to set them, and it turns 207px of stacked
     pairs into one band. The inclusivity note stays — it is the paragraph most
     worth reading on this page and is not a candidate for the savings. It is
     set smaller, like everything else here. */
  html.cinematic #audition .spec-4 { grid-template-columns: repeat(4, 1fr); margin-top: 0.65rem; }
  html.cinematic #audition .spec-4 dd { font-size: 0.72rem; line-height: 1.3; }
  html.cinematic #audition .spec-4 dt { margin-bottom: 0.25rem; }
  html.cinematic #audition .act-head h2.h1 { font-size: clamp(1.35rem, 6.6vw, 1.9rem); }
  html.cinematic #audition .inclusive { margin: 0.75rem 0 0; font-size: 0.78rem; }
  html.cinematic #audition .act-aside,
  html.cinematic #registry .act-aside { margin-bottom: 0.9rem; }
  html.cinematic #audition .act-head .prose,
  html.cinematic #registry .act-head .prose { font-size: 0.875rem; }
  html.cinematic #audition form .prose.small { font-size: 0.75rem; margin-bottom: 0.5rem; }
  html.cinematic #audition .act-head .prose,
  html.cinematic #registry .act-head .prose { line-height: 1.45; }
  html.cinematic #audition .panel-tab,
  html.cinematic #registry .panel-tab { display: none; }   /* the heading names it */
  html.cinematic #audition .act-head { margin-bottom: 0.8rem; }

  /* THE SPEC GOES SIDEWAYS RATHER THAN INTO COLUMNS. Three columns was the
     obvious saving and the wrong one: at a third of 375px, "A decaying
     Victorian estate" breaks over four lines and "Six worlds · one path
     between them" over five, so the row is ragged, the columns are different
     heights and it reads as a squeeze. Setting the label BESIDE its value
     instead of above it is shorter still — three tidy rows of one line each
     rather than three columns of five — and it is how a spec sheet is set on a
     narrow page anyway. Same words, less height, better line lengths. */
  html.cinematic #host .spec > div {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 1rem; padding: 0.6rem 0.85rem;
  }
  /* The label gives up most of its tracking so the value gets the width it
     needs to sit on one line. A 0.26em spread is a display flourish that only
     earns its space when there is space. */
  html.cinematic #host .spec dt {
    margin-bottom: 0; flex: none; font-size: 0.58rem; letter-spacing: 0.12em;
  }
  html.cinematic #host .spec dd { text-align: right; font-size: 0.82rem; line-height: 1.35; }
  html.cinematic #host .prose { margin-bottom: 0.6rem; }
  html.cinematic #colophon .foot-sign { margin-bottom: 0.6rem; }
  html.cinematic #colophon .foot-grid { row-gap: clamp(0.5rem, 1.4vh, 0.9rem); }

  /* ---- 06 · THE DOOR --------------------------------------------------- */
  html.cinematic #colophon {
    display: flex; flex-direction: column; justify-content: center;
    padding-top: clamp(4.5rem, 10.5vh, 6rem);
    padding-bottom: clamp(1.5rem, 3.5vh, 2.5rem);
  }
  html.cinematic .foot-grid {
    grid-template-columns: 1fr 1fr; gap: 0.9rem 1.25rem; margin-bottom: 1.1rem;
  }
  html.cinematic .foot-sign { font-size: clamp(1.35rem, 6.4vw, 2rem); margin-bottom: 0.9rem; }
  html.cinematic .foot-lead { font-size: 0.9375rem; }
  /* The footer's four blocks are the densest thing on the page and the least
     read; they give up their leading first. */
  html.cinematic #colophon .foot-grid p {
    font-size: 0.8125rem; line-height: 1.45; margin-bottom: 0.3rem;
  }
  html.cinematic #colophon .foot-grid > * > *:last-child { margin-bottom: 0; }
  html.cinematic #colophon .foot-rule { margin-top: 0; gap: 0.4rem; }
  html.cinematic #colophon .foot-rule p { margin-bottom: 0.25rem; font-size: 0.6rem; }
}

/* One gesture, one section, is a promise about SCROLLING. It must never become
   a promise about reading, so a section being worked with the keyboard is
   exempt: focus inside a form releases snapping for as long as the focus is
   there. Set by main.js. */
html.cinematic.is-typing { scroll-snap-type: none; }

/* Programmatic jumps — the rail, the skip link, the hero's two buttons — run
   with snapping off. `scroll-snap-stop: always` applies to those too, and
   without this a click on "The Casting Call" from the hero stops at every
   section in between. */
html.cinematic.is-navigating { scroll-snap-type: none; }
