/* ============================================================================
   democraci matters™ — standalone site
   ----------------------------------------------------------------------------
   Written fresh for this domain, but the values here are inherited from the
   reference implementation on stephencottonjr.com (Career Portfolio → democraci
   matters). Where a number looks arbitrary it is usually measured, not guessed —
   those spots carry a comment.
   ============================================================================ */

:root {
  --bg: #0a0a0c;
  --ink: #f5f3ef;              /* primary text  */
  --ink-soft: #b9b6b0;         /* secondary text */
  --line: rgba(245, 243, 239, 0.16);
  --accent: #e9e6df;

  --max: 76rem;                /* cards container */
  --col: 40rem;                /* reading column */
  --figure-w: min(100%, 58rem); /* hero, crest and figure share this width */
  --pad: clamp(1.25rem, 4vw, 3rem);

  /* THE FLOOR. How far the bottom-most furniture sits above the bottom edge of
     the screen — the seek bar in the playback cluster, and the footer's
     sign-off. At the end of the page those two share a line, so a difference
     between them reads as one of them being adrift. Declared once and consumed
     by .controls and .foot; the phone breakpoint overrides it in one place. */
  --floor: var(--pad);

  /* Mobile browsers change viewport height as their chrome collapses. lvh is the
     LARGE (bars-collapsed) height — locking to it stops the sticky stage from
     shrinking mid-scroll. svh is the fallback for engines without lvh. */
  --app-h: 100svh;
  --app-h: 100lvh;

  /* How much TALLER the bars-collapsed viewport is than the bars-shown one —
     i.e. the height of the browser's own chrome, and zero everywhere the two
     agree (every desktop). Anything sized to --app-h therefore overhangs the
     visible area by this much while the bars are up, and anything that has to
     stay ON screen inside such a box has to be pulled back up by it. */
  --ui-gap: 0px;
  --ui-gap: calc(100lvh - 100svh);

  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-ui: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* The torch's aurora, flattened into a line. Used by the live callout's rule
     and by the ring around the door — ONE declaration so the two cannot drift
     apart, because the whole point of them is that they are the same thing
     appearing twice. Paired with @keyframes aurora-flow and a 320% background
     -size; all three move together or none of them do. */
  --aurora-line: linear-gradient(
    180deg,
    #bef264 0%, #4ade80 22%, #22c55e 42%,
    #2dd4bf 62%, #22d3ee 78%, #4ade80 100%
  );
}

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

html { -webkit-text-size-adjust: 100%; }

/* Held only for the length of the intro, so the timed sequence cannot be
   scrolled out from under itself. Released the moment the curtain clears —
   and released early if the visitor tries to scroll (they get skipped ahead).
   It has to be set on BOTH html and body: `overflow: hidden` on body alone is
   propagated to the viewport only when html is `visible`, so on its own it
   silently fails to hold in several engines. */
html.intro-lock,
html.intro-lock body {
  overflow: hidden;
  touch-action: none;
  overscroll-behavior: none;
}

/* ⚠ AND WHILE THE LOCK IS ON, THE FILM IS HELD TO THE SMALL VIEWPORT.
   This is the fix for the strip of video that shows under the black on iOS.

   The two layers do not measure the screen the same way and cannot be made to:
     · .film__stage is in normal flow at --app-h (100lvh) — the height with the
       browser bars COLLAPSED — and iOS Safari floats its toolbar over the page,
       so the stage genuinely paints in the band behind it.
     · .opening is `position: fixed`, which resolves against the LAYOUT viewport
       and stops short of that band.
   The film therefore shows below the curtain, which is exactly what it looks
   like: a bright strip of footage under the title card, framing the toolbar.

   Two earlier attempts failed and are worth not repeating. Painting past the
   curtain's edge (an oversized pseudo-element, a big box-shadow) is cut off,
   because `overflow: hidden` on the root above propagates to the viewport.
   Giving .opening `height: var(--app-h)` does not help either — a fixed box is
   still laid out against the layout viewport whatever height it is given.

   So the fix goes the other way: shrink the thing that overhangs. 100svh is the
   bars-SHOWN height, so the stage can never exceed what is on screen, and any
   remainder below it is .film's own black. The moment the lock lifts the stage
   returns to --app-h and covers the band itself — by then the curtain has gone,
   so there is nothing left to disagree with. Engines without svh drop this line
   and behave exactly as they did before. */
html.intro-lock .film__stage { height: 100svh; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.5;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

/* Keyboard users land here first; visually hidden until focused. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.7rem 1.1rem;
  background: var(--ink);
  color: var(--bg);
  font-size: 0.85rem;
  border-radius: 0 0 6px 0;
}
.skip:focus { left: 0; }

/* ============================================================================
   THE WORDMARK — type, never an image, so it stays crisp and selectable.
   The bold falls on `i matter`: it reads "i matter" inside "democraci matters".
   Do not re-break it.
   ============================================================================ */
.dm-mark {
  font-family: var(--font-ui);
  text-transform: none;
  /* Light weight and open tracking — the mark should read as composed rather
     than loud. TWO things separate "i matter" from the rest: weight AND value.
     The surround is held back to ~70% while the bold sits at full ink, so the
     hidden phrase surfaces out of the word instead of merely being heavier.
     Done with colour alpha, not `opacity` — a parent's opacity would cap the
     child's, and the bold could never come back to full. */
  letter-spacing: 0.06em;
  font-weight: 200;
  color: rgba(245, 243, 239, 0.7);
  white-space: nowrap;
}
.dm-mark b {
  font-weight: 600;
  color: var(--ink);
}

/* Over live footage at small sizes, 200 at 70% disappears — the strip keeps a
   normal weight and full ink so it stays readable against a bright frame. */
.film__id .dm-mark { font-weight: 400; color: var(--ink); }
.film__id .dm-mark b { font-weight: 700; }

/* ============================================================================
   THE OPENING — full-screen black, the torch, the mark. No scroll required:
   the whole sequence is on a timer in script.js (see INTRO_* there). Fixed
   rather than sticky so it covers the viewport regardless of scroll position.
   `.is-gone` at the end kills visibility and pointer-events so the cleared
   curtain can never swallow a click.
   ============================================================================ */
.opening {
  position: fixed;
  /* Height rather than `inset: 0` so the box matches the stage it covers on
     paper. It is NOT what keeps footage from showing underneath, though — a
     fixed box is laid out against the layout viewport whatever height you give
     it. That job belongs to the `html.intro-lock .film__stage` rule near the
     top of this file; read it before touching anything here. */
  top: 0;
  left: 0;
  right: 0;
  height: var(--app-h);
  z-index: 40;
  display: grid;
  place-items: center;
  background: var(--bg);
  padding: var(--pad);
  /* Sits the composition slightly ABOVE true centre — optically centred rather
     than measured, which is where a standing mark wants to be. Extra bottom
     padding does this without a transform on an ancestor of the aurora.
     + --ui-gap, because the box now runs under the browser's bars: without it
     the composition centres against the taller box and the door ends up behind
     the toolbar. On desktop --ui-gap is 0 and this term disappears. */
  padding-bottom: calc(var(--pad) + 4vh + var(--ui-gap));
  will-change: opacity;
  /* No transition HERE. The curtain has one exit and it is scripted — the
     durations live on .is-dissolving, further down, so that the contents and
     the backdrop can leave on separate clocks. */
}

/* Only the cinematic path starts opaque. Under reduced-motion the class is never
   added (see the inline <head> script) and the opening never covers at all. */
html:not(.cinematic) .opening {
  position: relative;
  height: auto;
  min-height: 60vh;
  z-index: 1;
}

.opening.is-gone { visibility: hidden; pointer-events: none; }

/* ⚠ FREEZE THE CHOREOGRAPHY THE INSTANT THE CURTAIN STARTS CLEARING.
   Without this the reveal STUTTERS — out, back in, out. The cause: a child may
   still be fading IN (the app block, or "presents") while the parent fades OUT.
   The two opacities multiply, so the composite RISES even as the layer falls,
   and the curtain appears to change its mind. Pausing holds every child at
   whatever value it had reached, leaving the parent's fade as the only thing
   moving — which is the only way the fade reads as a single clean gesture.
   Most visible when skipping early, which is exactly when someone clicks. */
.opening.is-gone,
.opening.is-gone *,
.opening.is-gone *::before,
.opening.is-gone *::after,
.opening.is-dissolving *,
.opening.is-dissolving *::before,
.opening.is-dissolving *::after {
  animation-play-state: paused !important;
}

/* ----- THE DOOR -----
   The last beat of the title sequence. It arrives after the app block has
   settled (5.2s) and then simply waits — this is the way into the film, not a
   hint offered to someone who has stalled, so it is always on screen and never
   has to be earned. Styled as a bigger sibling of the playback pills over the
   footage (same hairline, same glass, same lowercase label), so the first thing
   the visitor presses already teaches them what the site's controls look like.

   ⚠ IN THE FLOW, pinned to the wordmark rather than to the viewport. It used to
   be absolutely positioned at the bottom of .opening, which held on a laptop
   and fell apart on anything taller: the composition is optically centred, the
   door was stuck to the floor, and on a large screen the two drifted a third of
   a viewport apart — the door stopped reading as the last line of the block and
   started reading as unrelated page furniture. As a grid row it keeps a fixed
   distance under "community. square" at every height, and the group centres as
   one. The short-screen block near the end of this file tightens it with
   everything else rather than working around it. */
.opening__watch {
  margin-top: clamp(0.9rem, 3vh, 1.9rem);
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  /* It is a <button>: strip the UA chrome before the pill is drawn back on. */
  -webkit-appearance: none;
  appearance: none;
  padding: 0.72rem 1.7rem;
  /* No border of its own — the ring is drawn by ::before below, because a
     border cannot carry a moving gradient around a rounded corner. */
  border: 0;
  border-radius: 999px;
  background: rgba(245, 243, 239, 0.05);
  position: relative;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: clamp(0.78rem, 1.1vw, 0.92rem);
  font-weight: 300;
  text-transform: lowercase;
  letter-spacing: 0.2em;
  cursor: pointer;
  /* Held back until armReveal says the sequence has played out — see .is-ready.
     A transition rather than a keyframe fill, so the dissolve below can take
     the opacity straight back off it without fighting an animation. */
  opacity: 0;
  pointer-events: none;
  /* `visibility`, not just opacity: pointer-events stops the mouse but leaves
     the button in the tab order, and a keyboard visitor should not be able to
     land on a door that has not been offered yet (or, below, one already used).
     It flips at the START of the fade in either direction, so the transition is
     still the opacity's to run. */
  visibility: hidden;
  transition: opacity 0.9s ease, visibility 0s, color 0.25s ease,
    border-color 0.25s ease, background 0.25s ease;
}
/* THE RING — the same drifting aurora as the live callout's rule, and the same
   as the torch it sits under. Three appearances of one idea: the mark, the
   claim, and the way in are visibly the same material.

   Drawn as a masked pseudo-element rather than a border, because `border-image`
   cannot follow a 999px radius — it squares the corners off. The pattern is the
   standard one: fill the box with the gradient, then punch the middle out by
   XOR-ing a content-box mask against a full-box one, leaving exactly the
   1px `padding` as ring. Safari needs the -webkit- spelling and `xor`; everyone
   else takes `exclude`.

   Only `background-position` animates, as on the callout — the gradient itself
   is never recomputed. */
.opening__watch::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px; /* = ring thickness */
  background: var(--aurora-line);
  background-size: 100% 320%;
  animation: aurora-flow 8s ease-in-out infinite;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  /* Held back a little so it reads as a rim on the black rather than a lit
     tube; :hover below brings it up to full. */
  opacity: 0.62;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

/* letter-spacing trails the LAST glyph too, so the pair sits 0.2em left of
   centre inside the pill. Matching padding on the label puts it back. */
.opening__watch-label { padding-left: 0.2em; }
.opening__watch-icon { flex: none; opacity: 0.85; }

.opening.is-ready .opening__watch {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
}
.opening__watch:hover,
.opening__watch:focus-visible {
  color: var(--ink);
  background: rgba(245, 243, 239, 0.1);
  outline: none;
}
.opening__watch:hover::before,
.opening__watch:focus-visible::before {
  opacity: 1;
}

/* Only if the visitor stalls (see armReveal in script.js) does the door start
   breathing. The word is already there and already legible; this just moves,
   because a still page gives the eye nothing to catch. */
.opening.is-inviting .opening__watch {
  animation: watch-breathe 2.8s ease-in-out infinite;
}
@keyframes watch-breathe {
  0%, 100% { box-shadow: 0 0 0 0 rgba(245, 243, 239, 0); }
  50%      { box-shadow: 0 0 0 7px rgba(245, 243, 239, 0.055); }
}
@media (prefers-reduced-motion: reduce) {
  .opening.is-inviting .opening__watch { animation: none; }
}

/* Under reduced-motion there is no curtain to dissolve and no timed sequence:
   the poster and the playback controls are already up, and the visitor starts
   the film from those. A door onto a room they are standing in is clutter. */
html:not(.cinematic) .opening__watch { display: none; }

/* ----- THE DISSOLVE -----
   What the press actually does, and the only thing on the page allowed to
   transition the curtain's opacity.

   Two clocks, not one: the contents must be gone BEFORE the backdrop thins, or
   the torch and the screens ghost over the footage coming up behind them. */
.opening.is-dissolving { transition: opacity 0.95s ease; }
.opening.is-dissolving .opening__inner { transition: opacity 0.42s ease; }
/* THE DOOR IS SPENT THE MOMENT IT IS PRESSED — and stays spent. It leaves
   fastest of everything here, because leaving it up while the black fades makes
   the press look like it missed; and it does not come back, because scrolling
   to the top raises the curtain again (the reveal is reversible by design — see
   onScroll) and would otherwise raise a used button with it, one whose handler
   now returns immediately. Scrolling back down is the way out of that black,
   exactly as it is for a visitor who never pressed anything.

   The visibility flip is held until the fade has finished, so the exit is still
   a fade and not a disappearance. */
.opening.is-entered .opening__watch {
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition: opacity 0.28s ease, visibility 0s linear 0.28s;
}

.opening__inner {
  display: grid;
  justify-items: center;
  gap: clamp(0.9rem, 3vh, 2rem);
  text-align: center;
}

/* THE STAGE — all three states share ONE grid cell, so the cell is always as
   tall as its tallest child (the app block) and NOTHING REFLOWS as the states
   hand over. That is the whole point: the torch above must not shift when the
   app arrives, and a sequenced layout would move it twice.
   `align-self: start` keeps each state anchored to the top of the cell, so the
   name and "presents" sit close under the torch rather than floating in the
   middle of the reserved space. */
.opening__stage {
  display: grid;
  width: 100%;
}
.opening__stage > * {
  grid-area: 1 / 1;
  align-self: start;
  justify-self: center;
}

/* The torch is the single strong visual the whole opening rests on. The mark
   itself (silhouette, aurora fill, halo) lives in democraci-torch.css; this only
   sizes it and runs its entrance.

   ⚠ No `filter` and no `opacity` on THIS element — it is the ancestor of the
   drifting aurora blobs, and either property promotes it to a cached composited
   layer that freezes them. The entrance therefore animates an inner wrapper's
   opacity instead of the mark's own. */
.opening__torch {
  /* Scales with viewport HEIGHT, and the ceiling is deliberately high — on a
     full-screen desktop the old 176px cap left the whole composition marooned
     in the middle of the screen. */
  --size: clamp(90px, 16vh, 232px);
}
/* Pure dissolve — no rise, no scale. The mark should be REVEALED, as if it were
   already standing there and the dark simply thinned enough to show it. Any
   movement on the entrance turns that into an arrival, which is a different
   (and busier) idea. */
.opening__torch-wrap {
  display: inline-block;
  opacity: 0;
  /* In, and then it STAYS — the torch holds the screen for the whole black. */
  animation: torch-in 1.2s ease 0.2s forwards;
}

/* The pair. Both sit in the stage cell together; this box is what fades them
   out as one when the app arrives. */
.opening__brand {
  display: grid;
  justify-items: center;
  gap: clamp(0.6rem, 2.2vh, 1.4rem);
  animation: brand-out 1s ease 4s forwards;
}

.opening__mark {
  margin: 0;
  font-weight: 200;
  /* Smaller than it was. It sits directly under the torch, and at the old size
     it overpowered the mark it is supposed to be standing beneath. */
  font-size: clamp(1.1rem, 4.6vw, 3.05rem);
  opacity: 0;
  animation: mark-in 1.3s ease 1.5s forwards; /* in, and holds */
}

/* "presents" — deliberately muted and small. It arrives in the space the name
   just vacated and is only a connective word, so it must not read as a title. */
.opening__app {
  width: 100%;
  opacity: 0;
  animation: mark-in 1.3s ease 3.9s forwards;
}
.opening__mark .dm-mark {
  letter-spacing: 0.18em;
  /* letter-spacing trails the LAST glyph too, so the line sits 0.18em left of
     true centre. Matching padding on the left puts it back. */
  padding-left: 0.18em;
}

/* The wall-clock schedule of the opening:
     torch     0.2s +1.2s → in by 1.4s, then holds for the whole black
     mark      1.5s +1.3s → in by 2.8s, HOLDS
     brand out 4.0s +1.0s → gone by 5.0s
     app       3.9s +1.3s → in by 5.2s, stays   (crossfades with brand out)
   ⚠ INTRO_HOLD in script.js must stay after the app lands (5.2s). It is 5.6s. */
@keyframes brand-out { to { opacity: 0; } }
@keyframes torch-in { to { opacity: 1; } }
@keyframes mark-in  { to { opacity: 1; } }

/* ----- The third beat of the title sequence -----
   "presents / Community Square / …". Arrives after the wordmark has settled,
   so the black reads as: whose site this is, then what they are showing you,
   then why it matters. A hairline rule separates it from the mark above
   without adding another block of type. */
/* The screens — the hero of the final state. Capped by HEIGHT as well as width:
   on a short laptop an unconstrained image pushes the name below the fold. */
.app__screens {
  max-width: min(78vw, 56rem);
  max-height: 40vh;
  width: auto;
  height: auto;
  margin: clamp(0.4rem, 1.6vh, 1rem) auto clamp(1.2rem, 3.4vh, 2rem);
}

.app__line {
  max-width: 40rem;
  margin: 0 auto;
  font-size: clamp(0.9rem, 1.5vw, 1.3rem);
  line-height: 1.5;
  font-weight: 300;
  color: var(--ink-soft);
  text-wrap: balance;
}

/* The app's name, signing off the block. */
.app__wordmark {
  width: clamp(150px, 24vw, 340px);
  height: auto;
  margin: clamp(1.2rem, 3.6vh, 2.4rem) auto 0;
}

/* Phones are narrow but TALL, so the screens can take much more width than the
   desktop 54vw — at 54vw on a 390px phone the handsets are ~210px wide and the
   product reads as a smudge rather than an app. */
@media (max-width: 760px) {
  .app__screens { max-width: 90vw; max-height: 40vh; }
  .app__wordmark { width: clamp(150px, 50vw, 250px); }
}

/* Short laptop screens: the final state is torch + screens + copy + name, which
   will otherwise push the name below the fold. Everything tightens together
   rather than any one part shrinking alone, so the proportions hold. */
@media (max-height: 780px) {
  .opening { padding-bottom: calc(var(--pad) + 2vh + var(--ui-gap)); }
  .opening__watch {
    margin-top: clamp(0.7rem, 2.2vh, 1.2rem);
    padding: 0.58rem 1.4rem;
    font-size: clamp(0.72rem, 1vw, 0.82rem);
  }
  .opening__inner { gap: clamp(0.55rem, 1.8vh, 1.1rem); }
  .opening__torch { --size: clamp(74px, 13vh, 152px); }
  .opening__mark { font-size: clamp(1.15rem, 4.2vw, 2.3rem); line-height: 1.2; }
  .app__screens { max-height: 37vh; margin-bottom: clamp(0.8rem, 2.4vh, 1.4rem); }
  .app__line { font-size: clamp(0.82rem, 1.25vw, 1.02rem); }
  .app__wordmark {
    width: clamp(126px, 20vw, 220px);
    margin-top: clamp(0.8rem, 2.4vh, 1.4rem);
  }
}

/* ----- Scroll cue: lives over the video, not in the opening -----
   The intro no longer asks for a scroll, so this is the only signal that there
   is anything below the film. Fades permanently on the first scroll. */
.scroll-cue {
  position: fixed;
  left: 50%;
  bottom: clamp(1.4rem, 4.5vh, 2.6rem);
  transform: translateX(-50%);
  z-index: 7;
  display: grid;
  justify-items: center;
  gap: 0.75rem;
  font-size: 0.62rem;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--ink);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9), 0 2px 14px rgba(0, 0, 0, 0.65);
  opacity: 0;
  pointer-events: none;
  transition: opacity 1s ease;
}
.scroll-cue.is-lit { opacity: 0.85; }
.scroll-cue.is-out { opacity: 0; }
.scroll-cue__word { padding-left: 0.42em; /* optical: kill the trailing letterspace */ }
.scroll-cue__rule {
  width: 1px;
  height: clamp(1.8rem, 5vh, 2.8rem);
  background: linear-gradient(to bottom, var(--ink), transparent);
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
  animation: cue-drift 2.6s var(--ease) infinite;
}
@keyframes cue-drift {
  0%, 100% { transform: scaleY(0.55); transform-origin: top; opacity: 0.35; }
  50%      { transform: scaleY(1);    transform-origin: top; opacity: 0.85; }
}

/* ============================================================================
   THE FILM — sticky full-bleed video with the cards rising over it.
   ============================================================================ */
.film {
  position: relative;
  z-index: 1;
  background: var(--bg);
}

.film__stage {
  position: sticky;
  top: 0;
  height: var(--app-h);
  z-index: 0;
  overflow: hidden;
  background: var(--bg) url("assets/democraciMatters-poster.jpg") center / cover no-repeat;
}

/* Both video layers: `translateZ(0)` is not decoration — iOS Safari will happily
   play a video's AUDIO while the picture stays black until something forces a
   relayout. Giving each its own compositor layer is half the fix; the other half
   is the repaint nudge in script.js. */
.film__blur,
.film__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1s ease;
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
}

.film__video.is-playing { opacity: 1; }

/* The pre-blurred PORTRAIT backdrop. It is a BACKDROP, not a stylistic blur:
   it covers at any height on any phone, so the sharp cut never has to win the
   fight to fill every aspect ratio. It is also what carries the approach while
   the sharp cut is still buffering — hence it sits UNDER .film__video and stays
   visible behind it rather than crossfading away. Silent by design. */
.film__blur {
  z-index: 0;
  display: none;              /* only earns its keep on narrow screens */
}
.film__blur.is-playing { opacity: 1; }
.film__video { z-index: 1; }

@media (max-width: 900px) {
  .film__blur { display: block; }
  /* Hold the sharp cut back until it is genuinely painting, so a cold video can
     never flash black over the blur that is already carrying the frame. */
  .film__video { transition: opacity 0.5s ease; }
}

/* ----- Overlay -----
   THE FOOTAGE IS SHOWN CLEAN. The reference implementation carried a dark scrim
   plus a cool soft-light colour grade (#1e63c0 → #0f3168), because there the
   video only ever appeared BEHIND a full screen of text and never had to stand
   on its own. Here it is the hero, and both of those wash it out — the grade in
   particular puts a blue cast over every frame. Both are gone.

   This layer is now transparent until there is actually copy to read over it.
   Legibility elsewhere is handled by the elements that need it: the identity
   strip carries its own gradient, and the cards sit on their own blur plane. */
.film__overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    to bottom,
    rgba(10, 10, 12, 0.5) 0%,
    rgba(10, 10, 12, 0.26) 38%,
    rgba(10, 10, 12, 0.66) 100%
  );
  transition: opacity 0.8s ease;
}

/* Reading mode — the cards are up, so legibility outranks the footage. Neutral
   black only; no colour grade at any point. */
.film.is-reading .film__overlay { opacity: 1; }

/* The film has to END somewhere, and a hard cut from live video to a solid
   background reads as two unrelated containers butted together — which is what
   the bottom of this section looked like. This dissolves the footage into the
   colour the next section starts on, so the seam is a transition rather than an
   edge. z-index 3: above the stage and its overlay (0–2), below the cards (4). */
.film::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(16rem, 42vh, 32rem);
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent 0%, var(--bg) 86%);
}

/* ----- The mark, bottom-centre over the footage -----
   z-index 3 is the whole trick: above the video and its scrim (0–2), below
   .cards (4). So as the copy scrolls up, the cards' blur plane passes over the
   mark and hides it — the reveal of the reading surface retires it, and no
   extra state is needed to say when. */
.film__mark {
  --size: clamp(44px, 7vh, 78px);
  position: fixed;
  left: 50%;
  /* Lifted from its old clamp(1.1rem, 3.6vh, 2.3rem) to seat .film__cue in the
     gap underneath. The cue has to be BELOW the mark to mean "there is more
     under this", and there was no room down there before. */
  bottom: clamp(2.2rem, 5.6vh, 3.6rem);
  transform: translateX(-50%);
  z-index: 3;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.9s ease;
}
.film.is-lit .film__mark { opacity: 1; }
.film__mark.is-out { opacity: 0; }

/* The mark is light green and the footage behind it is whatever it happens to
   be — pale sky, a white shirt, a blown-out window. It needs separation.

   ⚠ It CANNOT be a drop-shadow filter on .dm-torch__fill. That element is
   masked, and democraci-torch.css documents the consequence: a parent that is
   both filtered and masked is promoted to a cached layer, and a cached layer
   freezes the drifting blobs inside it. The aurora would simply stop.

   So the shadow is a COPY OF THE SILHOUETTE — the torch art used as a mask over
   black, blurred, behind the real mark. Masked and filtered, which is fine here
   only because it has no children to freeze.

   TWO copies, at different radii. One blurred silhouette — however soft — keeps
   an opaque core, so its falloff is short and it reads as a dark outline
   tracing the shape. Stacking a wide faint pass under a tighter one gives a
   falloff that actually gradates: dense near the mark, nothing by the edge. */
.film__mark::before,
.film__mark::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: #000;
  -webkit-mask-image: var(--torch-art);
  mask-image: var(--torch-art);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* ⚠ NEVER SCALED — scaling the silhouette and blurring it puts an even ring
   around the whole shape, which reads as a black object behind a green one.

   A LARGE offset turned out to be just as bad: displaced far enough to be seen
   on its own, the blurred silhouette is legible as a second torch rather than
   as a shadow. So this is now barely displaced and barely there — an ambient
   darkening that sits almost directly under the mark and only separates it from
   a bright frame. Nudged down-right, matching the upper-left light the torch's
   own shading in democraci-torch.css already implies.

   Both offset and blur scale with --size, so the mark casts proportionally the
   same shadow at any size. */
.film__mark::before {
  filter: blur(calc(var(--size) * 0.34));
  transform: translate(calc(var(--size) * 0.02), calc(var(--size) * 0.045));
  opacity: 0.34;
}
.film__mark::after {
  filter: blur(calc(var(--size) * 0.15));
  transform: translate(calc(var(--size) * 0.012), calc(var(--size) * 0.028));
  opacity: 0.2;
}

/* The outer halo is what made the shadow read as a glowing blob rather than a
   shadow — the two were fighting. On the footage the silhouette does the work. */
.film__mark .dm-torch__glow { display: none; }

/* ----- THE SCROLL CUE — the only thing that says there is a page under the film
   Nothing else does: the footage loops, and a visitor with no reason to believe
   there is more will sit and then leave. So it has to appear — and it has to
   stay out of the way while it does, because a film is already asking for the
   viewer's attention and a second thing competing for it is worse than no cue
   at all.

   Hence the shape of the behaviour: a ten-second cycle, visible for about two
   and a half of them, and it ONLY FADES. No bob, no bounce, no pulse. Movement
   in the corner of the eye is exactly what pulls attention off a picture, which
   is the one thing this must not do. The chevron itself already points down;
   it does not need to mime the gesture as well. */
.film__cue {
  position: fixed;
  left: 50%;
  bottom: clamp(0.85rem, 2.3vh, 1.5rem);
  z-index: 3;
  width: 13px;
  height: 13px;
  /* Half the box, so it is centred before the rotation — `transform` is spoken
     for by the 45° turn that draws the chevron, so it cannot also translate. */
  margin-left: -6.5px;
  border-right: 1.5px solid var(--ink);
  border-bottom: 1.5px solid var(--ink);
  transform: rotate(45deg);
  /* Same problem the mark has: the footage behind it is whatever it happens to
     be. Two hairlines need separation or they vanish over a bright frame. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.85))
    drop-shadow(0 2px 10px rgba(0, 0, 0, 0.55));
  opacity: 0;
  pointer-events: none;
}

/* Only once the film is up, and with a delay so the cue is not part of the
   film's own opening — by the time it first shows, the visitor has had a while
   to sit with the picture and is the right kind of restless. */
.film.is-lit .film__cue {
  animation: cue-breathe 10s ease-in-out 6s infinite;
}

/* SPENT ON THE FIRST SCROLL (see script.js). The message has landed; a cue that
   keeps repeating after the visitor has already done the thing is just a tic.

   ⚠ TWO TRAPS, both of which this rule has to clear at once:
     · `animation: none`, not merely `opacity: 0` — the keyframes set opacity
       themselves and an animation beats a plain declaration outright.
     · and it must be selected through `.film.is-lit`, matching the rule that
       starts the animation. As a bare `.film__cue.is-out` it is a weaker
       selector than `.film.is-lit .film__cue` and simply loses: the cue keeps
       cycling, and looks retired only for whichever part of the ten seconds
       happens to be transparent. */
.film.is-lit .film__cue.is-out {
  animation: none;
  opacity: 0;
}

@keyframes cue-breathe {
  0%   { opacity: 0; }
  6%   { opacity: 0.62; }  /* 0.6s dissolving in  */
  16%  { opacity: 0.62; }  /* 1.0s held           */
  26%  { opacity: 0; }     /* 1.0s dissolving out */
  100% { opacity: 0; }     /* 7.4s of nothing     */
}

/* No cycling for anyone who has asked for stillness — but the cue still has a
   job, so it stays up quietly instead of disappearing. */
@media (prefers-reduced-motion: reduce) {
  .film.is-lit .film__cue { animation: none; opacity: 0.45; }
}

/* ----- Identity strip: persistent, top-left, over the video -----
   FIXED, not sticky. It follows the sticky stage in the DOM, so as a sticky
   element its flow position is one whole viewport down the page — it would not
   pin to the top until the visitor had scrolled past the entire stage, leaving
   it floating in mid-screen during the reveal. Fixed puts it where the design
   wants it from the first frame; .is-out retires it when the film scrolls away. */
.film__id {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 6;
  margin: 0;
  padding: clamp(1rem, 3.2vh, 1.9rem) var(--pad);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.55rem;
  font-size: clamp(0.78rem, 1.5vw, 0.95rem);
  color: var(--ink);
  /* Tint and blur both live on ::before below, so this box carries only type.
     The shadow stays here: it is local to the glyphs and holds the mark against
     a blown-out frame in a way no amount of scrim behind it can. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.92), 0 2px 16px rgba(0, 0, 0, 0.7);
  opacity: 0;
  transition: opacity 0.9s ease;
}

/* ⚠ THE STRIP NEEDS TO DEFOCUS WHAT PASSES UNDER IT, not just darken it.
   The mark is pinned to the top of the screen and the cards scroll beneath it,
   so headings ride straight through the wordmark — "What it is" and
   "democraci matters™" land on top of each other and both become unreadable.
   A tint alone cannot fix that: dimming sharp text under sharp text still
   leaves two legible things in the same place. Blurring it removes the
   competing detail, which is the only thing that actually separates them.

   The mask is what keeps this from being the band across the picture the strip
   has always been careful not to draw. Blur and tint fade out together over the
   strip's own height and a little beyond, so there is no edge anywhere — the
   frame is untouched a couple of centimetres down. Same technique as the cards'
   plane; see .cards::before. */
.film__id::before {
  content: "";
  position: absolute;
  inset: 0 0 -1.6rem 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(6, 7, 10, 0.42) 0%,
    rgba(6, 7, 10, 0.14) 62%,
    transparent 100%
  );
  -webkit-backdrop-filter: blur(13px) saturate(1.05);
  backdrop-filter: blur(13px) saturate(1.05);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 38%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 38%, transparent 100%);
}
.film__id.is-lit { opacity: 1; }
.film__id.is-out { opacity: 0; }
.film__id-sep { color: var(--ink); opacity: 0.6; }
.film__id-role {
  /* Not --ink-soft here: this sits on live footage that can be near-white, and
     a soft grey disappears into it. Full ink at reduced opacity keeps the
     hierarchy without giving up contrast. */
  color: var(--ink);
  opacity: 0.82;
  font-size: 0.92em;
  letter-spacing: 0.01em;
}
@media (max-width: 560px) {
  .film__id-sep { display: none; }
  .film__id-role { flex-basis: 100%; }
}

/* ============================================================================
   UNMUTE — shown only when the browser refused the unmuted autoplay.

   The ENTIRE SCREEN is the button. No ring, no accent colour, no small target:
   the page is playing a talking head in silence, which is the one moment that
   earns the whole viewport. Anywhere they press fixes it.
   ============================================================================ */
.unmute {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  width: 100%;
  padding: 0;
  border: none;
  /* Just enough veil to seat white type over live footage. */
  background: rgba(6, 7, 10, 0.3);
  color: var(--ink);
  font-family: var(--font-ui);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.55s ease;
}
.unmute[hidden] { display: none; }
.unmute.is-lit { opacity: 1; }

.unmute__inner {
  display: grid;
  justify-items: center;
  gap: clamp(1.1rem, 3vh, 1.9rem);
  transform: translateY(-2vh); /* optical: centres against the label's weight */
}

.unmute__icon {
  width: clamp(58px, 9vh, 92px);
  height: auto;
  filter: drop-shadow(0 2px 20px rgba(0, 0, 0, 0.7));
  transition: transform 0.25s var(--ease);
}
.unmute:hover .unmute__icon,
.unmute:focus-visible .unmute__icon { transform: scale(1.06); }

.unmute__label {
  font-size: clamp(1.05rem, 2.4vw, 1.6rem);
  font-weight: 200;
  letter-spacing: 0.2em;
  padding-left: 0.2em; /* optical: kill the trailing letterspace */
  text-transform: uppercase;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 2px 20px rgba(0, 0, 0, 0.8);
}

/* ============================================================================
   PLAYER CONTROLS — seek bar + play/pause + sound + replay.
   Ported from the personal site so the two sites read as the same hand: same
   cluster, same class names, same lowercase labels naming the ACTION rather
   than the state ("pause" while playing, "play" while paused).
   ============================================================================ */
.controls {
  position: fixed;
  right: var(--pad);
  bottom: var(--floor);
  z-index: 30;
  display: grid;
  /* 1fr rather than auto: fr tracks in a shrink-to-fit grid all resolve to the
     widest track's contribution, so the three pills come out identical without
     anyone hard-coding a width that a label change ("unmute" -> "mute") breaks. */
  grid-template-columns: repeat(3, 1fr);
  align-items: center;
  justify-content: end;
  gap: 0.5rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s ease;
}
.controls.is-shown { opacity: 1; pointer-events: auto; }

.motion-toggle,
.sound-toggle,
.refresh-toggle {
  text-transform: lowercase;
  display: inline-flex;
  align-items: center;
  /* The pills are wider than their content now that they share a width, so the
     icon+label pair is centred instead of hugging the leading edge. */
  justify-content: center;
  gap: 0.55rem;
  padding: 0.55rem 0.95rem;
  border: 1px solid rgba(245, 243, 239, 0.22);
  border-radius: 999px;
  /* Proper glass: the blur does the work, not the tint. A heavy blur with only
     a whisper of dark keeps the footage's colour alive through the pill instead
     of stamping a black lozenge over it. The text shadow (not a darker plate)
     is what holds legibility when the clip blows out to white. */
  background: rgba(12, 13, 16, 0.26);
  -webkit-backdrop-filter: blur(22px) saturate(1.5);
  backdrop-filter: blur(22px) saturate(1.5);
  color: var(--ink-soft);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.75);
  font-family: inherit;
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.motion-toggle:hover,
.sound-toggle:hover,
.refresh-toggle:hover,
.motion-toggle:focus-visible,
.sound-toggle:focus-visible,
.refresh-toggle:focus-visible {
  color: var(--ink);
  border-color: var(--ink);
}

/* ----- Seek bar ----- */
.seek-bar {
  grid-column: 1 / -1; /* span the full width of the button cluster */
  width: 100%;
  height: 14px;
  margin: 0;
  padding: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}
.seek-bar::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(to right,
    var(--ink) 0%, var(--ink) var(--seek, 0%),
    rgba(255, 255, 255, 0.18) var(--seek, 0%), rgba(255, 255, 255, 0.18) 100%);
}
.seek-bar::-moz-range-track { height: 3px; border-radius: 2px; background: rgba(255, 255, 255, 0.18); }
.seek-bar::-moz-range-progress { height: 3px; border-radius: 2px; background: var(--ink); }
.seek-bar::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  margin-top: -4.5px;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.seek-bar:hover::-webkit-slider-thumb,
.seek-bar:active::-webkit-slider-thumb { opacity: 1; }
.seek-bar::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
  opacity: 0;
}
.seek-bar:hover::-moz-range-thumb,
.seek-bar:active::-moz-range-thumb { opacity: 1; }

/* ----- Icons ----- */
.motion-toggle__icon {
  width: 9px;
  height: 10px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}
.motion-toggle.is-paused .motion-toggle__icon {
  width: 0;
  height: 0;
  border-left: 9px solid currentColor;
  border-right: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}
.sound-toggle__icon,
.refresh-toggle__icon { display: block; }
.sound-toggle__wave { opacity: 0; transition: opacity 0.2s ease; }
.sound-toggle.is-on .sound-toggle__wave { opacity: 1; }
.sound-toggle.is-on .sound-toggle__slash { opacity: 0; }

/* Phones: stack the buttons at the bottom-right with the scrubber as a line
   beneath them, clear of the footer logo. */
@media (max-width: 760px) {
  /* Phones sit the cluster lower than --pad would. The footer follows it here
     rather than carrying its own number — see --floor. */
  :root { --floor: 0.6rem; }
  .controls {
    left: var(--pad);
    right: var(--pad);
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
  }
  .controls .seek-bar {
    order: 1;          /* drop the scrubber below the buttons */
    align-self: flex-end;
    width: 45%;
  }
  .motion-toggle,
  .sound-toggle,
  .refresh-toggle {
    font-size: 0.74rem;
    padding: 0.45rem 0.8rem;
    /* Stacked, the pills can't share a grid track, so a floor does the levelling.
       "unmute" is the widest label any of the three ever shows (the others swap
       to shorter words — "play", "mute"), and it measures ~93px here. */
    min-width: 6rem;
  }
}

/* ============================================================================
   THE CARDS — the content spine, rising over the dimming video.
   ============================================================================ */
.cards {
  position: relative;
  z-index: 4;
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--pad) clamp(6rem, 18vh, 12rem);
  /* Leading spacer. NOTE this is ON TOP of a full viewport already: the sticky
     .film__stage still occupies its normal flow space, so .cards begins one
     screen down before this padding is added — the number below is measured
     from there, not from the top of the document.

     ⚠ THE BLUR AND THE FIRST TITLE ARE A PAIR. Whatever this is, the blur
     plane's vertical fade (below, and in the narrow-screen block after it) has
     to finish just under it. Set them apart and the visitor scrolls through a
     screen of out-of-focus footage with nothing in it, which does not read as
     "text is coming" — it reads as a video that has gone soft. About an eighth
     of a screen of blur ahead of the first card is enough to promise something
     and not enough to feel like a mistake.

     ⚠ AND THE PLANE MUST NOT REACH THE FILM'S OWN SCREEN. The blur fades in at
     a FULL SCREEN (1.0) below .cards' top edge, which is itself one screen down
     — so the whole first screenful of scroll is untouched footage and the panel
     only begins as the visitor leaves it. Pulled any earlier, a band of heavy
     blur with nothing in it sits over the picture, and since the phone cut is
     letterboxed over its own blurred backdrop that lands as a THIRD horizontal
     blur band across the frame. It does not read as a surface arriving; it
     reads as the video breaking.

     This used to be more than twice as far, and not on purpose: .cards was
     accidentally opened TWICE in the markup, so the spacer and the blur plane
     were both applied to a nested pair. That is fixed; do not re-nest it. */
  padding-top: calc(var(--app-h) * 1.15);
}

/* ----- The left→right gradient blur the cards sit on -----
   Two stacked layers: a light 6px pass carrying a dark tint, and a heavy 20px
   pass whose taper starts earlier so the two blend instead of stepping. Full
   strength to ~75% of the VIEWPORT width, feathering to clear at the right edge,
   so the right side stays sharp video.

   The full-bleed breakout (left:50% + translateX(-50%) + 100vw) is load-bearing:
   `inset: 0` would paint a centred 76rem column and leave bare video down both
   sides once .cards is capped. Any scrollbar overshoot is clipped by .film. */
@media (min-width: 901px) {
  /* ⚠ ONE backdrop-filter layer, not two.
     The reference implementation stacked a light pass and a heavy pass to blend
     the blur. Over a <video> that reads as TWO PLANES AT DIFFERENT HEIGHTS —
     each masked backdrop-filter element establishes its own backdrop root, and
     the compositor samples them independently, so their edges do not agree.
     One layer cannot disagree with itself. The tint the second layer used to
     carry is folded into this one's background.

     The mask now does two jobs, intersected:
       · horizontally — full strength to 75%, feathering to the right edge, so
         the right side stays sharp video
       · vertically — fades IN over the container's leading padding, so the
         plane has no hard top edge cutting across the picture. That edge was
         the other half of what looked like a second, higher container. */
  .cards::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100vw;
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.9s ease;

    background: linear-gradient(
      to right,
      rgba(10, 10, 13, 0.34) 0%,
      rgba(10, 10, 13, 0.14) 60%,
      transparent 100%
    );
    -webkit-backdrop-filter: blur(22px) saturate(1.12);
    backdrop-filter: blur(22px) saturate(1.12);

    /* ⚠ The vertical fade starts LATE on purpose, but not as late as the
       container. .cards begins one screen down (the sticky stage occupies that
       much flow), so a fade beginning at its top edge would put blur over the
       picture the instant the visitor moved. Holding it clear for a full screen
       means the film keeps its own screenful of scroll untouched; the plane is
       then fully formed at 1.12, just before the first card lands at 1.15, so
       nothing ever sits on a half-built surface.
       See the pairing note on .cards above before changing either number. */
    -webkit-mask-image:
      linear-gradient(to right, #000 0%, #000 75%, transparent 100%),
      linear-gradient(to bottom,
        transparent 0,
        transparent calc(var(--app-h) * 1),
        #000 calc(var(--app-h) * 1.12),
        #000 calc(100% - var(--app-h) * 0.22),
        transparent 100%);
    mask-image:
      linear-gradient(to right, #000 0%, #000 75%, transparent 100%),
      linear-gradient(to bottom,
        transparent 0,
        transparent calc(var(--app-h) * 1),
        #000 calc(var(--app-h) * 1.12),
        #000 calc(100% - var(--app-h) * 0.22),
        transparent 100%);
    -webkit-mask-composite: source-in; /* Safari's spelling of `intersect` */
    mask-composite: intersect;
  }
  .film.is-lit .cards::before { opacity: 1; }
}

/* On narrow screens there is no "right half" to keep clear, so the gradient is
   replaced by a plain vertical scrim under the full width of the text. */
@media (max-width: 900px) {
  .cards::before {
    content: "";
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    width: 100vw;
    /* ⚠ MASKED IN, NOT OFFSET WITH `top`. This used to start at a `top:` inset,
       which gave the plane a HARD HORIZONTAL EDGE: sharp footage above the
       line, 26px of blur below it, switching in a single pixel. The desktop
       plane has always faded in for exactly this reason, and the phone needs it
       more, not less — the vertical cut is letterboxed over its own blurred
       backdrop, so a hard-edged third band across the frame reads as the video
       failing rather than as a surface arriving. `top: 0` with the mask doing
       the entrance is the same technique as the wide layout above. */
    top: 0;
    bottom: 0;
    z-index: -1;
    pointer-events: none;
    background: linear-gradient(
      to bottom,
      rgba(10, 10, 13, 0.5) 0,
      rgba(10, 10, 13, 0.62) 100%
    );
    -webkit-backdrop-filter: blur(26px) saturate(1.12);
    backdrop-filter: blur(26px) saturate(1.12);

    /* Same schedule as the wide layout: clear for the film's own screenful,
       fully formed by 1.12, just before the first card lands at 1.15. The tail
       lets the plane go before the footer's own background takes over. */
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent 0,
      transparent calc(var(--app-h) * 1),
      #000 calc(var(--app-h) * 1.12),
      #000 calc(100% - var(--app-h) * 0.22),
      transparent 100%
    );
    mask-image: linear-gradient(
      to bottom,
      transparent 0,
      transparent calc(var(--app-h) * 1),
      #000 calc(var(--app-h) * 1.12),
      #000 calc(100% - var(--app-h) * 0.22),
      transparent 100%
    );
  }
}

.card {
  position: relative;
  margin: 0 0 clamp(5rem, 16vh, 11rem);
  opacity: 0;
  transform: translateY(2.2rem);
  transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}
.card:last-child { margin-bottom: 0; }
.card.is-in { opacity: 1; transform: none; }

.card__body { max-width: var(--col); }

/* A visual that runs ACROSS THE TOP of a card rather than docking beside it.
   The screens need real width to read as a product — at aside width they were
   a dozen thumbnail handsets. This deliberately breaks past --col (the reading
   measure) because an image has no reason to be bound by a line length. */
.card__hero {
  width: 100%;
  max-width: var(--figure-w);
  margin: 0 0 clamp(1.4rem, 3.5vh, 2.2rem);
  filter: drop-shadow(0 16px 40px rgba(0, 0, 0, 0.55));
}

.card__h {
  margin: 0 0 0.9rem;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.5rem, 3.4vw, 2.3rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.55);
}

.lead {
  margin: 0 0 1.1rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 1.9vw, 1.32rem);
  line-height: 1.42;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);
}

.body {
  margin: 0 0 0.8rem;
  font-size: clamp(0.92rem, 1.2vw, 1.02rem);
  line-height: 1.55;
  color: var(--ink-soft);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}
.body:last-child { margin-bottom: 0; }

/* The pivot lines — "Community Square corrects this error." */
.turn {
  margin: 1.3rem 0 0.9rem;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.02rem, 1.7vw, 1.2rem);
  line-height: 1.35;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);
}
.turn--sub {
  margin-top: -0.55rem;
  font-weight: 400;
  font-size: clamp(0.95rem, 1.4vw, 1.05rem);
  color: var(--ink-soft);
}

/* The name, centred beneath the screens and given real air before the heading.
   Community Square has no icon — the wordmark IS the mark — so it names the
   section and frees the heading to be parallel with the ones below it.
   The crest matches the figure width so the mark centres under the IMAGE, not
   under the narrower reading column, which would have read as off-centre. */
.card__crest {
  width: 100%;
  max-width: var(--figure-w);
  display: grid;
  justify-items: center;
  /* The separation that was missing: at tight spacing the mark read as a
     caption on the screenshot rather than as the name of the product. */
  margin: 0 0 clamp(2.6rem, 8vh, 5rem);
}
.card__wordmark {
  width: clamp(170px, 30vw, 320px);
  height: auto;
}

/* The two-gesture diagram. Runs the full figure width BELOW the copy rather
   than docking beside it — at aside width the two labels it exists to show
   ("swipe → learn more", "pull-up → take action") were unreadable. */
.card__figure {
  width: 100%;
  max-width: var(--figure-w);
  border-radius: 10px;
  margin: clamp(2.4rem, 7vh, 4rem) 0 0;
  filter: drop-shadow(0 14px 34px rgba(0, 0, 0, 0.55));
}

/* ----- Callout: indented, with a rule -----
   For copy that elaborates on the line above it rather than continuing the
   argument. The rule is the brand green so it reads as the site's own voice
   rather than a blockquote from somewhere else. */
.callout {
  /* Space on BOTH sides. With top margin only, the paragraph after a callout
     butted straight against its lower edge and the two read as one block. */
  margin: clamp(1.6rem, 4vh, 2.4rem) 0 clamp(1.7rem, 4.5vh, 2.6rem);
  padding-left: clamp(1rem, 2.4vw, 1.7rem);
  border-left: 2px solid rgba(74, 222, 128, 0.5);
}
.callout p {
  margin: 0 0 0.85rem;
  font-size: clamp(0.92rem, 1.25vw, 1.02rem);
  line-height: 1.6;
  color: var(--ink-soft);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}
.callout p:last-child { margin-bottom: 0; }
.callout b { color: var(--ink); font-weight: 600; }

/* ----- Live callout -----
   The one sentence the whole site rests on. Its rule carries the same drifting
   green as the torch, so the mark and the claim are visibly the same thing.
   Animating background-position (not the gradient itself) keeps it on the
   compositor — a 3px sliver repainting every frame would not be worth it. */
.callout--live {
  position: relative;
  border-left: none;
  padding-left: clamp(1.1rem, 2.6vw, 1.9rem);
}
.callout--live::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.15em;
  bottom: 0.15em;
  width: 3px;
  border-radius: 3px;
  background: var(--aurora-line);
  background-size: 100% 320%;
  animation: aurora-flow 8s ease-in-out infinite;
}
/* The definition deserves lead treatment — it is the thesis, not an aside. */
.callout--live p {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.9vw, 1.3rem);
  line-height: 1.42;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);
}
@keyframes aurora-flow {
  0%, 100% { background-position: 0% 0%; }
  50%      { background-position: 0% 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .callout--live::before,
  .opening__watch::before,
  .loop::before { animation: none; }
}

/* ----- Status list: pilot → rebuild → paused -----
   Numbered because the order is the point; the counter is drawn rather than
   using the list marker so it can sit in the margin and stay out of the text. */
.status {
  list-style: none;
  counter-reset: status;
  margin: 1.4rem 0 0;
  padding: 0;
  display: grid;
  gap: 1.5rem;
}
.status li {
  counter-increment: status;
  position: relative;
  padding-left: 2.2rem;
}
.status li::before {
  content: counter(status, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 0.1em;
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: rgba(74, 222, 128, 0.9);
}
.status li b {
  font-weight: 600;
  font-size: clamp(0.98rem, 1.35vw, 1.1rem);
  color: var(--ink);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
}
/* A qualifier on the entry's name, not part of it — same treatment as
   .loop__alt, which is the other place on the page a bold label takes a
   parenthetical. Lighter and smaller so the label still reads as the label; at
   the same weight the two halves fight and neither leads. */
.status__alt {
  font-weight: 400;
  font-size: 0.86em;
  color: var(--ink-soft);
}
.status__when {
  margin-left: 0.6rem;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  opacity: 0.8;
}
.status li p {
  margin: 0.5rem 0 0;
  font-size: clamp(0.9rem, 1.2vw, 0.99rem);
  line-height: 1.58;
  color: var(--ink-soft);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}

/* ----- Bulleted lists (what I did / what broke) ----- */
.did {
  list-style: none;
  margin: 0 0 1.2rem;
  padding: 0;
  display: grid;
  gap: 0.42rem;
}
.did li {
  position: relative;
  padding-left: 1.05rem;
  font-size: clamp(0.9rem, 1.15vw, 0.98rem);
  line-height: 1.5;
  color: var(--ink-soft);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}
.did li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 4px;
  height: 1px;
  background: var(--ink-soft);
  opacity: 0.7;
}
.did b { color: var(--ink); font-weight: 600; }
.fail li::before { background: #ce5c8e; opacity: 0.9; }

/* A closing pull-quote: the line a card wants you to leave holding. */
.lesson {
  margin: 1.4rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.05rem, 1.8vw, 1.3rem);
  line-height: 1.35;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55);
}

/* ----- The seven loops -----
   Each carries the same drifting aurora as the live callout and the ring around
   the door, so every vertical rule on the page is visibly one material. Same
   --aurora-line, same @keyframes aurora-flow, same 320% background-size — the
   three are deliberately not independent, and changing one means changing the
   variable, not this rule.

   ⚠ HIERARCHY IS CARRIED BY WEIGHT, NOT BY COLOUR. The live callout is 3px at
   full strength because it is the one sentence the site rests on; there are
   seven of these and they are supporting detail, so they run thinner and
   dimmer. Match them to the callout and the thesis stops being the loudest
   thing in the column.

   2px rather than the 1px border this replaces: a 1px gradient has nowhere to
   gradate and just resolves to its average colour, which is a flat green line
   and not the effect. */
.loop {
  position: relative;
  margin-top: 1.6rem;
  padding-left: 1rem;
  border-left: 0;
}
.loop::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.15em;
  bottom: 0.15em;
  width: 2px;
  border-radius: 2px;
  background: var(--aurora-line);
  background-size: 100% 320%;
  animation: aurora-flow 8s ease-in-out infinite;
  opacity: 0.5;
  pointer-events: none;
}
.loop__name {
  margin: 0 0 0.4rem;
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: clamp(0.93rem, 1.25vw, 1.02rem);
  letter-spacing: 0.01em;
  color: var(--ink);
}
.loop__alt {
  font-weight: 400;
  font-size: 0.86em;
  color: var(--ink-soft);
}
.loop__flow {
  margin: 0 0 0.4rem;
  font-size: clamp(0.88rem, 1.15vw, 0.97rem);
  line-height: 1.55;
  color: var(--ink-soft);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}
.loop__fix {
  margin: 0;
  font-size: clamp(0.85rem, 1.1vw, 0.93rem);
  line-height: 1.5;
  color: var(--ink);
  opacity: 0.9;
}
.loop__fix b { font-weight: 600; }

/* ----- Docked visuals -----
   On wide screens the image sits just past the reading column and overlaps its
   right edge by 1.3rem — which stays inside the column's own padding, so no text
   is ever covered. Below that it simply flows under the copy. */





/* ============================================================================
   FOOT — the text block anchored BOTTOM-LEFT, the torch centred on the page.

   Both sit on the same band at the foot of the document. Previously the text
   sat at the top of a tall footer with the torch below it, which left the whole
   group floating in the middle of an empty field with nothing holding it down.
   ============================================================================ */
.foot {
  position: relative;
  z-index: 5;
  background: var(--bg);
  /* Tall enough to breathe after the last card, but the content sits at the
     BOTTOM of it — hence flex-end rather than a big top padding. */
  min-height: clamp(20rem, 42vh, 30rem);
  display: flex;
  align-items: flex-end;
  /* ⚠ THE BOTTOM PADDING IS var(--pad) BECAUSE THE CONTROLS SIT AT
     `bottom: var(--pad)`. At the end of the page the footer's floor and the
     viewport's floor are the same line, so the sign-off and the seek bar are
     read as a pair sitting on it — and any difference between the two numbers
     shows up as one of them being slightly adrift. It is the same value on
     purpose; if .controls moves, this moves with it. */
  padding: clamp(3rem, 9vh, 5rem) var(--pad) var(--floor);
}

.foot__inner {
  /* NOT capped at --max and NOT centred: flush at the page's true left gutter,
     the same edge the identity strip uses at the top. */
  max-width: none;
  margin: 0;
  display: grid;
  justify-items: start;
  gap: 0.85rem;
  text-align: left;

  /* ONE typeface, ONE size, ONE weight for the whole footer. It previously ran
     four different treatments down four lines, which read as four unrelated
     things rather than as one block. Everything below inherits from here and
     only differs where meaning requires it. */
  font-family: var(--font-ui);
  font-size: clamp(0.86rem, 1.15vw, 0.95rem);
  font-weight: 300;
  line-height: 1.5;
  color: rgba(245, 243, 239, 0.62);
}

/* An ABSOLUTE line box, not a multiplier. With line-height: 1.5 the smaller
   third row had a shorter box, so the three rows were 34px / 37px apart even
   though the grid gap was identical. A fixed rem height makes every row the
   same height regardless of its font-size, which is what actually produces an
   even rhythm. */
.foot__inner p {
  margin: 0;
  font-size: inherit;
  font-weight: inherit;
  color: inherit;
  line-height: 1.35rem;
}

/* ----- The two founders -----
   Face on the left, name and that person's own links stacked beside it. The
   grouping is doing attribution work, not styling work — see the note in the
   markup. `align-items: start` so the portrait hangs from the name's line
   rather than centring itself against a block whose height depends on how many
   links someone happens to have. */
.foot__person {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0 0.85rem;
}
.foot__who {
  display: grid;
  gap: 0.1rem;
}
/* Air between the two people, and ONLY between them — not on .foot__inner's
   gap, which would push the status line and the sign-off out with them and
   flatten the footer back into five evenly spaced rows. Each person is two
   tightly-set lines (0.1rem apart); without a clear break between the pairs the
   four lines read as one four-line list, and Harry's name looks like it belongs
   to the links above it. */
.foot__person + .foot__person {
  margin-top: 0.95rem;
}
.foot__face {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  /* The two shots share no background — a pale studio wall and a saturated
     green sweep — so a hairline ring gives them one common edge. Without it the
     lighter portrait reads as a hole punched in the page rather than as a
     picture sitting on it. */
  box-shadow: 0 0 0 1px rgba(245, 243, 239, 0.18);
  /* Nudged down by the difference between the row's line box and the circle,
     so the face's centre lands on the name rather than above it. */
  margin-top: 0.1rem;
}

/* Stacked, the portrait would sit above a name it is meant to sit beside, so
   below this width it goes back to being a row and shrinks with the type. */
@media (max-width: 420px) {
  .foot__face { width: 38px; height: 38px; }
  .foot__person { gap: 0 0.65rem; }
}

.foot__contact {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
}
/* Underlined with text-decoration, NOT a border + padding. A border box grew
   this row by 3px, which is exactly why the three footer rows were 36/38px
   apart instead of even — the underline was silently doing layout. */
.foot__contact a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: rgba(245, 243, 239, 0.22);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s ease, color 0.2s ease;
}
.foot__contact a:hover,
.foot__contact a:focus-visible {
  color: #86efac;
  text-decoration-color: rgba(134, 239, 172, 0.7);
}

/* Same treatment as every other footer row — one typeface, one size. */
.foot__links {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
}
.foot__links a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: rgba(245, 243, 239, 0.22);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.2s ease, color 0.2s ease;
}
.foot__links a:hover,
.foot__links a:focus-visible {
  color: #86efac;
  text-decoration-color: rgba(134, 239, 172, 0.7);
}

/* Same size as everything else — the mark earns its distinction from the bold
   inside it, not from being bigger than its neighbours. */
/* Smaller than the two lines above it, and no extra margin: the three rows sit
   on one even rhythm. It reads as the sign-off rather than as a third
   announcement — and at this size the row height matches its neighbours, which
   is what made the stack look uneven before. */
.foot__mark {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 1rem;
  font-size: clamp(0.72rem, 0.95vw, 0.79rem) !important;
}
.foot__mark .dm-mark {
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: 0.1em;
  color: inherit;
}
.foot__mark .dm-mark b { font-weight: 600; color: rgba(245, 243, 239, 0.92); }
.foot__legal { letter-spacing: 0.06em; }

/* Centred on the PAGE, not on the text column — so it is taken out of the flow
   and pinned. It shares the text block's bottom edge, so the two read as one
   band rather than as a stack with a gap. */
.foot__torch {
  --size: clamp(58px, 9vh, 104px);
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: clamp(1.4rem, 3.5vh, 2.2rem);
}

/* Phones: no room for a centred mark beside left-aligned text — stack it. */
@media (max-width: 760px) {
  .foot {
    display: block;
    min-height: 0;
    /* padding-bottom is inherited from the base rule via --floor, which this
       breakpoint has already lowered — nothing to restate. */
  }
  /* ⚠ position: RELATIVE, never static. .dm-torch's children are absolutely
     positioned with inset: 0, so the mark must stay a containing block. Setting
     it static here made them resolve against .foot instead and the torch
     rendered at ~5x its box, spilling across the footer text. */
  .foot__torch {
    position: relative;
    left: auto;
    transform: none;
    justify-self: center;
    margin: clamp(2rem, 6vh, 3rem) auto 0;
  }
}

/* ============================================================================
   REDUCED MOTION — no crossfade, no autoplay, no rising cards. The opening
   becomes an ordinary first block and everything below it is simply readable.
   ============================================================================ */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* Both are transient overlays tied to the cinematic reveal, which does not
     run here. The title card's copy is card 2's lead, present in full below. */
  .scroll-cue,
  .presents { display: none; }
  .opening__torch-wrap,
  .opening__mark { opacity: 1; transform: none; animation: none; }
  .card { opacity: 1; transform: none; }
  /* No pinned overlay when there is no cinema to overlay — the strip becomes an
     ordinary heading above the stage. */
  .film__id { position: static; opacity: 1; background: none; }
  .film__id::before { display: none; }
  .cards { padding-top: clamp(3rem, 12vh, 7rem); }
  .film__stage { position: relative; height: min(70vh, 40rem); }
  .cards::before { opacity: 1 !important; }
}
