/* ============================================================
   CANDACE — scroll-driven film landing page
   One fullscreen section; stacked 4K videos swapped on
   pixel-identical boundary frames.
   ============================================================ */

/* ---------- Type ----------
   Two licensed faces, self-hosted so the type never waits on a third
   party: Baygo carries every display header (its erosion and 11° rake
   are drawn into the glyphs — the grain is the typeface, not an
   effect), Earlgos sets all body copy. Both ship as a single weight,
   so each is declared across the full 100–900 range: that tells the
   browser to use the real face for any weight the stylesheet asks for
   instead of smearing a synthetic bold over an already-heavy design. */
@font-face {
  font-family: "Baygo";
  src: url("assets/fonts/Baygo.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Earlgos";
  src: url("assets/fonts/Earlgos.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* JetBrains Mono, self-hosted: the same variable latin cut Google
   served, declared exactly the way its CSS did (one file, both
   weights, same unicode-range) — identical rendering with no
   render-blocking third-party request. */
@font-face {
  font-family: "JetBrains Mono";
  src: url("assets/fonts/JetBrainsMono.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("assets/fonts/JetBrainsMono.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;               /* the page never scrolls — gestures drive the film */
  overscroll-behavior: none;      /* no rubber-band / pull-to-refresh */
  background: #000;
}

body {
  touch-action: none;             /* we handle touch gestures ourselves */
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  font-family: "Earlgos", "Helvetica Neue", Helvetica, Arial, ui-sans-serif, system-ui, sans-serif;
  color: #fff;
}

/* ---------- Film stage ---------- */

.stage {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;                  /* fallback */
  height: 100dvh;                 /* tracks mobile browser chrome show/hide */
  background: #000;
  overflow: hidden;
  z-index: 0;
  /* The held-frame defocus, kept as separate parts so the engine's
     snapshot canvas can read the very numbers in force (armSnapshot in
     script.js) instead of carrying its own copy — the painted frame and
     the live filter can never disagree, whichever chapter is holding. */
  --held-blur: 1.8px;
  --held-sat: 0.94;
  --held-bri: 0.96;
}

/* Each chapter's wrapper layer: carries visibility, the editorial-drift
   transform, and the soft-edge mask. Masking/transforming a div is
   reliable everywhere; doing it on <video> directly is not (Safari). */
.stage__layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* Transitions (drift transform + hand-off dissolve) are set inline by
     script.js so each keeps its own duration. No will-change: with drift
     and the dissolve both disabled, hand-offs are instant class toggles,
     and hinting every one of the stacked layers only pinned a permanent
     compositor surface per layer (a real source of mobile stutter). */
  pointer-events: none;
  z-index: 1;
}

/* Chapter hand-offs are a short dissolve: the incoming layer fades in
   over the outgoing one, which holds fully opaque beneath it, so the
   crossover never dips toward black. Gentle by design — just enough to
   absorb the small framing difference between one clip's last frame
   and the next clip's first. */
.stage__layer {
  transition: opacity 240ms ease;
}

/* The outgoing frame, held at full strength under the incoming fade. */
.stage__layer.is-under {
  opacity: 1;
  z-index: 2;
}

/* Declared after .is-under so it wins if a layer briefly carries both. */
.stage__layer.is-active {
  opacity: 1;
  z-index: 3;
}

/* Reversed-sidecar layers never dissolve. A rewind layer's first frame
   matches the frame it covers, and its clip is already moving when it
   appears — fading it in double-exposed a moving picture over a still
   one, which read as a glitch at every rewind start and landing. */
.stage__layer--rev {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .stage__layer,
  .stage__video {
    transition-duration: 0ms !important;
  }
}

/* The media itself just fills its wrapper layer. The fit mode is set
   from CONFIG in script.js ('contain' by default: the full frame at the
   largest size that fits, never cropped, never scaled past its native
   pixels). All layers share identical geometry, keeping the boundary
   frames perfectly aligned at every hand-off. */
.stage__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center center;
  pointer-events: none;
}

/* While a text block holds the frame, the film itself steps back into
   a background image: defocused, drained a step, dimmed. The filter
   sits on the video elements — not on a backdrop-filter scrim, which
   cannot reliably sample hardware-decoded video and so never showed on
   some machines. Clears with the class the engine drops the moment the
   film moves. Scoped to the layer actually on screen, so the hidden
   videos never carry a filtered surface. */
.stage__layer.is-active .stage__video {
  transition: filter 1s ease;     /* the defocus ramps over the clip's last second */
}

.stage--held .stage__layer.is-active .stage__video {
  filter: blur(var(--held-blur)) saturate(var(--held-sat)) brightness(var(--held-bri));
}

/* Chapters that carry a lighter hand: the same treatment, dialled back,
   so those frames sit a step closer to the film behind their text. The
   engine raises this alongside .stage--held for the positions listed in
   CONFIG.heldSoftStages. */
.stage--held-soft {
  --held-blur: 1.25px;
  --held-sat: 0.955;
  --held-bri: 0.973;
}

/* A parked frame copied into a canvas by the engine, replacing the
   per-composite re-filtering of a static video quad. The defocus below
   is the same declaration the video carries, reading the same custom
   properties — so a chapter on the softened variant paints its own
   values — but the canvas never changes, so the browser rasterizes the
   filtered layer once instead of on every composite. */
.stage__snap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.stage--held .stage__snap {
  filter: blur(var(--held-blur)) saturate(var(--held-sat)) brightness(var(--held-bri));
}

.stage--snap .stage__video {
  filter: none !important;
}

/* The frozen frame a cut dissolves away: the last thing on screen,
   held over the stage while the seek behind it decodes. Above every
   film layer, below all page chrome (the stage is its own stacking
   context at z-index 0, so the thesis and banner still sit on top). */
.stage__cut {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 5;
  pointer-events: none;
  opacity: 1;
  transition: opacity 260ms ease;
}

.stage__cut.is-gone {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .stage__cut {
    transition-duration: 0ms;
  }
}

/* The opening still: a high-resolution render of chapter 1's first
   frame, shown only while the film rests at the default screen. It
   lives inside chapter 1's own layer, so the drift transform and the
   soft-edge mask carry over; opacity is the only thing that moves.
   The fade is short enough to read as the film simply starting, long
   enough to absorb the small grade difference between the still and
   the decoded frame it hands off to. */
.stage__still {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.14s ease;
}

.stage--still .stage__still {
  opacity: 1;
}

/* ---------- Brand wordmark ---------- */

.brand {
  /* Top-left corner, clear of the centered compositions in the film. */
  position: fixed;
  top: max(26px, env(safe-area-inset-top));
  left: max(30px, env(safe-area-inset-left));
  display: flex;
  pointer-events: none;
  /* Above the thesis wash, so the chrome stays sharp while the film
     behind it is graded down. */
  z-index: 12;
}

.brand__wordmark {
  margin: 0;
  font-size: clamp(18px, 2.2vw, 27px);
  font-weight: 500;
  letter-spacing: 0.5em;
  text-indent: 0.5em;             /* visually re-centres letter-spaced text */
  color: #fff;
  mix-blend-mode: difference;     /* stays legible over any footage */
}

/* ---------- Top banner (site navigation) ---------- */
/* Four page links, centred at the top, persistent over every frame.
   Same chrome treatment as the rail: white over drop shadows, resting
   dimmed, rising on hover with a hairline that draws in beneath. */

.topnav {
  position: fixed;
  top: max(26px, env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: clamp(22px, 3vw, 44px);
  z-index: 12;                    /* above the thesis wash — see .brand */
  pointer-events: none;
}

.topnav__link {
  position: relative;
  pointer-events: auto;
  padding: 6px 2px;
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: #fff;
  opacity: 0.34;
  cursor: default;
  transition: opacity 0.3s ease;
  text-shadow:
    0 1px 4px rgba(0, 0, 0, 0.75),
    0 2px 14px rgba(0, 0, 0, 0.7);
  -webkit-tap-highlight-color: transparent;
}

/* The hover mark: a hairline that draws out beneath the word. */
.topnav__link::after {
  content: "";
  position: absolute;
  left: 2px;
  right: 2px;
  bottom: 0;
  height: 1px;
  background: currentColor;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.35s cubic-bezier(0.2, 0.6, 0.2, 1);
}

/* Links are not yet operational: hover/focus stays dimmed and the hairline
   never draws, so nothing signals these as live destinations. */
.topnav__link:hover,
.topnav__link:focus-visible {
  opacity: 0.44;
  outline: none;
}

.topnav__link:hover::after,
.topnav__link:focus-visible::after {
  transform: scaleX(0);
}

/* Narrow screens: the centre of the top row belongs to the wordmark's
   line, so the banner drops to its own row beneath, sized to clear the
   chapter cards (which start at 9vh). */
@media (max-width: 700px) {
  .topnav {
    top: 52px;
    gap: 14px;
  }
  .topnav__link {
    padding: 4px 1px;
    font-size: 10px;
    letter-spacing: 0.18em;
  }
}

/* ---------- Side menu (chapter navigation) ---------- */
/* A numbered rail on the right edge: one button per resting position.
   The nav itself stays transparent to gestures; only the buttons take
   the pointer. White type over a drop shadow — NOT difference-blended:
   difference of white over a mid-tone frame lands on mid-grey, which
   made the rail vanish over the bright daylight chapters. */

.steps {
  position: fixed;
  right: max(26px, env(safe-area-inset-right));
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 9px;
  pointer-events: none;
  z-index: 12;                    /* above the thesis wash — see .brand */
}

.steps__item {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0;
  padding: 10px 0 10px 18px;      /* generous hit area, invisible */
  background: none;
  border: 0;
  pointer-events: auto;
  cursor: pointer;
  color: #fff;
  opacity: 0.6;
  transition: opacity 0.3s ease;
  -webkit-tap-highlight-color: transparent;
}

/* Tick line that grows toward the number as the item takes focus. */
.steps__item::before {
  content: "";
  width: 19px;
  height: 2.5px;
  background: currentColor;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
  transition: width 0.3s ease;
}

.steps__item:hover,
.steps__item:focus-visible {
  opacity: 0.85;
}

.steps__item:focus-visible {
  outline: 1px solid rgba(255, 255, 255, 0.7);
  outline-offset: 3px;
}

.steps__item.is-current {
  opacity: 1;
}

.steps__item.is-current::before {
  width: 40px;
}

.steps__num {
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.18em;
  line-height: 1;
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.6),
    0 2px 12px rgba(0, 0, 0, 0.55);
}

/* Narrow screens: the rail hugs the very edge as bare numbers — ticks
   go, the pitch opens into a real touch target, and the strip stays
   clear of the chapter cards, which all stop short of it. */
@media (max-width: 700px) {
  .steps {
    right: 6px;
    gap: 2px;
  }
  .steps__item {
    padding: 10px 2px 10px 11px;
  }
  .steps__item::before {
    display: none;
  }
  .steps__num {
    font-size: 12px;
  }
}

/* ---------- Scroll cue ---------- */

.cue {
  /* Bottom-right corner — never sits on the film's own typography. */
  position: fixed;
  right: max(26px, env(safe-area-inset-right));
  bottom: max(28px, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  pointer-events: none;
  z-index: 12;                    /* above the thesis wash — see .brand */
  mix-blend-mode: difference;
  opacity: 1;
  transition: opacity 0.5s ease, visibility 0s;
}

/* visibility flips only after the fade-out completes, so the hide
   still eases — but once hidden the element stops compositing
   entirely (an opacity-0 difference-blend with a running keyframe
   otherwise forces a backdrop readback every frame, forever). The
   un-delayed transition on the base state makes re-shows instant. */
.cue.is-hidden {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.5s ease, visibility 0s linear 0.5s;
}

.cue.is-hidden .cue__line {
  animation-play-state: paused;
}

.cue__label {
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: #fff;
}

.cue__line {
  width: 1px;
  height: 32px;
  background: linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0));
  transform-origin: top center;
  animation: cue-drop 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

@keyframes cue-drop {
  0%   { transform: scaleY(0);   opacity: 0; }
  35%  { transform: scaleY(1);   opacity: 1; }
  75%  { transform: scaleY(1);   opacity: 1; }
  100% { transform: scaleY(1);   opacity: 0; }
}

/* ---------- Buffering spinner ---------- */

.spinner {
  position: fixed;
  left: max(24px, env(safe-area-inset-left));
  bottom: max(24px, env(safe-area-inset-bottom));
  width: 18px;
  height: 18px;
  border: 1.5px solid rgba(255, 255, 255, 0.25);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
  z-index: 15;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ---------- Thesis text sections ---------- */
/* Cinematic title-card system. Five fully distinct compositions, not
   one template restyled five times, because a director blocks every
   shot differently: where the subject sits, how much light the frame
   carries, what is left over to hold type. No panel, no boxed edge — a
   directional wash that fades to fully clear well before the far side
   of the frame, a giant soft-focus chapter numeral bled off one corner
   (the classic film chapter-card device), a target-lock corner bracket
   that snaps in with the kicker, and a genuine display face reserved
   for the one line in the whole film meant to hit hardest. */

.thesis {
  position: fixed;
  inset: 0;
  z-index: 11;
  pointer-events: none;
  /* Full-viewport root: paint containment clips nothing, and the
     entrance cascade's invalidations stay scoped to the card. */
  contain: layout style paint;
  opacity: 0;
  visibility: hidden;
  font-family: "Earlgos", "Helvetica Neue", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  --accent: rgba(255, 255, 255, 0.85);
  --accent-soft: rgba(255, 255, 255, 0.72);
  --numeral-opacity: 0;
  /* Strength of the backdrop wash painted by .thesis__scrim below. */
  --wash-mid: 0.14;
  --wash-out: 0.38;
  --wash-edge: 0.55;
  --wash-flat: 0.24;
  /* Shared measure for the poster card: the headline sets its type from
     it and the subhead and rule below take their width from it, so the
     three always stop together. Breakpoints re-point this one value. */
  --headline-size: clamp(30px, 5.4vw, 104px);
  /* Exit is deliberately quick — the film never waits on the text; it
     drops away while the next clip is already rolling. The unhurried
     0.7s entrance lives on .is-shown below. */
  transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}

.thesis.is-shown {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.7s ease;
}

/* The same grain the wash carries, run over the letterforms too —
   painted after the inner block, so the noise sits on the type and the
   text reads as printed into the shot rather than laid over it. */
.thesis::after {
  content: "";
  position: absolute;
  inset: 0;
  /* the same feTurbulence grain (baseFrequency .82, 3 octaves), rendered
     once into a 256px tile — a full-viewport SVG raster otherwise
     re-rasterizes on every resize/dvh change */
  background-image: url("assets/grain.png");
  background-repeat: repeat;
  background-size: 256px 256px;
  opacity: 0.055;
  mix-blend-mode: overlay;
}

/* No wash: all darkening lives in the feathered frost pools behind the
   text blocks — the footage itself is never dimmed. The scrim element
   stays only to carry the film grain below. */
.thesis__scrim {
  position: absolute;
  inset: 0;
  /* The backdrop grade that seats the frame while text holds it: a
     vignette — gentle over the middle, deepening toward every edge —
     over a flat dim. Plain paint on purpose: it reads on machines
     where backdrop-filter never runs (GPU off, reduced transparency),
     so the "background image" feel survives everywhere. It rides the
     section's own fade and clears the moment the film moves.

     This wash — not the 1.25-1.8px lens defocus on the video — is what
     actually pushes a held frame back: measured against the raw frame
     it moves 95% of the pixels, where the whole video filter moves 6%.
     So its strength lives in variables, and a chapter that wants a
     lighter hand overrides them below. */
  background:
    radial-gradient(120% 90% at 50% 42%,
      rgba(2, 3, 6, var(--wash-mid)) 38%,
      rgba(2, 3, 6, var(--wash-out)) 74%,
      rgba(2, 3, 6, var(--wash-edge)) 100%),
    rgba(2, 3, 6, var(--wash-flat));
}

/* The chapter numeral: huge, soft-focus, bled off a corner like an
   old-fashioned film chapter card. Content and placement are set per
   chapter below; a chapter that never gets a content value (04, the
   finale) simply renders nothing. */
.thesis__scrim::before {
  content: "";
  position: absolute;
  font-family: "Baygo", "Anton", "Arial Narrow", sans-serif;
  font-weight: 400;
  line-height: 0.74;
  letter-spacing: -0.015em;
  color: var(--accent);
  white-space: nowrap;
  user-select: none;
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 1.6s ease 0.1s, transform 1.8s cubic-bezier(0.16, 1, 0.3, 1) 0.1s;
}

.thesis.is-shown .thesis__scrim::before {
  opacity: var(--numeral-opacity);
  transform: scale(1);
}

/* Fine grain, so the wash sits in the photography instead of over it. */
.thesis__scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  /* the same feTurbulence grain (baseFrequency .82, 3 octaves), rendered
     once into a 256px tile — a full-viewport SVG raster otherwise
     re-rasterizes on every resize/dvh change */
  background-image: url("assets/grain.png");
  background-repeat: repeat;
  background-size: 256px 256px;
  opacity: 0.04;
  mix-blend-mode: overlay;
}

/* Positioned entirely per chapter below — this carries only what
   every layout shares. */
.thesis__inner {
  position: absolute;
}

/* Edge rule: a hairline that draws out on entrance, the way a title
   card is ruled off. Left-edge by default; mirrored, rotated into a
   top rule, or removed entirely per chapter below. */
.thesis__inner::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(
    to bottom,
    var(--accent-soft) 0%,
    rgba(255, 255, 255, 0.22) 62%,
    rgba(255, 255, 255, 0) 100%
  );
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 0.9s cubic-bezier(0.2, 0.6, 0.2, 1) 0.06s;
}

.thesis.is-shown .thesis__inner::before {
  transform: scaleY(1);
}

/* Per-element reveal: rise and fade. Opacity and transform only — both
   run on the compositor. The old focus-in blur is gone: animating a
   filter re-rasterizes the element every frame while the film is still
   playing behind it, real work on an integrated GPU. */
.thesis__inner > * {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity 0.7s ease,
    transform 0.85s cubic-bezier(0.2, 0.6, 0.2, 1);
}

.thesis.is-shown .thesis__inner > * {
  opacity: 1;
  transform: translateY(0);
}

/* Cascade the reveal down the block (kicker, then each line, then the
   finale tagline). Delays apply only while shown, so the block hides
   as one when the film moves on. */
.thesis.is-shown .thesis__inner > *:nth-child(1) { transition-delay: 0.06s; }
.thesis.is-shown .thesis__inner > *:nth-child(2) { transition-delay: 0.17s; }
.thesis.is-shown .thesis__inner > *:nth-child(3) { transition-delay: 0.28s; }
.thesis.is-shown .thesis__inner > *:nth-child(4) { transition-delay: 0.39s; }
.thesis.is-shown .thesis__inner > *:nth-child(5) { transition-delay: 0.50s; }
.thesis.is-shown .thesis__inner > *:nth-child(6) { transition-delay: 0.61s; }
.thesis.is-shown .thesis__inner > *:nth-child(7) { transition-delay: 0.72s; }

.thesis__kicker {
  position: relative;
  display: flex;
  align-items: center;
  gap: 15px;
  margin: 0 0 clamp(20px, 2.4vh, 30px);
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(10px, 0.78vw, 12px);
  font-weight: 500;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--accent-soft);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.7);
}

/* Target-lock corner bracket: snaps in with the kicker like a reticle
   acquiring its mark. Mirrored to the opposite corner, or replaced
   with a flanking rule, per chapter below. */
.thesis__kicker::before {
  content: "";
  flex: 0 0 auto;
  width: 13px;
  height: 13px;
  border-left: 1.5px solid currentColor;
  border-top: 1.5px solid currentColor;
  opacity: 0;
  transform: scale(0.4);
  transform-origin: center;
  transition: transform 0.55s cubic-bezier(0.16, 1, 0.3, 1) 0.15s, opacity 0.4s ease 0.15s;
}

.thesis.is-shown .thesis__kicker::before {
  opacity: 0.85;
  transform: scale(1);
}

/* Poster headline: one word per line, stacked tight enough that the
   words read as a single mass rather than three lines. The colour
   alternation and the exact size are set per chapter, because only the
   cold open is blocked this way. */
.thesis__headline {
  margin: 0 0 clamp(15px, 2vh, 26px);
  font-family: "Baygo", "Anton", "Arial Narrow", sans-serif;
  font-weight: 400;
  /* Held in a variable, not written straight into font-size, because
     the subhead and the rule below are measured off the headline's own
     width — the card reads as one object only while all three keep the
     same measure. Every breakpoint re-points this one value. */
  font-size: var(--headline-size);
  line-height: 0.85;
  letter-spacing: 0.006em;
  text-transform: uppercase;
}

.thesis__word {
  display: block;
  color: var(--headline-bone, #fff);
  /* Soft, low-alpha shadows: a hard dark edge under bright type is what
     makes it read as pasted on rather than photographed in. */
  text-shadow:
    0 2px 6px rgba(0, 0, 0, 0.32),
    0 8px 48px rgba(0, 0, 0, 0.55);
}

/* First and third word carry the accent, the middle one holds bone —
   the alternation is what makes the stack read as a poster. */
.thesis__word:nth-child(odd) {
  color: var(--headline-accent, #fff);
  text-shadow:
    0 2px 6px rgba(0, 0, 0, 0.34),
    0 0 38px var(--headline-glow, transparent),
    0 8px 48px rgba(0, 0, 0, 0.55);
}

/* The stack reveals a word at a time, like consecutive cuts, so the
   container itself sits still and the words carry the entrance. */
.thesis__inner > .thesis__headline {
  opacity: 1;
  transform: none;
  filter: none;
  transition: none;
}

.thesis__word {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 0.7s ease,
    transform 0.85s cubic-bezier(0.2, 0.6, 0.2, 1);
}

/* Rests at 0.9, not 1: a breath of the frame lives inside the letters,
   which is what seats display type into footage. */
.thesis.is-shown .thesis__word {
  opacity: 0.96;
  transform: translateY(0);
}

.thesis.is-shown .thesis__word:nth-child(1) { transition-delay: 0.06s; }
.thesis.is-shown .thesis__word:nth-child(2) { transition-delay: 0.16s; }
.thesis.is-shown .thesis__word:nth-child(3) { transition-delay: 0.26s; }
.thesis.is-shown .thesis__word:nth-child(4) { transition-delay: 0.36s; }

/* Tracked-out line under the headline: condensed caps, opened up so it
   reads as a strapline against the solid mass above it. */
.thesis__subhead {
  /* Held to a reading measure rather than the headline's full width, so
     a long strapline wraps into balanced lines inside the dark corner
     instead of running on across whatever the frame is holding. */
  max-width: max(calc(var(--headline-size) * 6), 340px);
  /* Same rhythm as line-to-line: the strapline and the copy read as
     one small-text section, not two blocks. */
  margin: 0 0 clamp(8px, 1.1vh, 14px);
  /* The nav bar's face in sentence case, at the same size as the
     chapter copy, so the two small texts read as one voice. */
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(11px, 1.05vw, 17px);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0.06em;
  text-transform: none;
  text-wrap: balance;
  color: rgba(255, 255, 255, 0.92);
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.4),
    0 2px 24px rgba(0, 0, 0, 0.6);
}

/* The ruled line between the headline and the strapline: one solid
   stroke, stopping well short of the frame's subject. */
.thesis__rule {
  height: 2px;
  max-width: calc(var(--headline-size) * 4.5);
  margin: 0 0 clamp(11px, 1.4vh, 17px);
  background: var(--rule-colour, #fff);
  box-shadow: 0 0 16px var(--rule-glow, transparent);
}

/* Drawn out from the left rather than risen, like a rule being ruled. */
.thesis__inner > .thesis__rule {
  transform: scaleX(0);
  transform-origin: left center;
}

.thesis.is-shown .thesis__inner > .thesis__rule {
  transform: scaleX(1);
}

/* Body copy. Always pure white regardless of a chapter's accent —
   legibility is never negotiable. */
.thesis__line {
  margin: 0 0 clamp(8px, 1.1vh, 14px);
  /* The nav bar's own face, reading in sentence case — as written. */
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(14px, 1.7vw, 27px);
  line-height: 1.3;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: rgba(255, 255, 255, 0.95);
  text-wrap: pretty;
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.38),
    0 2px 34px rgba(0, 0, 0, 0.6);
}

/* The closing statement. A genuine display face, reserved for the one
   line in the whole film meant to carry the frame by itself. */
.thesis__tagline {
  margin: clamp(26px, 3.6vh, 46px) 0 0;
  font-family: "Baygo", "Anton", "Arial Narrow", sans-serif;
  font-size: clamp(30px, 5.8vw, 84px);
  line-height: 0.98;
  font-weight: 400;
  letter-spacing: 0.012em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
  text-shadow:
    0 2px 5px rgba(0, 0, 0, 0.32),
    0 6px 54px rgba(0, 0, 0, 0.55);
}

/* ==================================================================
   Per-chapter composition. The five blocks are the only <section>
   elements on the page, generated in chapter order, so :nth-of-type
   maps one-to-one onto the frames — no DOM changes needed to give
   every chapter its own blocking, wash, and accent.
   ================================================================== */

/* ==================================================================
   Poster blocking. Every chapter that opens on a stacked display
   headline shares one composition — top-left, no numeral, no left
   hairline, the argument stepped back under the card — so it is
   written once here and each chapter below supplies only its own wash.
   ================================================================== */

.thesis--poster {
  --accent: rgba(255, 96, 102, 0.85);
  --accent-soft: rgba(255, 206, 208, 0.84);
  --headline-accent: #c1161f;
  --cta-bg: #c1161f;                /* the CTA plate, in the chapter's red */
  --cta-hover: #e01f2a;             /* the plate's colour, stronger on hover */
  --cta-glow: rgba(193, 22, 31, 0.26);  /* the plate's own hover glow */
  --headline-bone: #dcdcde;
  --headline-glow: rgba(193, 22, 31, 0.26);
  --rule-colour: rgba(193, 22, 31, 0.7);
  --rule-glow: rgba(193, 22, 31, 0.3);
}

.thesis--poster .thesis__inner {
  left: clamp(26px, 5vw, 100px);
  right: auto;
  top: clamp(58px, 9vh, 112px);
  bottom: auto;
  /* fit-content: the box hugs the text, so the frost pool behind it
     covers exactly the card and never a wide empty area. */
  width: fit-content;
  max-width: min(900px, calc(100vw - clamp(52px, 9vw, 200px)));
  padding-left: 0;
  padding-right: 0;
  text-align: left;
}

/* The headline is the mark, so nothing else marks the chapter: the
   numeral would sit exactly where the stack lands. */
.thesis--poster .thesis__scrim::before {
  content: none;
}

/* The pool: every chapter's only backing. A feathered patch of gentle
   dark sitting exactly behind the text block — never the full image.
   Plain tint, no backdrop-filter: frosted glass over video re-blurs
   the moving footage behind it on every frame for as long as the card
   is up — the single heaviest steady cost the page had, and exactly
   the kind of load that hitches an integrated GPU. */
.thesis--poster .thesis__inner::before {
  content: "";
  display: block;
  position: absolute;
  z-index: -1;
  left: clamp(-70px, -5vw, -36px);
  right: clamp(-70px, -5vw, -36px);
  top: clamp(-70px, -5vw, -36px);
  bottom: clamp(-70px, -5vw, -36px);
  width: auto;
  height: auto;
  background: rgba(2, 3, 6, 0.19);
  transform: none;
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 14%, #000 86%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%),
    linear-gradient(180deg, transparent 0, #000 14%, #000 86%, transparent 100%);
  mask-composite: intersect;
}

/* The argument runs under the card, stepped well back so the stack
   above it stays the thing you read first. */
.thesis--poster .thesis__line {
  max-width: 46ch;
  font-size: clamp(11px, 1.05vw, 17px);
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.95);
}

/* Re-timed for this order: the three headline words carry 0.06–0.26s
   on their own, so everything below picks up after them. */
.thesis--poster.is-shown .thesis__inner > *:nth-child(2) { transition-delay: 0.40s; }
.thesis--poster.is-shown .thesis__inner > *:nth-child(3) { transition-delay: 0.50s; }
.thesis--poster.is-shown .thesis__inner > *:nth-child(4) { transition-delay: 0.60s; }
.thesis--poster.is-shown .thesis__inner > *:nth-child(5) { transition-delay: 0.69s; }
.thesis--poster.is-shown .thesis__inner > *:nth-child(6) { transition-delay: 0.78s; }
.thesis--poster.is-shown .thesis__inner > *:nth-child(7) { transition-delay: 0.87s; }
.thesis--poster.is-shown .thesis__inner > *:nth-child(8) { transition-delay: 0.96s; }

/* ==================================================================
   Supporting metrics column. Rides the right edge alongside the
   chapter rail rather than trailing the copy, so the numbers read
   as instrumentation on the frame instead of a tail on the text.
   Fixed to the viewport and vertically centred on the same axis as
   .steps, seated just inboard of it. Chapter 04 is the exception and
   mirrors to the left edge (its card is the one anchored right); the
   override sits with that chapter's own blocking below. Each stat
   carries a numeral, a one-line claim it backs, and the primary
   source. Reflows beneath the copy on narrower screens (below).
   ================================================================== */

.thesis__stats {
  position: fixed;
  left: auto;
  /* Inboard of the .steps rail: that rail sits at right:26px and
     runs ~95px wide (tick, gap, numeral), reaching ~121px in from
     the edge, so the column stops clear of it rather than
     crowding the ticks. */
  right: clamp(140px, 10.5vw, 176px);
  /* Centred by auto margins between a top and bottom of zero, not by
     a translate: the card's entrance cascade sets a transform on every
     direct child of .thesis__inner at a specificity this rule cannot
     reach, and it would take the centring with it. */
  top: 0;
  bottom: 0;
  height: fit-content;
  margin: auto 0;
  display: flex;
  flex-direction: column;
  gap: clamp(18px, 2.4vh, 30px);
  width: clamp(150px, 13vw, 210px);
  pointer-events: none;
}

/* The aside is a positioning container only — it sits out the card's
   entrance entirely, at a specificity that covers the cascade's own
   is-shown rules. The stats inside carry the reveal. */
.thesis__inner > .thesis__stats,
.thesis.is-shown .thesis__inner > .thesis__stats,
.thesis > .thesis__stats,
.thesis.is-shown > .thesis__stats {
  opacity: 1;
  transform: none;
  transition: none;
}

.thesis__stat {
  display: flex;
  flex-direction: column;
  gap: 6px;
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 0.6s ease,
    transform 0.7s cubic-bezier(0.2, 0.6, 0.2, 1);
}

.thesis.is-shown .thesis__stat {
  opacity: 1;
  transform: translateY(0);
}

/* Stagger after the card cascade. The card's last child lands at
   0.96s (poster with subhead) or 0.78s (without); first stat at
   1.05s — a beat after the card settles. */
.thesis.is-shown .thesis__stat:nth-child(1) { transition-delay: 1.05s; }
.thesis.is-shown .thesis__stat:nth-child(2) { transition-delay: 1.20s; }
.thesis.is-shown .thesis__stat:nth-child(3) { transition-delay: 1.35s; }

/* Stepped chapters hold every number back until the plate under the
   copy names it and is clicked, so there is no cascade and no delay
   here — each figure arrives on its own beat. The blocks keep their
   boxes the whole time, so the column is measured once and nothing
   already on screen moves as the rest of it fills in. */
.thesis--stepped.is-shown .thesis__stat {
  opacity: 0;
  transform: translateY(14px);
}

.thesis--stepped.is-shown .thesis__stat:nth-child(1),
.thesis--stepped.is-shown .thesis__stat:nth-child(2),
.thesis--stepped.is-shown .thesis__stat:nth-child(3) {
  transition-delay: 0s;
}

.thesis--stepped.is-shown .thesis__stat.is-revealed {
  opacity: 1;
  transform: translateY(0);
}

.thesis__stat-value {
  font-family: "Baygo", "Anton", "Arial Narrow", sans-serif;
  font-weight: 400;
  font-size: clamp(28px, 3.6vw, 52px);
  line-height: 0.9;
  letter-spacing: 0.004em;
  color: var(--headline-bone, #dcdcde);
  text-shadow:
    0 2px 6px rgba(0, 0, 0, 0.32),
    0 8px 48px rgba(0, 0, 0, 0.55);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.thesis__stat-label {
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(9px, 0.74vw, 11px);
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
  text-shadow:
    0 1px 3px rgba(0, 0, 0, 0.4),
    0 2px 24px rgba(0, 0, 0, 0.6);
}

.thesis__stat-source {
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(8px, 0.62vw, 10px);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
  margin-top: 2px;
}

/* ==================================================================
   Per-chapter wash. Only the grade differs between the two poster
   chapters — the blocking above is shared.
   ================================================================== */

/* 01 and 02 hold their frame with a lighter hand than the rest: the
   backdrop wash eases back a quarter, so the footage stays nearer the
   front while the copy is up. These two are also listed in
   CONFIG.heldExemptStages, so their frame keeps its focus entirely —
   this wash is the only thing seating the type on them. */
.thesis:nth-of-type(1),
.thesis:nth-of-type(2) {
  --wash-mid: 0.105;
  --wash-out: 0.285;
  --wash-edge: 0.41;
  --wash-flat: 0.18;
}

/* 01 — the cold open: default poster blocking, one line of copy. */
.thesis:nth-of-type(1) .thesis__line {
  max-width: 34ch;              /* a narrower reading column under the card */
}

/* Every chapter shares one palette — the red set on .thesis--poster
   above (the CTA plate is the one exception, kept black). Chapters
   02-05 used to override it with the old footage's blue; with the
   poster artwork there is one accent for the whole film, so the
   override is gone and no chapter can drift off-palette. */

/* 02 — neon city. A pure title card — headline, typed strapline, rule
   — vertically centred on the left, type set left, clear of the C sign
   and its figure in the centre. */
/* 02 blocks exactly like the cold open — same corner, same measure;
   only the reading colour steps up a touch against the neon city. */
.thesis:nth-of-type(2) .thesis__subhead {
  max-width: 34ch;
  letter-spacing: normal;
  color: rgba(255, 255, 255, 0.95);
}

/* 03 — full daylight. Default poster blocking: header top left, the
   copy directly beneath it, set left, over its own frost pool. */
/* The long-fade pool, shared by every chapter that showed a rim: more
   body behind the type, and edges that dissolve over a long eased run
   — the ramp holds most of the patch, so the dark reads as a grade
   settling into the frame rather than a card with a visible rim. (The
   default short-ramp mask above read as a soft-edged square on these
   frames.) Chapters 1 and 4 keep the base tint; 03 and 05 set their
   own body below. */
.thesis:nth-of-type(1) .thesis__inner::before,
.thesis:nth-of-type(3) .thesis__inner::before,
.thesis:nth-of-type(4) .thesis__inner::before,
.thesis:nth-of-type(5) .thesis__inner::before {
  left: clamp(-190px, -11vw, -90px);
  right: clamp(-190px, -11vw, -90px);
  top: clamp(-170px, -10vw, -80px);
  bottom: clamp(-170px, -10vw, -80px);
  -webkit-mask-image:
    linear-gradient(90deg, transparent 0, rgba(0, 0, 0, 0.3) 10%, rgba(0, 0, 0, 0.75) 19%, #000 28%, #000 72%, rgba(0, 0, 0, 0.75) 81%, rgba(0, 0, 0, 0.3) 90%, transparent 100%),
    linear-gradient(180deg, transparent 0, rgba(0, 0, 0, 0.3) 10%, rgba(0, 0, 0, 0.75) 19%, #000 28%, #000 72%, rgba(0, 0, 0, 0.75) 81%, rgba(0, 0, 0, 0.3) 90%, transparent 100%);
  mask-image:
    linear-gradient(90deg, transparent 0, rgba(0, 0, 0, 0.3) 10%, rgba(0, 0, 0, 0.75) 19%, #000 28%, #000 72%, rgba(0, 0, 0, 0.75) 81%, rgba(0, 0, 0, 0.3) 90%, transparent 100%),
    linear-gradient(180deg, transparent 0, rgba(0, 0, 0, 0.3) 10%, rgba(0, 0, 0, 0.75) 19%, #000 28%, #000 72%, rgba(0, 0, 0, 0.75) 81%, rgba(0, 0, 0, 0.3) 90%, transparent 100%);
}

.thesis:nth-of-type(3) .thesis__inner::before {
  background: rgba(2, 3, 6, 0.28);
}

/* 05 — a slight body behind the closing card, same dissolving edges. */
.thesis:nth-of-type(5) .thesis__inner::before {
  background: rgba(2, 3, 6, 0.26);
}

/* 04 — the card sits right of frame, anchored right, set left, over
   the shared frost pool. The copy wraps in the shared poster column
   (46ch, from .thesis--poster .thesis__line): the old one-sentence-
   per-line blocking was built for short sentences and ran the longer
   brief clean across the frame. */
.thesis:nth-of-type(4) .thesis__inner {
  left: auto;
  right: clamp(26px, 5vw, 100px);
  top: clamp(58px, 9vh, 112px);
  width: fit-content;
  max-width: min(900px, calc(100vw - clamp(52px, 9vw, 200px)));
  text-align: left;                 /* set left inside the same footprint */
}

/* The REVEAL plate sits under the copy, its left edge flush with the
   line boxes above it. */
.thesis:nth-of-type(4) .thesis__cta--flow {
  align-self: flex-start;
}

/* 04 is the only chapter whose card is anchored right, so its metrics
   column mirrors to the opposite edge — held at the shared placement
   it would sit under the copy. The inset matches the one the card
   takes from its own side, measured from the left instead, which
   clears the frost pool at every width the side column survives
   (above 700px it reflows under the copy). Vertical centring, width
   and the reveal are all inherited untouched. */
.thesis:nth-of-type(4) .thesis__stats {
  right: auto;
  left: clamp(26px, 5vw, 100px);
}

/* 00 — the opening screen. It carries the plate and nothing else, so
   every layer that seats type on a frame is switched off: no wash, no
   grain, no edge rule. The logo frame must read exactly as it does
   with no card over it at all. The plate is black here rather than the
   chapter red, so it sits on the mark instead of competing with it. */
.thesis--opening {
  --wash-mid: 0;
  --wash-out: 0;
  --wash-edge: 0;
  --wash-flat: 0;
  --cta-bg: #0b0b0d;
  --cta-hover: #2b2b30;
  --cta-glow: rgba(0, 0, 0, 0.35);
}

.thesis--opening::after,
.thesis--opening .thesis__scrim::after,
.thesis--opening .thesis__inner::before {
  content: none;
}

/* Centred on the frame and sat low, so the mark has room to breathe
   above the plate rather than the two reading as one stacked object. */
.thesis--opening .thesis__inner {
  left: 50%;
  right: auto;
  top: auto;
  bottom: clamp(38px, 7vh, 92px);
  width: fit-content;
  transform: translateX(-50%);
  text-align: center;
}

/* 05 — the finale. The closing card sits right of frame, vertically
   centred, type centred, over the shared frost pool. */
/* The poster line measure is built for a left-set column; a centred
   card needs the paragraph box itself centred, so the cap comes off
   and the inner's own width holds the measure. */
.thesis--center .thesis__line {
  max-width: none;
}

/* The finale keeps its frame live: no vignette grade, no defocus (the
   engine exempts it), just the faintest settle so the type reads. */
.thesis:nth-of-type(5) .thesis__scrim {
  background: rgba(2, 3, 6, 0.1);
}

/* ==================================================================
   The chapter call-to-action: an outlined plate, label tracked wide,
   a solid little arrow — and on hover the black plate lifts a shade,
   the type staying white. One element, two placements.
   ================================================================== */
.thesis__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 2.1vh, 23px) clamp(34px, 3.4vw, 58px);
  border: 1.5px solid var(--cta-bg, #fff);
  border-radius: 0;
  /* The plate is its own colour, independent of the headline accent, so
     the type can be red while the button stays black. Hover deepens it,
     lifts a soft glow off it and grows the plate a hair.

     It starts EMPTY: the plate itself carries only a faint scrim so the
     label stays legible over the frame, and .thesis__cta-fill below
     paints the real colour in as the chapter downloads. */
  position: relative;
  overflow: hidden;
  background: rgba(2, 3, 6, 0.28);
  color: rgba(255, 255, 255, 0.95);
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(10px, 0.63vw, 12px);
  font-weight: 500;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  cursor: pointer;
  pointer-events: auto;             /* the section around it stays gesture-transparent */
  transition: background-color 0.35s ease, box-shadow 0.35s ease, scale 0.35s ease;
}

/* The gauge: the plate's real colour, wiped in from the left as the
   chapter behind the button downloads. The engine sets --cta-fill from
   0 to 1. Scaled rather than widened, so the growth runs on the
   compositor and never lays the button out mid-fill. */
.thesis__cta-fill {
  position: absolute;
  inset: 0;
  background: var(--cta-bg, #333);
  transform: scaleX(var(--cta-fill, 0));
  transform-origin: left center;
  transition: transform 0.3s linear, background-color 0.35s ease;
  pointer-events: none;
}

/* Locked while its chapter is still arriving. */
.thesis__cta:disabled {
  cursor: progress;
}

.thesis__cta-label {
  position: relative;               /* above the gauge */
  text-indent: 0.35em;              /* re-centres letter-spaced caps */
}

.thesis__cta-arrow {
  position: relative;               /* above the gauge */
  width: 8px;
  height: 10px;
  margin-left: clamp(12px, 1.2vw, 20px);
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.2, 0.6, 0.2, 1);
}

/* Hover belongs to a plate that can actually be used. */
.thesis__cta:hover:not(:disabled),
.thesis__cta:focus-visible:not(:disabled) {
  border-color: var(--cta-hover, #fff);
  box-shadow: 0 0 30px var(--cta-glow, rgba(255, 255, 255, 0.3));
  /* `scale`, not `transform`: the edge plate's centring lives in its
     transform and the entrance animates it. */
  scale: 1.04;
  outline: none;
}

.thesis__cta:hover:not(:disabled) .thesis__cta-fill,
.thesis__cta:focus-visible:not(:disabled) .thesis__cta-fill {
  background: var(--cta-hover, #fff);
}

.thesis__cta:hover:not(:disabled) .thesis__cta-arrow,
.thesis__cta:focus-visible:not(:disabled) .thesis__cta-arrow {
  transform: translateX(3px);
}

/* Every chapter: the plate flows beneath the copy, flush with the text
   block's own left edge, and rides the inner cascade like any other
   child — but its hover fill must fire instantly, so the full
   transition list is spelled out here and the cascade's per-child delay
   is re-scoped below to the entrance properties only. */
.thesis__cta--flow {
  margin-top: clamp(18px, 2.6vh, 30px);
  transition:
    background-color 0.35s ease,
    border-color 0.35s ease,
    box-shadow 0.35s ease,
    scale 0.35s ease,
    opacity 0.7s ease,
    transform 0.85s cubic-bezier(0.2, 0.6, 0.2, 1),
    filter 0.85s ease;
}

.thesis--poster.is-shown .thesis__inner > .thesis__cta--flow {
  transition-delay: 0s, 0s, 0s, 0s, 0.6s, 0.6s, 0.6s;
}

/* ------------------------------------------------------------------
   Stepped chapters. The in-flow plate names the next figure rather
   than the next chapter, and the film control moves to the foot of
   the metrics column.
   ------------------------------------------------------------------ */

/* Held to one width so the plate itself never jitters as its label
   steps from one figure to the next. The card is fit-content on a
   46ch measure, so this is never the widest thing in it. */
.thesis__cta--step {
  min-width: clamp(200px, 16vw, 260px);
}

/* Spent: every figure is up, so the stepper has nothing left to name.
   It goes, but its box stays — the frost pool is sized to the card, so
   collapsing the plate would resize the pool under the copy. */
.thesis--stepped.is-shown .thesis__inner > .thesis__cta--step.is-spent {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.4s ease, visibility 0s linear 0.4s;
}

/* The plate that actually plays the next chapter: last in the metrics
   column and stretched across it, so its edges sit flush under the
   numbers above. Its box is reserved from the start — the column is
   centred on the viewport, and a plate appearing into the flow would
   otherwise push every figure up as it arrived. Hidden, and out of the
   tab order, until the last figure lands. */
.thesis__cta--rail {
  align-self: stretch;
  margin-top: clamp(10px, 1.8vh, 20px);
  /* Height and type size match the in-flow plate exactly. The side
     padding deliberately does not: this plate is stretched to the
     column, so its width comes from the column and that padding only
     narrows the label's own box — the in-flow value would clip a label
     like "The Solution" once the column reaches its 150px floor. */
  padding: clamp(16px, 2.1vh, 23px) clamp(10px, 1vw, 16px);
  font-size: clamp(10px, 0.63vw, 12px);
  letter-spacing: 0.18em;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transform: translateY(14px);
  transition:
    background-color 0.35s ease,
    border-color 0.35s ease,
    box-shadow 0.35s ease,
    scale 0.35s ease,
    opacity 0.6s ease,
    transform 0.7s cubic-bezier(0.2, 0.6, 0.2, 1),
    visibility 0s linear 0.6s;
}

/* Tracking is tighter here than on the in-flow plate, so the label's
   own re-centring and the arrow's gap step back with it. */
.thesis__cta--rail .thesis__cta-label {
  text-indent: 0.18em;
}

.thesis__cta--rail .thesis__cta-arrow {
  margin-left: clamp(8px, 0.8vw, 14px);
}

.thesis__cta--rail.is-revealed {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
}

.thesis--center .thesis__inner {
  left: auto;
  /* Seated a step further in from the right edge than the rail it
     shares that side with, so the closing card reads as centred-ish
     rather than pinned to the margin. */
  right: clamp(38px, 8.6vw, 160px);
  top: 50%;                         /* unpin the poster's top anchor */
  bottom: auto;
  width: fit-content;
  max-width: min(720px, calc(100vw - clamp(52px, 9vw, 200px)));
  padding-left: 0;
  padding-right: 0;
  transform: translateY(-50%);
  text-align: center;               /* centred type; the block holds its spot */
}

/* The final card occupies the right half of the frame. Its detached
   metrics column mirrors to the left so the two blocks never collide. */
.thesis--center > .thesis__stats {
  right: auto;
  left: clamp(26px, 5vw, 100px);
}

/* Eleven rest positions still fit at common desktop heights. Compress
   only short viewports so the complete chapter rail remains reachable. */
@media (max-height: 640px) {
  .steps {
    gap: 2px;
  }

  .steps__item {
    padding-top: 6px;
    padding-bottom: 6px;
  }
}

.thesis--center .thesis__kicker {
  justify-content: center;
}

.thesis--center .thesis__kicker::before {
  width: clamp(20px, 2.6vw, 40px);
  height: 1px;
  border: none;
  background: currentColor;
  transform-origin: right center;
}

.thesis--center.is-shown .thesis__kicker::before {
  opacity: 0.7;
}

.thesis--center .thesis__kicker::after {
  content: "";
  flex: 0 0 auto;
  width: clamp(20px, 2.6vw, 40px);
  height: 1px;
  background: currentColor;
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.7s cubic-bezier(0.2, 0.6, 0.2, 1) 0.2s, opacity 0.4s ease 0.2s;
}

.thesis--center.is-shown .thesis__kicker::after {
  opacity: 0.7;
  transform: scaleX(1);
}

/* Suppress the giant chapter numeral on small or short screens, where
   it would overwhelm the composition rather than sit behind it. */
@media (max-width: 700px), (max-height: 640px) {
  .thesis__scrim::before {
    display: none;
  }
}

/* Tall, narrow screens (tablet portrait): width-based sizing alone
   under-scales the type on a big display, so give it its own step. */
@media (min-width: 700px) and (max-width: 1100px) and (min-height: 900px) {
  .thesis__line {
    font-size: clamp(17px, 2.4vw, 25px);
  }
  .thesis__tagline {
    font-size: clamp(40px, 6.6vw, 76px);
  }
}

/* Narrow screens: the headline's longest word sets the ceiling, so it
   is sized off the viewport directly rather than left to the desktop
   clamp, which would run it past the edge. */
@media (max-width: 700px) {
  .thesis {
    --headline-size: min(9.8vw, 46px);
  }
}

/* Short viewports: hold the same composition, step the type back so a
   five-line block never runs past the frame. */
@media (max-height: 720px) {
  .thesis__line {
    font-size: clamp(15px, 1.65vw, 23px);
    line-height: 1.26;
  }
  .thesis__tagline {
    font-size: clamp(28px, 4vw, 56px);
  }
  .thesis {
    --headline-size: clamp(26px, 4.3vw, 68px);
  }
  .thesis--poster .thesis__line {
    font-size: clamp(10px, 0.9vw, 14px);
    line-height: 1.34;
  }
  /* Straplines are sized to the poster copy line, so they step back
     with it here. */
  .thesis__subhead {
    font-size: clamp(10px, 0.9vw, 14px);
    line-height: 1.34;
  }
}

/* ==================================================================
   Stats column responsive. Only the poster chapters carry numbers,
   and they hold the rail down to 700px: the column is measured off
   a viewport edge, not the card — the right for 01-03, the left for
   04, whose card is anchored right — and the card's own copy measure
   (46ch) keeps it well short of the column on either side at every
   width above that. Below 700px there is no room for a side
   column at all, so the numbers drop under the copy — directly
   beneath the stepper that opens them, with the proceed plate still
   last in the stack. The fallback has to clear the fixed placement
   AND the auto-margin centring that depends on it.
   ================================================================== */

@media (max-width: 700px) {
  .thesis--poster .thesis__stats,
  .thesis--poster .thesis__inner > .thesis__stats {
    position: static;
    right: auto;
    left: auto;                   /* 04 mirrors to the left edge above */
    top: auto;
    bottom: auto;
    height: auto;
    width: auto;
    max-width: none;
    margin: clamp(18px, 2.6vh, 28px) 0 0;
  }
  .thesis__stats {
    flex-direction: column;
    gap: clamp(14px, 2vh, 20px);
  }
  .thesis__stat {
    max-width: none;
  }
  .thesis__stat-value {
    font-size: clamp(24px, 6vw, 36px);
  }
  .thesis__stat-label {
    font-size: clamp(9px, 2.4vw, 11px);
  }
  .thesis__stat-source {
    font-size: clamp(8px, 2vw, 10px);
  }
}

/* Reduced motion drops the reveal, never the composition: only the
   animated transforms are cleared. .thesis__inner and .thesis__scrim
   carry the layout and are deliberately left alone. */
@media (prefers-reduced-motion: reduce) {
  .thesis,
  .thesis__inner > *,
  .thesis__word,
  .thesis__scrim::before,
  .thesis__kicker::before,
  .thesis__kicker::after {
    transition: none !important;
    filter: none !important;
  }
  .thesis__word {
    transform: none !important;
    opacity: 0.96;
  }
  .thesis__inner > * {
    transform: none !important;
    opacity: 1;
  }
  .thesis:not(.is-shown) .thesis__inner > * {
    opacity: 0;
  }
  .thesis__inner::before {
    transform: scaleY(1) !important;
  }
  .thesis:nth-of-type(4) .thesis__inner::before {
    transform: scaleX(1) !important;
  }
  .thesis__kicker::before {
    transform: scale(1) !important;
    opacity: 0.85 !important;
  }
  .thesis--center .thesis__kicker::before,
  .thesis--center .thesis__kicker::after {
    transform: scaleX(1) !important;
    opacity: 0.7 !important;
  }
  .thesis__scrim::before {
    transform: scale(1) !important;
    opacity: var(--numeral-opacity) !important;
  }
  .thesis__stat {
    transition: none !important;
    transform: none !important;
  }
  .thesis.is-shown .thesis__stat {
    opacity: 1;
  }
  .thesis:not(.is-shown) .thesis__stat {
    opacity: 0;
  }
  /* A stepped chapter's reveal is state, not motion: the figures still
     wait for their click, they just stop animating into place. */
  .thesis--stepped.is-shown .thesis__stat {
    opacity: 0;
  }
  .thesis--stepped.is-shown .thesis__stat.is-revealed {
    opacity: 1;
  }
  .thesis__cta--step.is-spent,
  .thesis__cta--rail {
    transition: none !important;
    transform: none !important;
  }
}

/* ---------- Boot veil ---------- */

.veil {
  position: fixed;
  inset: 0;
  background: #000;
  z-index: 20;
  opacity: 1;
  transition: opacity 0.8s ease;
  pointer-events: none;
}

.veil.is-lifted {
  opacity: 0;
}

/* Chapter 1's first frame, painted behind the veil so a cold load
   opens on the real picture instead of black. Fills the viewport
   the same way the video does, so the hand-off to the decoded frame
   is pixel-identical. */
.veil__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  /* Hidden if the poster somehow fails; the veil's own #000 then
     takes over as the boot backdrop. */
  background: #000;
}

/* ---------- Missing-file notice ---------- */

.notice {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.92);
  z-index: 30;
  text-align: center;
}

.notice[hidden] {
  display: none;
}

.notice__inner {
  max-width: 560px;
  font-size: 14px;
  line-height: 1.9;
  font-weight: 300;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.85);
}

.notice__inner code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 13px;
  color: #fff;
  background: rgba(255, 255, 255, 0.08);
  padding: 2px 8px;
  border-radius: 3px;
}

/* ---------- noscript ---------- */

.noscript {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  color: #fff;
  font-size: 14px;
  letter-spacing: 0.2em;
  z-index: 40;
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .cue__line {
    animation: none;
    opacity: 1;
    transform: scaleY(1);
  }
}

/* ---------- Contact popup (deployed build only) ----------
   Built by script.js on the trimmed public site when chapter five's
   plate is pressed. Styled to sit in the same visual language as the
   chapter cards: the site's red accent plate, the plain vignette wash
   with the shared grain, Baygo for the title and JetBrains Mono for the
   copy. No element with class .contact exists in the local build, so
   these rules never apply there. */
.contact {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  /* The chapter red, so the plate and rule match the site's accents. */
  --cta-bg: #c1161f;
  --cta-hover: #e01f2a;
  --cta-glow: rgba(193, 22, 31, 0.26);
}

.contact[hidden] { display: none; }

.contact__scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 90% at 50% 42%,
      rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.72) 58%, rgba(0, 0, 0, 0.88) 100%),
    rgba(2, 3, 6, 0.55);
  opacity: 0;
  transition: opacity 0.4s ease;
}

.contact.is-open .contact__scrim { opacity: 1; }

/* The same fine grain the chapter cards carry, so the wash sits in the
   photography rather than over it. */
.contact__scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("assets/grain.png");
  background-repeat: repeat;
  background-size: 256px 256px;
  opacity: 0.04;
  pointer-events: none;
}

.contact__card {
  position: relative;
  z-index: 1;
  width: min(560px, calc(100vw - 48px));
  padding: clamp(30px, 5vw, 56px);
  text-align: center;
  background: rgba(2, 3, 6, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6);
  opacity: 0;
  transform: translateY(18px) scale(0.98);
  transition:
    opacity 0.5s ease,
    transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.contact.is-open .contact__card {
  opacity: 1;
  transform: none;
}

.contact__title {
  margin: 0 0 clamp(14px, 2vh, 22px);
  font-family: "Baygo", "Anton", "Arial Narrow", sans-serif;
  font-weight: 400;
  font-size: clamp(40px, 7vw, 82px);
  line-height: 0.9;
  letter-spacing: 0.01em;
  color: #fff;
}

.contact__rule {
  height: 2px;
  width: 64px;
  margin: 0 auto clamp(16px, 2.2vh, 24px);
  background: var(--cta-bg);
  box-shadow: 0 0 16px var(--cta-glow);
}

.contact__line {
  margin: 0 0 clamp(24px, 3vh, 34px);
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(13px, 1.4vw, 16px);
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.85);
}

/* Reuse the chapter plate exactly; it just starts filled (not a loading
   gauge here) and drops the letter-spaced-caps transform since the
   label is an email address in lower case. */
.contact__cta {
  --cta-fill: 1;
  text-decoration: none;
  text-transform: none;
  letter-spacing: 0.12em;
}

.contact__cta .thesis__cta-label {
  text-indent: 0;
}

.contact__close {
  position: absolute;
  top: 12px;
  right: 14px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: rgba(255, 255, 255, 0.7);
  font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease;
}

.contact__close:hover,
.contact__close:focus-visible {
  color: #fff;
  outline: none;
}

@media (prefers-reduced-motion: reduce) {
  .contact__scrim,
  .contact__card {
    transition: none;
  }
}
