/* ══════════════════════════════════════════════════════════════════════════
   TNAADO — /media HERO: "THE MOVEMENT"
   2026-08-09.

   WHAT THIS REPLACES AND WHY
   ──────────────────────────
   The hero was THE RACK (css/mediaroom.css §0 + js/media-rack.js): a CSS-3D
   dolly through four street PHOTOGRAPHS, driven entirely by scroll on a 300vh
   track. Two defects, both fatal under the standard set on 2026-08-09:

     1. It is a photograph effect. Ethan, on the two technology heroes the
        same night: "don't use a photo at all... make a custom animation for
        the page that will jaw drop." A camera dollying past pictures is a
        treatment applied to images, not an object.
     2. NOTHING MOVED ON LOAD. --mp is a pure function of scrollTop, so at
        t=0..2s, the window a visitor actually spends looking at a hero, the
        frame was a still. The identical defect was diagnosed and rejected on
        /technology (css/tech-hero-build.css) and on the four service heroes
        (css/service-moments.css) earlier the same night.

   THE OBJECT
   ──────────
   A 35mm camera movement. Not a picture of one — the mechanism, built here
   out of CSS 3D geometry and run at its real cadence:

     · a rotary disc shutter, 180° open, ONE revolution per frame
     · a film gate with the aperture plate around it
     · raw stock threaded down through the gate on 4-perf pitch
     · two 12-tooth sprocket drums, so exactly 3 frames per revolution
     · a Latham loop above and below the gate, breathing once per frame as it
       absorbs the difference between continuous drum motion and intermittent
       pull-down

   Mechanically honest, and every number below follows from the one above it:
   perforation pitch is exactly frame-pitch/4; the drums turn once per three
   frames because they carry twelve teeth; the strip is dead still while the
   shutter is open and pulls down one full frame while it is closed. That is
   what an intermittent movement IS, and it is why the thing reads as a
   machine rather than as a loop of moving rectangles.

   WHY THIS IDEA FOR THIS PAGE
   ───────────────────────────
   The page is the media practice: journalists, videographers, editors, a
   newsroom with distribution. Its argument is that a practice is what decides
   what gets on the record. So the film ABOVE the gate is blank — raw stock,
   nothing on it — and the film BELOW the gate carries an image. The gate is
   where the practice happens. The exposure state is a property of POSITION,
   not of any one cell, which is also why it costs nothing: .mv-veil is a
   fixed overlay on the path, not a per-frame class swap.

   It is deliberately nothing like the two technology heroes shipped the same
   night, which are both static rectilinear assemblies that close and rest:
   /technology "THE ASSEMBLY" (a machined enclosure, tray + cells + lid) and
   /services/tech "THE STACK" (seven plates seating into a slab). This one is
   round, optical, and never stops — a mechanism running, not a system built.
   And it is not /services/media-production's "THE CONFORM", which is flat,
   horizontal, and made of edit data. Camera vs. edit bay: production side and
   post side of the same practice, and they must not look like each other.

   WHY NO CANVAS / WebGL
   ─────────────────────
   Measured workload: ~86 opaque untextured quads, no per-pixel lighting, no
   image sampling. That is precisely what the compositor already draws for
   free from CSS transforms. A WebGL context here would add a shader compile
   on first paint (~30–80ms), DPR + resize + context-loss handling, and a
   second live GL context on iOS — for geometry the browser can already do.
   Same measurement that kept mediaroom.css off canvas; it has not changed.
   Zero image bytes: nothing in this hero downloads.

   FRAME BUDGET — the actual claim
   ───────────────────────────────
   Target: 60fps on desktop (16.67ms/frame at 1440×900) and on an iPhone-class
   device at 390×844. Steady state main-thread cost of this hero is ZERO. There
   is NO JavaScript for the animation — no rAF, no scroll listener, no
   IntersectionObserver, no per-frame style write. Every moving property is
   `transform` or `opacity` on a declarative CSS animation, so the whole
   mechanism runs on the compositor. Nothing here reads layout, ever.
   Animating layers: 1 rig + 1 strip + 1 shutter + 1 gate light + 1 beam +
   2 drums + 2 loops + 2 counter reels = 11. Everything else is static
   geometry drawn into its parent.

   SYNC WITHOUT A CLOCK. The shutter, the pull-down, the gate light, the loop
   breath and the frame counter are separate CSS animations that must stay in
   lockstep forever. They do, because every period is an integer multiple of
   --mv-cyc and they all start at the same instant (page load, no delay on the
   drivers). No JS scheduler, no drift.

   COLOUR AND UNITS, STATED RATHER THAN INHERITED
   ──────────────────────────────────────────────
   Authored in sRGB. Palette is the locked site set ONLY: ink #0a0a0a, bone
   rgb(245,241,232) at low alpha for lit metal, red #c8102e for the light and
   the seams, --red-text #e8455a for the small mono type (--red measures
   3.37:1 on ink and must never carry small text — site.css:14). No new hue.
   Geometry in CSS px inside `perspective: 1500px`; angles in deg; type in rem.

   ALWAYS DARK, BOTH THEMES. This site's default theme is LIGHT (the head
   script sets data-theme="light" unless localStorage says otherwise), so a
   hero that pins only its background paints near-black type on near-black.
   The full dark token set is re-declared on the section. Same convention as
   .page-hero / .sv-close in site.css and .stk-hero in tech-stack-hero.css.

   FLASH SAFETY. The gate light pulses once per --mv-cyc = 0.72s → 1.39Hz,
   comfortably under the WCAG 2.3.1 three-flashes-per-second threshold, over
   ~2% of the viewport. Deliberately not the real 24fps, which would both
   strobe and read as noise.

   REDUCED MOTION / NO JS. Every animation is gated behind html.js-motion AND
   inside `prefers-reduced-motion: no-preference`. The RESTING styles are the
   settled, threaded, shutter-open state — so a reduced-motion or no-JS
   visitor gets the finished mechanism as a still photograph of an object,
   never an empty box and never a half-built one.
   ══════════════════════════════════════════════════════════════════════════ */

.mv-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #0a0a0a;
  /* T-1095: 92svh was a full-screen slab, and every other hero on the site
     was one too, which is most of why the pages read as the same page five
     times. The heroes are now deliberately different heights — index 100vh
     (a splash, and the only one), /technology 78svh, this 74svh, /tech
     72svh, /services/media-production 68svh. */
  min-height: 74vh;
  min-height: 74svh;
  display: flex;
  align-items: center;
  padding: clamp(7rem, 13vw, 10rem) 0 clamp(3.5rem, 7vw, 5.5rem);

  /* Full dark token set, re-declared — see header note. */
  --ink: #0a0a0a;
  --on-black: #f5f1e8;
  --on-black-mute: rgba(245,241,232,.58);
  --on-black-faint: rgba(245,241,232,.52);
  --red-text: #e8455a;

  /* ── THE ONE SET OF NUMBERS ────────────────────────────────────────
     Everything below derives from these. --mv-p is the 4-perf frame
     pitch; the perforation gradient period is --mv-p / 4 because that
     is what "4-perf" means. --mv-cyc is one frame cycle: shutter open
     for the first half, pull-down in the second. */
  --mv-p: 74px;    /* frame pitch (4 perforations)                     */
  --mv-w: 128px;   /* stock width, incl. two 13px perforation bands    */
  --mv-cyc: .72s;  /* one frame cycle → 1.39Hz, see FLASH SAFETY       */
  --mv-strip: 4.32s;  /* 6 × --mv-cyc, the strip content period        */
  --mv-drum: 2.16s;   /* 3 × --mv-cyc: 12 teeth ÷ 4 perf-per-frame     */
  --mv-r: 47px;    /* drum radius: 12 slats × 24.6px arc ÷ 2π          */
  --mv-k: 1.04;    /* one scale for the whole mechanism                */
}

.mv-hero .inner {
  position: relative;
  z-index: 3;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 clamp(1.2rem, 4vw, 2.5rem);
}

/* ── AMBIENT GROUND ───────────────────────────────────────────────────
   ONE dominant red radial, sitting behind the mechanism where the light
   comes from (ART-GUIDE §6). Static: the object carries the motion, and
   a breathing radial at this size would sit below the perception
   threshold and buy nothing but a composited layer. */
.mv-hero__ground {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 26% 34% at 70% 46%, rgba(255,238,224,.10) 0%, transparent 66%),
    radial-gradient(ellipse 46% 56% at 72% 44%, rgba(200,16,46,.30) 0%, rgba(200,16,46,.10) 42%, transparent 70%),
    radial-gradient(ellipse 62% 46% at 14% 4%, rgba(245,241,232,.055) 0%, transparent 60%),
    linear-gradient(to bottom, rgba(10,10,10,.62) 0%, transparent 24%, transparent 72%, #0a0a0a 100%);
}

/* Static film grain. Never animated — an animated grain re-rasterizes a
   full-viewport layer every frame and is the single most expensive thing
   that could be added to this page (ART-GUIDE §1). */
.mv-hero__grain {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: .05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ══════════════════════════════════════════════════════════════════════
   THE SCENE
   .mv-stage / .mv-rig / .mv-drum must never carry opacity, filter, mask,
   clip-path, mix-blend-mode or contain:paint. Any one of those forces
   transform-style to flat and the mechanism collapses into a flat picture.
   Load-bearing, not stylistic — the same constraint documented for
   /technology's hero in ART-GUIDE §5.
   ══════════════════════════════════════════════════════════════════════ */

.mv-scene {
  position: absolute;
  z-index: 1;
  inset: 0 0 0 auto;
  width: 58%;
  pointer-events: none;
}

.mv-stage {
  position: absolute;
  inset: 0;
  perspective: 1500px;
  perspective-origin: 50% 46%;
}

.mv-rig {
  position: absolute;
  left: 50%;
  top: 46%;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  /* SETTLED POSE. This is what a no-JS / reduced-motion visitor sees, so it
     is deliberately the finished pose and not the entry pose. 27° of yaw is
     the number that makes the drums read as cylinders: below about 18° the
     slats foreshorten to nothing and both drums read as flat bars. */
  transform: rotateX(8deg) rotateY(-19deg);
}

/* The whole mechanism scales with the viewport from one place. */
.mv-body {
  position: absolute;
  transform-style: preserve-3d;
  transform: scale(var(--mv-k, 1));
}

/* ── THE APERTURE PLATE ───────────────────────────────────────────────
   The machined face the whole movement is built on. Front plate at z=0,
   a raised bezel at z=+10 that the film runs in front of, and a back
   plate at z=-46 so the object has depth when yawed. */
.mv-plate {
  position: absolute;
  left: 0; top: 0;
  width: 316px;
  height: 396px;
  margin: -198px 0 0 -158px;
  transform-style: preserve-3d;
}

.mv-plate__face {
  position: absolute;
  inset: 0;
  /* Milled aluminium: a fine vertical tool pattern under a broad key from
     the upper left, then hard edges. Without the tool pattern a flat fill at
     this size reads as glass, which is exactly how the first pass looked. */
  background:
    repeating-linear-gradient(90deg,
      rgba(245,241,232,.028) 0px, rgba(245,241,232,.028) 1px,
      rgba(10,10,10,.05) 1px, rgba(10,10,10,.05) 3px),
    linear-gradient(148deg, rgba(245,241,232,.15) 0%, rgba(245,241,232,.05) 30%, rgba(10,10,10,.30) 72%, rgba(10,10,10,.6) 100%),
    #0f1216;
  box-shadow:
    inset 0 1px 0 rgba(245,241,232,.42),
    inset 1px 0 0 rgba(245,241,232,.16),
    inset -1px 0 0 rgba(10,10,10,.9),
    inset 0 -1px 0 rgba(10,10,10,.95),
    0 34px 80px rgba(0,0,0,.85);
}

.mv-plate__back {
  position: absolute;
  inset: 14px 10px;
  transform: translateZ(-44px);
  background: linear-gradient(160deg, #101318 0%, #08090c 70%);
  box-shadow: inset 0 1px 0 rgba(245,241,232,.10);
}

/* The channel the film runs in: a recess milled into the face. */
.mv-plate__channel {
  position: absolute;
  left: 50%;
  top: 22px;
  bottom: 22px;
  width: calc(var(--mv-w) + 26px);
  margin-left: calc((var(--mv-w) + 26px) / -2);
  transform: translateZ(4px);
  background: linear-gradient(90deg, rgba(6,6,7,.94) 0%, rgba(6,6,7,.7) 20%, rgba(6,6,7,.7) 80%, rgba(6,6,7,.96) 100%);
  box-shadow: inset 1px 0 0 rgba(245,241,232,.16), inset -1px 0 0 rgba(245,241,232,.09);
}

/* Two machined screw heads. Detail that costs nothing and is the
   difference between "a rectangle" and "a milled part". */
.mv-plate__screw {
  position: absolute;
  width: 11px; height: 11px;
  border-radius: 50%;
  transform: translateZ(6px);
  background: radial-gradient(circle at 34% 30%, rgba(245,241,232,.34) 0%, rgba(245,241,232,.10) 46%, rgba(10,10,10,.9) 100%);
  box-shadow: 0 1px 0 rgba(245,241,232,.10);
}
.mv-plate__screw--a { left: 16px;  top: 16px;  }
.mv-plate__screw--b { right: 16px; top: 16px;  }
.mv-plate__screw--c { left: 16px;  bottom: 16px; }
.mv-plate__screw--d { right: 16px; bottom: 16px; }

/* ── THE FILM PATH ────────────────────────────────────────────────────
   The straight run between the two loops. overflow:hidden on the WINDOW,
   never on a preserve-3d ancestor. */
.mv-path {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--mv-w);
  height: calc(var(--mv-p) * 4);            /* four frames visible */
  margin: calc(var(--mv-p) * -2) 0 0 calc(var(--mv-w) / -2);
  transform: translateZ(10px);
  overflow: hidden;
}

/* Static edge light on the running stock. Sits above the strip and below the
   veil in paint order, and never moves — it is the room light, not the film. */
.mv-path::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background:
    linear-gradient(90deg,
      rgba(245,241,232,.30) 0px, rgba(245,241,232,.06) 3px, transparent 9px,
      transparent calc(100% - 9px), rgba(245,241,232,.05) calc(100% - 3px), rgba(245,241,232,.20) 100%);
}

.mv-strip {
  position: absolute;
  left: 0;
  top: calc(var(--mv-p) * -6);
  width: 100%;
  height: calc(var(--mv-p) * 12);
  /* Rest position = threaded and seated. */
  transform: translateY(0);
}

/* ONE FRAME CELL. --i 0..11. The image area is a 4:3-ish window inset from
   two 13px perforation bands, which is what 4-perf 35mm looks like. */
.mv-cell {
  position: absolute;
  left: 0;
  width: 100%;
  height: var(--mv-p);
  top: calc(var(--i) * var(--mv-p));
  background: linear-gradient(180deg, #2b2f37 0%, #23262d 46%, #191c21 100%);
}

/* The two perforation bands. A repeating gradient, NOT elements: the perf
   period is exactly --mv-p / 4 = 19px, which is what "4-perf" means, and
   19px of gradient costs nothing where 48 little divs would each be a box
   to lay out. */
.mv-cell::before,
.mv-cell::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 13px;
  background:
    repeating-linear-gradient(
      180deg,
      rgba(12,13,16,.98) 0px,
      rgba(12,13,16,.98) 5.5px,
      rgba(245,241,232,.44) 5.5px,
      rgba(245,241,232,.44) 13px,
      rgba(12,13,16,.98) 13px,
      rgba(12,13,16,.98) 18.5px
    );
  box-shadow: inset 0 0 0 1px rgba(6,6,7,.85);
}
.mv-cell::before { left: 0; }
.mv-cell::after  { right: 0; }

/* THE LATENT IMAGE. Six distinct frames, repeated twice down the 12-cell
   strip so a 6-frame pull-down loop tiles seamlessly. Abstract luminance
   fields, not pictures — there is no photograph anywhere in this hero, by
   instruction. Each is a horizon + a light source, which is what a frame of
   exposed stock actually looks like on a lightbox. */
.mv-cell__img {
  position: absolute;
  left: 15px; right: 15px;
  top: 6px; bottom: 8px;
  background: #0d0f12;
}
.mv-cell[data-f="0"] .mv-cell__img { background: linear-gradient(172deg, rgba(245,241,232,.72) 0%, rgba(245,241,232,.20) 34%, rgba(10,10,10,.92) 76%), radial-gradient(circle at 76% 24%, rgba(200,16,46,.62) 0%, transparent 48%), #0c0e11; }
.mv-cell[data-f="1"] .mv-cell__img { background: linear-gradient(94deg,  rgba(10,10,10,.9) 0%, rgba(245,241,232,.62) 52%, rgba(10,10,10,.94) 96%), radial-gradient(ellipse at 24% 76%, rgba(200,16,46,.5) 0%, transparent 56%), #0c0e11; }
.mv-cell[data-f="2"] .mv-cell__img { background: linear-gradient(186deg, rgba(245,241,232,.16) 0%, rgba(245,241,232,.80) 26%, rgba(10,10,10,.96) 70%), #0c0e11; }
.mv-cell[data-f="3"] .mv-cell__img { background: linear-gradient(166deg, rgba(245,241,232,.52) 0%, rgba(10,10,10,.9) 58%), radial-gradient(circle at 32% 36%, rgba(245,241,232,.78) 0%, transparent 34%), #0c0e11; }
.mv-cell[data-f="4"] .mv-cell__img { background: linear-gradient(202deg, rgba(200,16,46,.72) 0%, rgba(245,241,232,.34) 38%, rgba(10,10,10,.94) 82%), #0c0e11; }
.mv-cell[data-f="5"] .mv-cell__img { background: linear-gradient(178deg, rgba(10,10,10,.86) 0%, rgba(245,241,232,.66) 50%, rgba(10,10,10,.92) 92%), radial-gradient(ellipse at 64% 20%, rgba(245,241,232,.6) 0%, transparent 42%), #0c0e11; }

/* THE VEIL — the whole idea of the piece, and it costs one element.
   Fixed to the PATH, not the strip: everything above the gate line is raw
   unexposed stock, everything below it carries an image. The film moves
   through it, so frames arrive blank and leave with something on them. */
.mv-veil {
  position: absolute;
  left: 0; right: 0;
  top: 0;
  height: calc(var(--mv-p) * 1.46);
  z-index: 3;
  background: linear-gradient(180deg, #24272d 0%, #24272d 58%, rgba(36,39,45,.92) 82%, rgba(36,39,45,0) 100%);
}
/* The perforations keep running through the raw stock — it is the same
   film, only unexposed. Without this the veil reads as a lid. */
.mv-veil::before,
.mv-veil::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 13px;
  background: repeating-linear-gradient(
    180deg,
    rgba(12,13,16,.98) 0px, rgba(12,13,16,.98) 5.5px,
    rgba(245,241,232,.40) 5.5px, rgba(245,241,232,.40) 13px,
    rgba(12,13,16,.98) 13px, rgba(12,13,16,.98) 18.5px);
}
.mv-veil::before { left: 0; }
.mv-veil::after  { right: 0; }

/* ── THE GATE ─────────────────────────────────────────────────────────
   The aperture the frame is exposed through. Four machined edges around a
   window; the film shows through the middle. */
.mv-gate {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--mv-w) + 18px);
  height: calc(var(--mv-p) + 12px);
  margin: calc((var(--mv-p) + 12px) / -2) 0 0 calc((var(--mv-w) + 18px) / -2);
  transform: translate3d(0, -40px, 19px);
  transform-style: preserve-3d;
}

.mv-gate__edge {
  position: absolute;
  background: linear-gradient(180deg, rgba(245,241,232,.58) 0%, rgba(245,241,232,.18) 52%, rgba(10,10,10,.92) 100%);
  box-shadow: 0 0 0 1px rgba(6,6,7,.92), inset 0 1px 0 rgba(245,241,232,.72);
}
.mv-gate__edge--t { left: 0; right: 0; top: 0; height: 12px; }
.mv-gate__edge--b { left: 0; right: 0; bottom: 0; height: 12px; }
.mv-gate__edge--l { top: 12px; bottom: 12px; left: 0; width: 15px; }
.mv-gate__edge--r { top: 12px; bottom: 12px; right: 0; width: 15px; }

/* THE EXPOSURE. Sits in the aperture, behind the shutter. At rest (reduced
   motion) it is held at the open value, so the still frame is a frame being
   exposed rather than a dead machine. */
.mv-gate__light {
  position: absolute;
  left: 15px; right: 15px;
  top: 12px; bottom: 12px;
  transform: translateZ(-4px);
  opacity: .82;
  background:
    radial-gradient(ellipse 80% 92% at 50% 44%, rgba(255,250,244,1) 0%, rgba(255,244,232,.86) 26%, rgba(245,241,232,.52) 52%, rgba(200,16,46,.42) 80%, transparent 100%);
  box-shadow: 0 0 44px 12px rgba(255,240,226,.30), 0 0 90px 30px rgba(200,16,46,.22);
}

/* The registration pin: holds the frame still while the shutter is open.
   Static geometry — it does the work the eye cannot see. */
.mv-gate__pin {
  position: absolute;
  left: -9px;
  top: 50%;
  width: 16px; height: 6px;
  margin-top: -3px;
  transform: translateZ(6px);
  background: linear-gradient(180deg, rgba(245,241,232,.5) 0%, rgba(245,241,232,.16) 100%);
  box-shadow: 0 0 0 1px rgba(10,10,10,.8);
}

/* ── THE BEAM ─────────────────────────────────────────────────────────
   The light on its way to the gate, from a lens that is off-frame. A wedge,
   not a cone of god-rays: this is the inside of a camera body, where the
   only light is the one going through the aperture. */
.mv-beam {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 268px;
  height: 168px;
  margin: -84px 0 0 -256px;
  transform: translate3d(0, -40px, 74px) rotateY(28deg);
  transform-origin: 100% 50%;
  opacity: .5;
  clip-path: polygon(0% 6%, 100% 40%, 100% 60%, 0% 94%);
  background: linear-gradient(90deg, rgba(255,246,238,0) 0%, rgba(255,246,238,.035) 50%, rgba(255,246,238,.14) 86%, rgba(255,246,238,.28) 100%);
}

/* ── THE ROTARY DISC SHUTTER ──────────────────────────────────────────
   180° open, one revolution per frame. The half-disc is a hard-stop conic
   gradient, so the "cut away" half is genuinely empty and you can see the
   gate through it — which is the entire mechanism in one element.
   .mv-shutter is the mount (entry only); .mv-shutter__disc is the spin, so
   the ease-out seat and the infinite linear rotation never fight. */
.mv-shutter {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 190px; height: 190px;
  margin: -95px 0 0 -95px;
  transform: translate3d(52px, -92px, 36px) rotateY(-7deg);
  transform-style: preserve-3d;
}

.mv-shutter__disc {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Rest angle: the opening is over the gate, i.e. a frame being exposed. */
  transform: rotate(196deg);
  background:
    conic-gradient(from 0deg,
      #1c1f24 0deg,
      #2b3038 28deg,
      #454c57 74deg,
      #2e333b 118deg,
      #191c21 178deg,
      transparent 179.6deg,
      transparent 360deg);
  /* NO inset box-shadow here. A ring on a border-radius:50% element draws the
     full circle regardless of the conic gradient being transparent across the
     cut-away half — it painted a ghost outline of the missing 180deg. */
}

/* The blade's leading edge, lit. Half a ring, so it only draws on the solid
   half — the detail that stops the disc reading as a flat pie chart. */
.mv-shutter__edge {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  transform: rotate(196deg);
  background: conic-gradient(from 0deg,
    rgba(200,16,46,.95) 0deg,
    rgba(245,241,232,.55) 26deg,
    rgba(245,241,232,.16) 120deg,
    rgba(200,16,46,.85) 178deg,
    transparent 179.6deg,
    transparent 360deg);
  -webkit-mask: radial-gradient(circle, transparent 0 92%, #000 93%, #000 100%);
  mask: radial-gradient(circle, transparent 0 92%, #000 93%, #000 100%);
}

/* The hub and the drive shaft. */
.mv-shutter__hub {
  position: absolute;
  left: 50%; top: 50%;
  width: 46px; height: 46px;
  margin: -23px 0 0 -23px;
  border-radius: 50%;
  transform: translateZ(5px);
  background:
    radial-gradient(circle at 36% 30%, rgba(245,241,232,.42) 0%, rgba(245,241,232,.12) 40%, #0d0f12 78%);
  box-shadow: 0 0 0 1px rgba(10,10,10,.9), 0 6px 20px rgba(0,0,0,.6);
}

/* ── THE SPROCKET DRUMS ───────────────────────────────────────────────
   A real cylinder: twelve slats at 30° each, pushed out to --mv-r. Twelve
   teeth over 4-perf pitch is three frames per revolution, which is why
   --mv-drum is 3 × --mv-cyc and not a number picked to look right. */
.mv-drum {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0; height: 0;
  transform-style: preserve-3d;
}
.mv-drum--feed { transform: translate3d(0, -196px, 26px); }
.mv-drum--take { transform: translate3d(0,  196px, 26px); }

.mv-drum__cage {
  position: absolute;
  transform-style: preserve-3d;
}

.mv-drum__s {
  position: absolute;
  left: 0; top: 0;
  width: 152px;
  height: 26px;
  margin: -13px 0 0 -76px;
  transform: rotateX(calc(var(--i) * 30deg)) translateZ(var(--mv-r));
  background:
    linear-gradient(180deg, rgba(245,241,232,.22) 0%, rgba(245,241,232,.055) 52%, rgba(10,10,10,.86) 100%),
    #14171c;
  box-shadow: inset 0 1px 0 rgba(245,241,232,.20);
}
/* The two sprocket teeth on every slat, one per perforation row. */
.mv-drum__s::before,
.mv-drum__s::after {
  content: "";
  position: absolute;
  top: 8px;
  width: 6px; height: 10px;
  background: linear-gradient(180deg, rgba(245,241,232,.8) 0%, rgba(245,241,232,.26) 100%);
  box-shadow: 0 0 0 1px rgba(10,10,10,.8);
}
.mv-drum__s::before { left: 22px; }
.mv-drum__s::after  { right: 22px; }

/* CYLINDRICAL SHADE. Fixed in world space, in front of the rotating cage:
   dark at both silhouettes, a specular band a third of the way down. A slat's
   own gradient rotates with the slat, so without this the drum reads as a
   ladder rather than as a cylinder — it did, in the first pass. */
.mv-drum__shade {
  position: absolute;
  left: 0; top: 0;
  width: 152px;
  height: calc(var(--mv-r) * 2 + 4px);
  margin: calc((var(--mv-r) + 2px) * -1) 0 0 -76px;
  transform: translateZ(calc(var(--mv-r) + 1px));
  background:
    linear-gradient(180deg,
      rgba(6,6,7,.94) 0%,
      rgba(6,6,7,.55) 12%,
      rgba(245,241,232,.13) 30%,
      rgba(245,241,232,.02) 46%,
      rgba(6,6,7,.42) 74%,
      rgba(6,6,7,.95) 100%);
}

/* The two flanges that cap the drum, so it does not read as a floating
   ring of slats. Circles standing on the drum axis. */
.mv-drum__flange {
  position: absolute;
  left: 0; top: 0;
  width: calc(var(--mv-r) * 2 + 14px);
  height: calc(var(--mv-r) * 2 + 14px);
  margin: calc((var(--mv-r) + 7px) * -1) 0 0 calc((var(--mv-r) + 7px) * -1);
  border-radius: 50%;
  transform: rotateY(90deg) translateZ(var(--x));
  background:
    radial-gradient(circle at 38% 32%, rgba(245,241,232,.18) 0%, rgba(245,241,232,.05) 44%, #0c0e12 82%);
  box-shadow: inset 0 0 0 1px rgba(245,241,232,.13);
}

/* ── THE LATHAM LOOPS ─────────────────────────────────────────────────
   Slack film between the continuously-turning drum and the intermittently-
   moving gate. Three tangent segments each, bulging toward the camera; the
   whole loop breathes once per frame cycle because that is literally its
   job — it absorbs the pull-down. */
.mv-loop {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0; height: 0;
  transform-style: preserve-3d;
  transform-origin: 50% 50%;
}
.mv-loop--top { transform: translate3d(0, -140px, 0); }
.mv-loop--bot { transform: translate3d(0,  140px, 0); }

.mv-loop__s {
  position: absolute;
  left: 0; top: 0;
  width: var(--mv-w);
  height: 44px;
  margin: -22px 0 0 calc(var(--mv-w) / -2);
  background:
    linear-gradient(180deg, rgba(245,241,232,.13) 0%, rgba(245,241,232,.035) 55%, rgba(10,10,10,.82) 100%),
    #191c22;
  box-shadow: inset 0 1px 0 rgba(245,241,232,.16);
}
.mv-loop__s::before,
.mv-loop__s::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  width: 13px;
  background: repeating-linear-gradient(
    180deg,
    rgba(12,13,16,.96) 0px, rgba(12,13,16,.96) 5.5px,
    rgba(245,241,232,.36) 5.5px, rgba(245,241,232,.36) 13px,
    rgba(12,13,16,.96) 13px, rgba(12,13,16,.96) 18.5px);
}
.mv-loop__s::before { left: 0; }
.mv-loop__s::after  { right: 0; }

/* Tangent placement of the three segments. Hand-placed rather than
   computed: the arc is short enough that three chords read as a curve. */
.mv-loop--top .mv-loop__s:nth-child(1) { transform: translate3d(0, -34px, 6px)  rotateX(-34deg); }
.mv-loop--top .mv-loop__s:nth-child(2) { transform: translate3d(0,   0px, 22px) rotateX(0deg);    }
.mv-loop--top .mv-loop__s:nth-child(3) { transform: translate3d(0,  34px, 6px)  rotateX(34deg);  }
.mv-loop--bot .mv-loop__s:nth-child(1) { transform: translate3d(0, -34px, 6px)  rotateX(-34deg); }
.mv-loop--bot .mv-loop__s:nth-child(2) { transform: translate3d(0,   0px, 22px) rotateX(0deg);    }
.mv-loop--bot .mv-loop__s:nth-child(3) { transform: translate3d(0,  34px, 6px)  rotateX(34deg);  }

/* ── THE READOUT ──────────────────────────────────────────────────────
   A mechanical footage counter, and the spec of the machine you are
   watching. The counter is two digit reels stepping on the same clock as
   the pull-down — no JS, no setInterval, and it can never drift out of
   sync with the mechanism because it shares --mv-cyc. */
.mv-readout {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate3d(-206px, 262px, 46px) rotateY(15deg);
  transform-origin: 0 50%;
  /* 168px orphaned the word SHUTTER onto a second line: the spec string sets
     at ~181px in JetBrains Mono at 8.5px/.16em. Sized to the string, with the
     nowrap below as the guarantee — a fallback mono face is wider still and
     would re-break it. Nothing clips: the readout is absolutely positioned in
     a scene far wider than it, and .mv-scene does not hide overflow. */
  width: 204px;
  font-family: var(--font-mono);
}

.mv-counter {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding: 5px 6px;
  background: linear-gradient(180deg, rgba(10,10,10,.94) 0%, rgba(16,18,22,.94) 100%);
  box-shadow: inset 0 0 0 1px rgba(245,241,232,.14);
}

.mv-reel {
  position: relative;
  width: 15px;
  height: 20px;
  overflow: hidden;
  background: rgba(10,10,10,.9);
}
.mv-reel__strip {
  position: absolute;
  left: 0; top: 0;
  width: 100%;
  transform: translateY(0);
}
.mv-reel__strip b {
  display: block;
  height: 20px;
  line-height: 20px;
  text-align: center;
  font-size: 12px;
  font-weight: 500;
  color: var(--red-text);
}
.mv-counter__unit {
  align-self: center;
  margin-left: 7px;
  font-size: 8.5px;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: rgba(245,241,232,.42);
}

.mv-readout__spec {
  margin: 7px 0 0;
  white-space: nowrap;
  font-size: 8.5px;
  line-height: 1.7;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(245,241,232,.34);
}
.mv-readout__spec b { color: rgba(245,241,232,.62); font-weight: 500; }

/* ══════════════════════════════════════════════════════════════════════
   COPY
   ══════════════════════════════════════════════════════════════════════ */

.mv-copy { max-width: 520px; }

.mv-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 11px;
  line-height: 1;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--red-text);
  margin: 0 0 1.3rem;
}
.mv-eyebrow::before {
  content: "";
  width: 36px; height: 1px;
  background: var(--red);
  display: inline-block;
}

.mv-h1 {
  font-family: var(--font-display);
  font-weight: 400;
  /* T-1041: was clamp(2.3rem, 5vw, 3.7rem) */
  font-size: clamp(2rem, 4vw, 3rem);
  line-height: 1.08;
  letter-spacing: -.02em;
  color: var(--on-black);
  margin: 0 0 1.2rem;
  text-shadow: 0 2px 30px rgba(10,10,10,.8);
}
.mv-h1 em { font-style: italic; color: var(--red); }

.mv-lede {
  font-family: var(--font-display);
  font-weight: 400;
  /* was clamp(1rem, 1.5vw, 1.22rem) */
  font-size: clamp(.92rem, 1.2vw, 1.06rem);
  line-height: 1.58;
  color: var(--on-black-mute);
  margin: 0 0 2rem;
  max-width: 460px;
  text-shadow: 0 1px 16px rgba(10,10,10,.8);
}
.mv-lede em { font-style: italic; color: var(--on-black); }

.mv-ctas { display: flex; flex-wrap: wrap; gap: .8rem; }

/* ══════════════════════════════════════════════════════════════════════
   MOTION
   Gated twice: html.js-motion (set by the page's head snippet only when JS
   + IntersectionObserver + full motion are all available) AND the media
   query, so the CSS is still correct if the snippet never runs.
   ══════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: no-preference) {

  /* ── ENTRY. Black frame at t=0, complete machine by t≈1.9s, and it is
     ALREADY RUNNING before the entry finishes — the first exposure fires at
     1.15s, well inside the two seconds a visitor gives a hero. ───────── */

  html.js-motion .mv-rig {
    animation: mvCrane 2.1s cubic-bezier(.22,.61,.24,1) both;
  }
  @keyframes mvCrane {
    from { transform: rotateX(21deg) rotateY(-44deg) scale(.80); }
    to   { transform: rotateX(8deg) rotateY(-19deg) scale(1); }
  }

  html.js-motion .mv-plate__face,
  html.js-motion .mv-plate__back,
  html.js-motion .mv-plate__channel {
    animation: mvRise .62s cubic-bezier(.2,.8,.25,1) both;
  }
  html.js-motion .mv-plate__back { animation-delay: .04s; }
  @keyframes mvRise {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  html.js-motion .mv-plate__screw { animation: mvRise .4s ease-out both; animation-delay: .5s; }

  html.js-motion .mv-drum--feed,
  html.js-motion .mv-drum--take { animation: mvDrumIn .8s cubic-bezier(.2,.75,.3,1) both; }
  html.js-motion .mv-drum--feed { animation-delay: .3s; }
  html.js-motion .mv-drum--take { animation-delay: .38s; }
  @keyframes mvDrumIn {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* The stock threads: the strip slides down into the path from above and
     the loops form behind it. */
  html.js-motion .mv-path { animation: mvThread .78s cubic-bezier(.2,.8,.25,1) both; animation-delay: .42s; }
  @keyframes mvThread {
    from { opacity: 0; clip-path: inset(0 0 100% 0); }
    to   { opacity: 1; clip-path: inset(0 0 0 0); }
  }

  html.js-motion .mv-loop__s { animation: mvLoopIn .6s cubic-bezier(.2,.8,.25,1) both; }
  html.js-motion .mv-loop--top .mv-loop__s:nth-child(1) { animation-delay: .60s; }
  html.js-motion .mv-loop--top .mv-loop__s:nth-child(2) { animation-delay: .66s; }
  html.js-motion .mv-loop--top .mv-loop__s:nth-child(3) { animation-delay: .72s; }
  html.js-motion .mv-loop--bot .mv-loop__s:nth-child(1) { animation-delay: .72s; }
  html.js-motion .mv-loop--bot .mv-loop__s:nth-child(2) { animation-delay: .78s; }
  html.js-motion .mv-loop--bot .mv-loop__s:nth-child(3) { animation-delay: .84s; }
  @keyframes mvLoopIn { from { opacity: 0; } to { opacity: 1; } }

  html.js-motion .mv-gate__edge { animation: mvRise .44s cubic-bezier(.2,.8,.25,1) both; animation-delay: .82s; }
  html.js-motion .mv-gate__pin  { animation: mvRise .3s ease-out both; animation-delay: 1.0s; }

  html.js-motion .mv-shutter { animation: mvSeat .7s cubic-bezier(.2,.72,.28,1) both; animation-delay: .88s; }
  @keyframes mvSeat {
    from { opacity: 0; transform: translate3d(52px, -92px, 200px) rotateY(-7deg); }
    to   { opacity: 1; transform: translate3d(52px, -92px, 36px) rotateY(-7deg); }
  }

  html.js-motion .mv-readout { animation: mvRise .5s ease-out both; animation-delay: 1.25s; }

  /* ── STEADY STATE. Everything below runs forever and shares one clock. */

  /* THE SPIN. One revolution per frame cycle, linear, forever. Delayed to
     the instant the disc seats, so the entry and the run never fight for
     the same transform. */
  html.js-motion .mv-shutter__disc,
  html.js-motion .mv-shutter__edge {
    animation: mvSpin var(--mv-cyc) linear infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvSpin {
    from { transform: rotate(16deg); }
    to   { transform: rotate(376deg); }
  }

  /* THE EXPOSURE. Open for the first half of the cycle, shut for the
     second — which is what a 180° shutter means. 1.39Hz. */
  html.js-motion .mv-gate__light {
    animation: mvExpose var(--mv-cyc) linear infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvExpose {
    0%   { opacity: .06; }
    5%   { opacity: .96; }
    42%  { opacity: .80; }
    50%  { opacity: .05; }
    100% { opacity: .05; }
  }

  html.js-motion .mv-beam {
    animation: mvBeam var(--mv-cyc) linear infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvBeam {
    0%   { opacity: .10; }
    5%   { opacity: .62; }
    42%  { opacity: .50; }
    50%  { opacity: .08; }
    100% { opacity: .08; }
  }

  /* THE PULL-DOWN. Dead still while the shutter is open (0–50%), one full
     frame pitch while it is closed (55–80%), still again. Six pull-downs
     per loop, and the strip content has period 6, so it tiles seamlessly. */
  html.js-motion .mv-strip {
    animation: mvPull var(--mv-strip) cubic-bezier(.76,0,.18,1) infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvPull {
    0%,     9.17%  { transform: translateY(0); }
    13.33%, 25.84% { transform: translateY(calc(var(--mv-p) * -1)); }
    30.00%, 42.50% { transform: translateY(calc(var(--mv-p) * -2)); }
    46.67%, 59.17% { transform: translateY(calc(var(--mv-p) * -3)); }
    63.33%, 75.84% { transform: translateY(calc(var(--mv-p) * -4)); }
    80.00%, 92.50% { transform: translateY(calc(var(--mv-p) * -5)); }
    96.67%, 100%   { transform: translateY(calc(var(--mv-p) * -6)); }
  }

  /* THE DRUMS. Continuous, three frames per revolution. Front face travels
     downward with the film, which is why this is a NEGATIVE rotateX. */
  html.js-motion .mv-drum__cage {
    animation: mvRoll var(--mv-drum) linear infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvRoll {
    from { transform: rotateX(0deg); }
    to   { transform: rotateX(-360deg); }
  }

  /* THE LOOP BREATH. The slack takes up the pull-down, once per cycle. */
  html.js-motion .mv-loop {
    animation: mvBreath var(--mv-cyc) cubic-bezier(.4,0,.3,1) infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvBreath {
    0%, 52%  { }
    72%      { }
    100%     { }
  }
  html.js-motion .mv-loop--top { animation-name: mvBreathTop; }
  html.js-motion .mv-loop--bot { animation-name: mvBreathBot; }
  @keyframes mvBreathTop {
    0%, 52%  { transform: translate3d(0, -140px, 0) scale(1); }
    74%      { transform: translate3d(0, -140px, 0) scale(.965); }
    100%     { transform: translate3d(0, -140px, 0) scale(1); }
  }
  @keyframes mvBreathBot {
    0%, 52%  { transform: translate3d(0, 140px, 0) scale(1); }
    74%      { transform: translate3d(0, 140px, 0) scale(1.035); }
    100%     { transform: translate3d(0, 140px, 0) scale(1); }
  }

  /* THE COUNTER. Units reel steps once per frame cycle; the tens reel steps
     once per ten. Same clock as everything else, so it counts exactly the
     frames that were exposed. */
  html.js-motion .mv-reel--u .mv-reel__strip {
    animation: mvReelU calc(var(--mv-cyc) * 10) steps(10, end) infinite;
    animation-delay: 1.05s;
  }
  html.js-motion .mv-reel--t .mv-reel__strip {
    animation: mvReelT calc(var(--mv-cyc) * 100) steps(10, end) infinite;
    animation-delay: 1.05s;
  }
  @keyframes mvReelU { from { transform: translateY(0); } to { transform: translateY(-200px); } }
  @keyframes mvReelT { from { transform: translateY(0); } to { transform: translateY(-200px); } }

  /* Copy arrives with the machine, not after it. */
  html.js-motion .mv-copy > * {
    animation: mvCopyIn .8s cubic-bezier(.16,1,.3,1) both;
  }
  html.js-motion .mv-eyebrow { animation-delay: .10s; }
  html.js-motion .mv-h1      { animation-delay: .24s; }
  html.js-motion .mv-lede    { animation-delay: .40s; }
  html.js-motion .mv-ctas    { animation-delay: .56s; }
  @keyframes mvCopyIn {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: none; }
  }
}

/* ══════════════════════════════════════════════════════════════════════
   RESPONSIVE
   The mechanism is real geometry in px, so it scales with ONE number.
   ══════════════════════════════════════════════════════════════════════ */

@media (max-width: 1200px) { .mv-hero { --mv-k: .84; } .mv-scene { width: 52%; } }
@media (max-width: 1000px) { .mv-hero { --mv-k: .74; } }

/* Below 900px the scene stops competing with the copy for the same column
   and takes the top of the frame outright. The object is the point of the
   page; hiding it on a phone would be hiding the page. */
@media (max-width: 900px) {
  .mv-hero {
    display: block;
    min-height: 0;
    padding: clamp(5.5rem, 18vw, 7rem) 0 clamp(3rem, 9vw, 4rem);
    --mv-k: .62;
  }
  .mv-scene {
    position: relative;
    inset: auto;
    width: 100%;
    height: 46svh;
    min-height: 320px;
    margin: 0 0 1.6rem;
  }
  .mv-stage { perspective: 1200px; }
  .mv-rig { top: 50%; }
  .mv-copy { max-width: none; }
  .mv-hero__ground {
    background:
      radial-gradient(ellipse 76% 30% at 50% 24%, rgba(200,16,46,.20) 0%, rgba(200,16,46,.06) 46%, transparent 72%),
      linear-gradient(to bottom, rgba(10,10,10,.5) 0%, transparent 22%, transparent 74%, #0a0a0a 100%);
  }
}

@media (max-width: 560px) {
  .mv-hero { --mv-k: .48; }
  .mv-scene { height: 38svh; min-height: 268px; }
  /* Below this the counter is 4px of type. Illegible detail is not detail,
     it is noise — and it is aria-hidden, so nothing is lost by dropping it. */
  .mv-readout { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   LIGHT THEME
   The hero stays dark in both themes — same rule as .page-hero / .sv-cine /
   .sv-close in site.css. The token set is already re-declared on .mv-hero,
   so this only has to stop the section background being remapped.
   ══════════════════════════════════════════════════════════════════════ */
[data-theme="light"] .mv-hero { background: #0a0a0a; }
[data-theme="light"] .mv-hero .mv-h1,
[data-theme="light"] .mv-hero .mv-lede,
[data-theme="light"] .mv-hero .mv-eyebrow { color: inherit; }
[data-theme="light"] .mv-hero .mv-h1 { color: #f5f1e8; }
[data-theme="light"] .mv-hero .mv-lede { color: rgba(245,241,232,.58); }
[data-theme="light"] .mv-hero .mv-eyebrow { color: #e8455a; }
[data-theme="light"] .mv-hero .btn-cta {
  color: #f5f1e8;
  border-color: rgba(245,241,232,.28);
}
[data-theme="light"] .mv-hero .btn-cta--red { color: #f5f1e8; }
