/* ============================================================
   /courses — "Three disciplines, one foundation" cinematic scroll.

   A sticky stage scrubbed by one scroll rig. Four acts:
     0  the resting frame: PROGRAMS, the intake line, the site plate
     1  Civil     — the drafting film parts, the plate launches
     2  Computer  — the frame cross-fades
     3  Electronics
     4  the pathway slider flies in and loops

   Every visual is CSS or an existing /media/ice-intake photograph; nothing
   here loads a remote font, image or script (production CSP is `default-src
   'self'`). The stage is a deep-navy blueprint field in BOTH themes, the same
   way the site's other hero fields are — §02 navy carries the institution, so
   it is not re-tinted by data-theme.

   Direction (site CSS §0): "drafting film over the blueprint." The two
   shutters ARE the drafting film; the grid veil is the blueprint; the
   photographs are the real thing underneath.

   Animated values are scoped to the sticky stage by
   /js/ice-programs-cinema.js. With JS off or before first frame the
   declarations below are the resting frame — no content is gated on JS.
   ============================================================ */

:root {
  /* pointer parallax, -0.5 … 0.5 */
  --cine-mx: 0;
  --cine-my: 0;

  /* back stack (grid + slider + horizon) */
  --cine-back-opacity: 1;
  --cine-back-x: 0px;
  --cine-back-y: 0px;
  --cine-back-scale: 0.76;

  --cine-grid-y: 10vh;
  --cine-grid-scale: 0.78;
  --cine-horizon-y: 20vh;

  --cine-blur-px: 0px;
  --cine-brightness: 1;

  /* the navy wash that swallows the world while a panel reads */
  --cine-shade-opacity: 1;
  --cine-shade-top: 0;
  --cine-shade-mid: 0;
  --cine-shade-bottom: 0;
  --cine-tint: 8, 18, 42;
  --cine-clip-inset: 0%;
  --cine-word-y: 20px;
  --cine-word-opacity: 0;
  --cine-char-opacity: 0.2;

  --cine-title-y: 0px;
  --cine-title-scale: 1;
  --cine-title-opacity: 1;

  /* the framed site plate — act 0's foreground object */
  --cine-plate-x: -50%;
  --cine-plate-y: 0px;
  --cine-plate-bottom: -22vh;
  --cine-plate-width: 62vw;
  --cine-plate-scale: 1;
  --cine-plate-opacity: 1;

  /* drafting film. --cine-shutter-rest is the only one a media query may set:
     the engine composes the other two around it. */
  --cine-shutter-rest: 12vw;
  --cine-shutter-left-x: -12vw;
  --cine-shutter-right-x: 12vw;
  --cine-shutter-y: 0px;
  --cine-shutter-scale: 1;

  /* the three programme frames */
  --cine-photo1-opacity: 0;
  --cine-photo2-opacity: 0;
  --cine-photo3-opacity: 0;
  --cine-photo1-scale: 1.06;
  --cine-photo2-scale: 1.06;
  --cine-photo3-scale: 1.06;
  --cine-photo-x: 0px;
  --cine-photo-y: 0px;
  --cine-veil-opacity: 0;

  --cine-intro-y: 0px;
  --cine-intro-opacity: 1;
  --cine-outro-opacity: 0;
  --cine-outro-y: 40px;

  --cine-panel1-opacity: 0;
  --cine-panel1-y: calc(-50% + 58px);
  --cine-panel2-opacity: 0;
  --cine-panel2-y: calc(-50% + 58px);
  --cine-panel3-opacity: 0;
  --cine-panel3-y: calc(-50% + 58px);

  /* pathway ticker */
  --cine-slider-visibility: hidden;
  --cine-slider-opacity: 0;
  --cine-slider-enter-y: 80px;
  --cine-slider-scale: 1;
  --cine-slider-top: clamp(112px, 19vh, 220px);
  --cine-slider-left: 0px;
  --cine-slider-screen-top: clamp(112px, 19vh, 220px);
  --cine-ticker-x: 0px;
  --cine-controls-opacity: 0;
  /* One source of truth: the cards are this tall and the controls sit under
     them. Two hard-coded numbers is how the controls ended up printed across
     the cards when the cards grew. */
  --cine-card-h: clamp(250px, 27vh, 300px);

  /* film grain over the frames */
  --cine-grain-opacity: 0.05;

  /* palette — fixed, theme-independent (§02) */
  --cine-paper: #faf7f2;
  --cine-ink: #17181c;
  --cine-ink-muted: #4b4f57;
  --cine-navy: #0c1a38;
  --cine-navy-deep: #070d1e;
  --cine-terracotta: #e8915a;
  --cine-terracotta-ink: #9e4814;
  --cine-crimson: #c41e3a;

  /* rig length — the JS reads the real distance, this only sizes the page */
  --cine-span: 4400px;
}

/* ---------- 1 · the rig ---------- */

/* Carried over from the retired .page-hero-panel hook: the nav row condenses to
   fixed chrome once the contact row scrolls away, so in-page anchors need to
   clear it. */
.page-courses-cinema main section[id] {
  scroll-margin-top: 100px;
}

.cinema-scroll {
  position: relative;
  height: calc(100vh + var(--cine-span));
  background: var(--cine-navy-deep);
}

.cine-stage {
  position: sticky;
  top: 0;
  overflow: hidden;
  height: 100vh;
  min-height: 620px;
  isolation: isolate;
  background: var(--cine-navy-deep);
}

.cine-world {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--cine-navy);
}

.cine-world picture,
.cine-world img {
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}

/* ---------- 2 · sky: the campus, held far back ---------- */

.cine-sky {
  position: absolute;
  z-index: 0;
  inset: 0;
  filter: none;
}

.cine-sky img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
}

/* A permanent navy base wash: the photograph is a field, not a picture. */
.cine-sky::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(7, 13, 30, 0.74) 0%, rgba(12, 26, 56, 0.42) 44%, rgba(7, 13, 30, 0.84) 100%),
    linear-gradient(90deg, rgba(7, 13, 30, 0.5) 0%, transparent 30%, transparent 70%, rgba(7, 13, 30, 0.5) 100%);
}

/* ---------- 3 · back stack: blueprint, slider, horizon ---------- */

.cine-back {
  position: absolute;
  z-index: 1;
  top: 0;
  right: -3vw;
  bottom: 0;
  left: -3vw;
  opacity: var(--cine-back-opacity);
  transform: translate3d(var(--cine-back-x), var(--cine-back-y), 0) scale(var(--cine-back-scale));
  transform-origin: 50% 100%;
  will-change: transform, opacity;
}

.cine-grid {
  position: absolute;
  z-index: 1;
  inset: -14% -8%;
  opacity: 0.72;
  background-image:
    linear-gradient(rgba(232, 145, 90, 0.22) 1px, transparent 1px),
    linear-gradient(90deg, rgba(232, 145, 90, 0.22) 1px, transparent 1px),
    linear-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 176px 176px, 176px 176px, 44px 44px, 44px 44px;
  transform: translate3d(0, calc(var(--cine-grid-y) - 110px), 0) scale(var(--cine-grid-scale));
  transform-origin: 50% 100%;
  will-change: transform;
  pointer-events: none;
}

/* Bottom-anchored section line: the horizon the whole composition sits on. */
.cine-horizon {
  position: absolute;
  z-index: 3;
  right: -4vw;
  bottom: 0;
  left: -4vw;
  height: 46vh;
  transform: translate3d(0, var(--cine-horizon-y), 0);
  will-change: transform;
  pointer-events: none;
  background: linear-gradient(180deg, transparent 0%, rgba(7, 13, 30, 0.42) 46%, rgba(7, 13, 30, 0.86) 100%);
}

.cine-horizon::before {
  content: "";
  position: absolute;
  top: 46%;
  right: 0;
  left: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(232, 145, 90, 0.55) 18%, rgba(232, 145, 90, 0.55) 82%, transparent);
}

.cine-horizon::after {
  content: "";
  position: absolute;
  top: 46%;
  right: 0;
  left: 0;
  height: 9px;
  background-image: linear-gradient(90deg, rgba(232, 145, 90, 0.42) 1px, transparent 1px);
  background-size: 88px 100%;
}

/* ---------- 4 · the navy shade ---------- */

.cine-shade {
  position: absolute;
  z-index: 2;
  inset: 0;
  opacity: var(--cine-shade-opacity);
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(var(--cine-tint), var(--cine-shade-top)) 0%,
    rgba(var(--cine-tint), var(--cine-shade-mid)) 48%,
    rgba(var(--cine-tint), var(--cine-shade-bottom)) 100%);
}

/* ---------- 5 · the title ---------- */

/* ------------------------------------------------------------
   A note on the .cinema-scroll prefix and the !important flags below.

   ice-design-system.css §"Authoritative text hierarchy" declares
   `body.page-course main h1 / h2 / h3 / :is(p,li,dd)` and
   `body.page-course :is(figcaption,small,…)` with !important on font-family,
   font-size, font-weight, line-height and letter-spacing. /courses still needs
   .page-course — every section below the stage is scoped to it — so the stage
   has to answer in kind. Each selector here carries two classes, which beats
   those (0,1,3) selectors, and the flags are only on the five properties that
   block actually claims. course-typography.css's
   `.page-course main h1,h2,h3,h4 { color: var(--course-ink) }` is (0,1,2) and
   would otherwise paint the whole stage navy-on-navy; the same two classes
   settle it without a flag.
   ------------------------------------------------------------ */
.cinema-scroll .cine-title {
  position: absolute;
  /* Above the film (7). The title is 94vw and the film's inboard edges cut
     straight through it otherwise; by the time the film parts the title has
     already gone, so nothing is lost by lifting it. */
  z-index: 12;
  top: clamp(112px, 16vh, 180px);
  left: 50%;
  width: min(94vw, 1780px);
  margin: 0;
  color: var(--cine-paper);
  font-family: var(--ice-font-display, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif) !important;
  font-size: clamp(3.2rem, 11vw, 11rem) !important;
  font-weight: 400 !important;
  line-height: 0.82 !important;
  letter-spacing: -0.03em !important;
  text-align: center;
  text-transform: uppercase;
  opacity: var(--cine-title-opacity);
  transform: translate3d(-50%, var(--cine-title-y), 0) scale(var(--cine-title-scale));
  will-change: transform, opacity;
}

/* A span, not a <small>: `body.page-course :is(figcaption,small,…)` resolves to
   (0,2,2) through its .footer-stats span arm, which outranks any two-class
   selector this file can write for a <small>. */
.cinema-scroll .cine-subtitle {
  display: block;
  margin-top: clamp(14px, 2vh, 26px);
  color: var(--cine-terracotta);
  font-family: var(--ice-font-primary, system-ui, sans-serif) !important;
  font-size: clamp(0.7rem, 1.1vw, 0.8125rem) !important;
  font-weight: 700 !important;
  line-height: 1.4 !important;
  letter-spacing: 0.22em !important;
  text-transform: uppercase;
}

/* ---------- 6 · the site plate (act 0's foreground object) ---------- */

/* --cine-plate-bottom and --cine-plate-width are written by the engine on
   :root every frame. A media query must therefore NEVER redeclare them on
   .cine-plate itself — a value set on the element beats the inherited one and
   silently freezes the animation. Responsive tuning goes through the two
   separate knobs below, which the engine never touches. */
.cine-plate {
  position: absolute;
  z-index: 4;
  bottom: calc(var(--cine-plate-bottom) + var(--cine-plate-lift, 0vh));
  left: 50%;
  width: min(calc(var(--cine-plate-width) * var(--cine-plate-gain, 1)), 2140px);
  margin: 0;
  opacity: var(--cine-plate-opacity);
  transform: translate3d(var(--cine-plate-x), var(--cine-plate-y), 0) scale(var(--cine-plate-scale));
  transform-origin: 50% 48%;
  will-change: transform, opacity;
  pointer-events: none;
}

.cine-plate picture {
  position: relative;
  display: block;
  overflow: hidden;
  border: 1px solid rgba(250, 247, 242, 0.34);
  border-radius: 4px;
  box-shadow: 0 40px 90px rgba(4, 9, 36, 0.62);
}

.cine-plate img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* Three classes for the same reason as .cine-subtitle above. */
.cinema-scroll .cine-world .cine-plate figcaption {
  position: absolute;
  z-index: 2;
  top: 0;
  right: 0;
  left: 0;
  display: flex;
  /* Centred, not spread: the plate is wider than the gap in the film, so
     space-between pushes both labels behind it. */
  justify-content: center;
  gap: clamp(16px, 3vw, 40px);
  padding: 14px 18px;
  color: rgba(250, 247, 242, 0.9);
  font-size: clamp(0.62rem, 0.8vw, 0.72rem) !important;
  font-weight: 700 !important;
  line-height: 1.4 !important;
  letter-spacing: 0.16em !important;
  text-transform: uppercase;
  background: linear-gradient(180deg, rgba(7, 13, 30, 0.72), transparent);
}

.cinema-scroll .cine-world .cine-plate figcaption span:last-child {
  color: var(--cine-terracotta);
}

/* ---------- 7 · the three programme frames ---------- */

/* Opacity, z-index and transform are written inline by the
   engine, per frame, because which layer is on top changes as the chapters
   advance. Nothing here may set them — an inline style wins, so a stale rule
   would just be dead weight, and a `clip-path` referencing an undefined
   property silently invalidates the whole declaration. */
.cine-photo {
  position: absolute;
  z-index: 5;
  inset: 0;
  opacity: 0;
  will-change: transform, opacity;
  pointer-events: none;
}

.cine-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Film grain over the frames. Static: one inline SVG turbulence, rasterised
   once and tiled, rather than the reference's per-frame feTurbulence, which
   recomputes fractal noise across the viewport every frame and would put the
   stage well under 30fps. */
.cine-grain {
  position: absolute;
  z-index: 10;
  inset: 0;
  opacity: var(--cine-grain-opacity);
  pointer-events: none;
  display: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

/* Legibility scrim over the frames. The panels carry body copy at 1.14rem on
   top of a photograph; without this the contrast is whatever the photo happens
   to be that frame. Ramped by the same driver as the panels, so it costs
   nothing when no panel is reading. */
.cine-veil {
  position: absolute;
  z-index: 9;
  inset: 0;
  opacity: var(--cine-veil-opacity);
  pointer-events: none;
  background:
    /* A pool through the middle, where the panels sit, on top of the vertical
       wash. The wash alone left the copy sitting on whatever the photograph
       happened to be doing at that moment. */
    radial-gradient(ellipse 78% 62% at 50% 52%, rgba(4, 9, 36, 0.58) 0%, rgba(4, 9, 36, 0.22) 58%, rgba(4, 9, 36, 0) 100%),
    linear-gradient(180deg, rgba(7, 13, 30, 0.88) 0%, rgba(12, 26, 56, 0.6) 46%, rgba(7, 13, 30, 0.92) 100%);
}

/* ---------- 8 · drafting film ---------- */

/* Film, not shutters: translucent so the scene reads through the frame. The
   opaque version buried the campus photograph and cut the title in half. */
.cine-shutter {
  position: absolute;
  z-index: 11;
  top: -6vh;
  bottom: -6vh;
  width: 30vw;
  pointer-events: none;
  will-change: transform;
  background:
    linear-gradient(rgba(255, 255, 255, 0.075) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.075) 1px, transparent 1px),
    linear-gradient(180deg, rgba(7, 13, 30, 0.7), rgba(12, 26, 56, 0.52));
  background-size: 34px 34px, 34px 34px, 100% 100%;
}

.cine-shutter-left {
  left: 0;
  transform: translate3d(var(--cine-shutter-left-x), var(--cine-shutter-y), 0) scale(var(--cine-shutter-scale));
  transform-origin: 21% 52%;
}

.cine-shutter-right {
  right: 0;
  transform: translate3d(var(--cine-shutter-right-x), var(--cine-shutter-y), 0) scale(var(--cine-shutter-scale));
  transform-origin: 79% 52%;
}

/* The inboard edge carries the terracotta rule — the film's cut line. */
.cine-shutter::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(180deg, transparent, rgba(232, 145, 90, 0.7) 24%, rgba(232, 145, 90, 0.7) 76%, transparent);
}

.cine-shutter-left::after { right: 0; }
.cine-shutter-right::after { left: 0; }

/* writing-mode rather than a rotate(): a rotated inline box still lays out
   horizontally, so its origin has to be hand-solved and the label drifts off
   the film at anything but one viewport height. */
.cine-shutter b {
  position: absolute;
  top: 50%;
  color: rgba(250, 247, 242, 0.5);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  white-space: nowrap;
  writing-mode: vertical-rl;
}

/* Each label hugs its film's inboard edge — the cut line. */
.cine-shutter-left b {
  right: 20px;
  transform: translateY(-50%) rotate(180deg);
}

.cine-shutter-right b {
  left: 20px;
  transform: translateY(-50%);
}

/* ---------- 9 · intake line + programme pills ---------- */

.cine-intro {
  position: absolute;
  z-index: 13;
  bottom: clamp(56px, 44vh, 460px);
  left: 50%;
  width: min(620px, calc(100vw - 40px));
  text-align: center;
  opacity: var(--cine-intro-opacity);
  transform: translate3d(-50%, var(--cine-intro-y), 0);
  will-change: transform, opacity;
}

/* Faded out is not gone: without this the pills stay clickable under the
   panels for the rest of the scroll. */
.cine-intro.is-gone { pointer-events: none; }

.cinema-scroll .cine-intro p {
  margin: 0 auto;
  max-width: 560px;
  color: var(--cine-paper);
  font-size: clamp(1rem, 1.2vw, 1.18rem) !important;
  font-weight: 500 !important;
  line-height: 1.35 !important;
  text-shadow: 0 2px 18px rgba(4, 9, 36, 0.62);
}

.cine-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  margin-top: 26px;
}

.cine-pills a {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  min-height: 42px;
  padding: 0 22px;
  border-radius: 999px;
  color: var(--cine-ink);
  font-size: 0.95rem;
  font-weight: 600;
  text-decoration: none;
  background: var(--cine-paper);
  box-shadow: 0 12px 30px rgba(4, 9, 36, 0.32);
  transition: transform 180ms ease, box-shadow 180ms ease;
}

.cine-pills a b {
  color: var(--cine-terracotta-ink);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.cine-pills a:hover,
.cine-pills a:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 16px 36px rgba(4, 9, 36, 0.42);
}

/* ---------- 10 · the three programme panels ---------- */

.cine-panel {
  position: absolute;
  z-index: 14;
  top: 45%;
  left: 50%;
  width: min(780px, calc(100vw - 42px));
  text-align: center;
  pointer-events: none;
  transform: translate3d(-50%, -50%, 0);
  will-change: transform, opacity;
}

/* The full-frame veil evens out the photograph, but a bright patch behind the
   kicker or the body copy still drops them under AA. This local pool is what
   the text actually reads against. */
.cine-panel::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -14% -12%;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 50%, rgba(4, 9, 36, 0.72) 0%, rgba(4, 9, 36, 0.52) 52%, rgba(4, 9, 36, 0) 78%);
}

.cine-panel-1 { top: 58%; opacity: var(--cine-panel1-opacity); transform: translate3d(-50%, var(--cine-panel1-y), 0); }
/* 36%, not 31%: any higher and the kicker slides under the fixed nav row. */
.cine-panel-2 { top: 36%; opacity: var(--cine-panel2-opacity); transform: translate3d(-50%, var(--cine-panel2-y), 0); }
.cine-panel-3 { top: 58%; opacity: var(--cine-panel3-opacity); transform: translate3d(-50%, var(--cine-panel3-y), 0); }

.cine-code {
  display: block;
  margin: 0 0 18px;
  color: var(--cine-terracotta);
  font-size: clamp(0.68rem, 0.9vw, 0.8125rem);
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-shadow: 0 2px 14px rgba(4, 9, 36, 0.8);
}

.cinema-scroll .cine-panel h2 {
  margin: 0;
  color: var(--cine-paper);
  font-family: var(--ice-font-display, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif) !important;
  font-size: clamp(1.9rem, 4.2vw, 4rem) !important;
  font-weight: 400 !important;
  line-height: 1.02 !important;
  letter-spacing: -0.02em !important;
  text-shadow: 0 16px 38px rgba(4, 9, 36, 0.55);
}

.cine-panel h2::after { content: none; }

.cinema-scroll .cine-panel > p {
  width: min(540px, 100%);
  margin: 22px auto 0;
  color: var(--cine-paper);
  font-size: clamp(0.95rem, 1.15vw, 1.14rem) !important;
  font-weight: 400 !important;
  line-height: 1.45 !important;
  text-shadow: 0 2px 18px rgba(4, 9, 36, 0.62);
}

.cine-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(18px, 3vw, 54px);
  width: min(560px, 100%);
  margin: clamp(26px, 4vh, 48px) auto 0;
}

.cinema-scroll .cine-facts dt {
  color: var(--cine-paper);
  font-family: var(--ice-font-display, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif) !important;
  font-size: clamp(1.7rem, 3.1vw, 3rem) !important;
  font-weight: 400 !important;
  line-height: 0.9 !important;
  letter-spacing: -0.01em !important;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 14px 34px rgba(4, 9, 36, 0.55);
}

.cinema-scroll .cine-facts dd {
  margin: 12px 0 0;
  color: rgba(250, 247, 242, 0.82);
  font-size: clamp(0.72rem, 0.85vw, 0.82rem) !important;
  font-weight: 600 !important;
  line-height: 1.2 !important;
  letter-spacing: 0.1em !important;
  text-transform: uppercase;
}

.cine-open {
  display: inline-flex;
  gap: 12px;
  align-items: center;
  min-height: 50px;
  margin-top: 28px;
  padding: 0 28px;
  border-radius: 999px;
  color: var(--cine-paper);
  font-size: 0.95rem;
  font-weight: 700;
  text-decoration: none;
  background: var(--cine-crimson);
  box-shadow: 0 16px 34px rgba(4, 9, 36, 0.4);
  pointer-events: none;
  transition: transform 180ms ease, background-color 180ms ease;
}

/* Only the panel currently reading owns the pointer. */
.cine-panel.is-live .cine-open { pointer-events: auto; }

.cine-open:hover,
.cine-open:focus-visible {
  transform: translateY(-2px);
  background: #a81830;
}

/* ---------- 10b · word and character reveals ----------
   Each span carries a normalised 0..1 position in `--i`. The engine writes one
   number per panel, `--cine-reveal`, and every span derives its own progress
   from it here. That keeps a ~150-character paragraph at one style write per
   frame instead of one hundred and fifty.

   The multipliers set the stagger: a word starts moving when --cine-reveal
   passes 0.45 * its position, and takes 0.35 of the reveal to arrive. */
.word-span {
  --w: clamp(0, (var(--cine-reveal, 1) - var(--i, 0) * 0.45) * 2.86, 1);
  display: inline-block;
  opacity: var(--w);
  transform: translateY(calc((1 - var(--w)) * 22px));
}

/* No will-change here: three panels of split copy is several hundred elements,
   and promoting every one of them costs far more than it saves. */
.char-span {
  opacity: clamp(0.18, (var(--cine-reveal, 1) - var(--i, 0) * 0.55) * 2.6, 1);
}

.word-wrap {
  display: inline-block;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .word-span { --w: 1; transform: none; }
  .char-span { opacity: 1; }
}

/* ---------- 11 · pathway slider ---------- */

.cine-slider {
  position: absolute;
  z-index: 2;
  top: var(--cine-slider-top);
  right: auto;
  left: var(--cine-slider-left);
  width: 100vw;
  visibility: var(--cine-slider-visibility);
  opacity: var(--cine-slider-opacity);
  /* Arrives by rising and fading, not by flying 420vw across the screen. The
     old entrance moved the whole strip through the frame at enormous speed,
     which is what made the card act read as "wtf did I see". */
  transform: translate3d(0, var(--cine-slider-enter-y), 0) scale(var(--cine-slider-scale));
  transform-origin: 0 0;
  will-change: transform, opacity;
}

/* The gutter is padding, not a translate, so the engine's wrap arithmetic works
   in one coordinate space: card i sits at paddingLeft + tickerX + i * pitch. */
.cine-track {
  display: flex;
  gap: clamp(18px, 1.4vw, 28px);
  align-items: stretch;
  padding-left: clamp(20px, 4vw, 56px);
  /* No CSS transition: the engine drives this every frame. A transition here
     would fight the rAF loop and smear the modulo wrap into a visible slide. */
  transform: translate3d(var(--cine-ticker-x), 0, 0);
  will-change: transform;
}

.cine-card {
  position: relative;
  flex: 0 0 clamp(300px, 30vw, 460px);
  height: var(--cine-card-h);
  padding: 28px;
  overflow: hidden;
  border: 1px solid rgba(250, 247, 242, 0.42);
  border-radius: 22px;
  color: var(--cine-ink);
  text-decoration: none;
  background: var(--cine-paper);
  box-shadow: 0 18px 52px rgba(4, 9, 36, 0.28);
  /* Scale comes from the engine (distance to frame centre); the transition is
     only for the hover lift, which composes on top of it. */
  transform: scale(var(--cine-card-scale, 1)) translateY(var(--cine-card-lift, 0px));
  transition: box-shadow 240ms ease, border-color 240ms ease;
}

.cine-card:hover,
.cine-card:focus-visible {
  --cine-card-lift: -6px;
  border-color: var(--cine-terracotta);
  box-shadow: 0 30px 70px rgba(4, 9, 36, 0.46);
}

.cine-card .cine-card-kicker {
  display: block;
  color: var(--cine-terracotta-ink);
  font-size: 0.74rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* A drawn mark, not a thin glyph. It sits behind the copy at the top-right of
   the card and is the first thing read at ticker speed. */
/* Big enough to be the card's image at ticker speed, and low enough in the
   card to occupy the middle band that the kicker and the title leave empty. */
/* Big enough to be the card's image at ticker speed, and sitting in the band
   the kicker and the title leave empty — but stopping short of the title, which
   a vertically-centred mark ran straight through. */
.cine-card svg {
  position: absolute;
  top: 22px;
  right: 24px;
  width: clamp(80px, 8vw, 112px);
  height: clamp(80px, 8vw, 112px);
  color: var(--cine-navy);
  opacity: 0.16;
}

.cinema-scroll .cine-card h3 {
  position: absolute;
  right: 28px;
  bottom: calc(28px + (0.95rem * 1.25 * 2) + 14px);
  left: 28px;
  overflow: hidden;
  max-width: calc(100% - 56px);
  margin: 0;
  color: var(--cine-ink);
  font-family: var(--ice-font-primary, system-ui, sans-serif) !important;
  font-size: clamp(1.3rem, 1.7vw, 1.72rem) !important;
  font-weight: 700 !important;
  line-height: 1.04 !important;
  letter-spacing: -0.018em !important;
  text-wrap: balance;
}

.cine-card h3::after { content: none; }

.cinema-scroll .cine-card p {
  position: absolute;
  right: 28px;
  bottom: 28px;
  left: 28px;
  display: -webkit-box;
  overflow: hidden;
  max-height: calc(2em * 1.25);
  margin: 0;
  color: var(--cine-ink-muted);
  font-size: 0.95rem !important;
  font-weight: 400 !important;
  line-height: 1.25 !important;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

/* The card carries a hairline rule under the kicker — a drafting cue, and it
   gives the top half of a much taller card something to hold. */
.cine-card .cine-card-kicker::after {
  content: "";
  display: block;
  width: 44px;
  height: 1px;
  margin-top: 14px;
  background: var(--cine-terracotta);
}

/* ---------- 11b · the close ----------
   The slider sits on a fixed row near the top, which left the bottom two
   thirds of the last act empty. This is the page's exit: the one thing a
   visitor who has watched all three programs should be offered next. */

.cine-outro {
  position: absolute;
  z-index: 14;
  bottom: clamp(48px, 14vh, 150px);
  left: clamp(20px, 4vw, 56px);
  width: min(560px, calc(100vw - 40px));
  opacity: var(--cine-outro-opacity);
  pointer-events: none;
  transform: translate3d(0, var(--cine-outro-y), 0);
  will-change: transform, opacity;
}

.cine-outro.is-live { pointer-events: auto; }

.cine-outro .cine-code { text-align: left; }

.cinema-scroll .cine-outro h2 {
  margin: 0;
  color: var(--cine-paper);
  font-family: var(--ice-font-display, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif) !important;
  font-size: clamp(1.6rem, 3vw, 2.75rem) !important;
  font-weight: 400 !important;
  line-height: 1.06 !important;
  letter-spacing: -0.02em !important;
  text-shadow: 0 16px 38px rgba(4, 9, 36, 0.55);
}

.cinema-scroll .cine-outro h2::after { content: none; }

.cine-outro-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 26px;
}

.cine-outro-actions a {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  min-height: 50px;
  padding: 0 26px;
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--cine-paper);
  font-size: 0.95rem;
  font-weight: 700;
  text-decoration: none;
  background: var(--cine-crimson);
  box-shadow: 0 16px 34px rgba(4, 9, 36, 0.4);
  transition: transform 180ms ease, background-color 180ms ease;
}

.cine-outro-actions a + a {
  color: var(--cine-paper);
  border-color: rgba(250, 247, 242, 0.42);
  background: rgba(7, 13, 30, 0.42);
  box-shadow: none;
}

.cine-outro-actions a:hover,
.cine-outro-actions a:focus-visible { transform: translateY(-2px); }
.cine-outro-actions a:first-child:hover { background: #a81830; }
.cine-outro-actions a + a:hover { background: rgba(7, 13, 30, 0.72); }

.cine-controls {
  position: absolute;
  z-index: 15;
  top: calc(var(--cine-slider-screen-top) + var(--cine-card-h) + 26px);
  left: clamp(20px, 4vw, 56px);
  display: flex;
  gap: 14px;
  align-items: center;
  opacity: var(--cine-controls-opacity);
  pointer-events: none;
  will-change: opacity;
}

.cine-controls.is-ready { pointer-events: auto; }

.cine-nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  border: 0;
  border-radius: 999px;
  color: var(--cine-navy);
  font: inherit;
  font-size: 1.1rem;
  background: rgba(250, 247, 242, 0.94);
  box-shadow: 0 18px 36px rgba(4, 9, 36, 0.3);
  cursor: pointer;
  transition: transform 180ms ease, background-color 180ms ease;
}

.cine-nav:hover { transform: translateY(-2px); background: #fff; }

.cinema-scroll .cine-controls p {
  max-width: 34ch;
  margin: 0 0 0 8px;
  color: rgba(250, 247, 242, 0.86);
  font-size: 0.92rem !important;
  line-height: 1.35 !important;
  text-shadow: 0 2px 18px rgba(4, 9, 36, 0.62);
}

/* ---------- 12 · responsive ---------- */

@media (max-width: 1100px) {
  .cine-shutter { width: 26vw; --cine-shutter-rest: 10vw; }
  .cine-panel { width: min(640px, calc(100vw - 36px)); }
  .cine-facts { gap: 20px; }
}

@media (max-width: 640px) {
  .cinema-scroll { --cine-span: 3900px; height: calc(100svh + var(--cine-span)); }
  .cine-stage { height: 100svh; min-height: 0; }
  .cine-title { top: clamp(104px, 15vh, 150px); }
  .cine-intro { bottom: clamp(44px, 40vh, 320px); width: calc(100vw - 32px); }
  /* A 62vw plate is a stamp on a phone; lift it clear of the intake line too. */
  .cine-plate { --cine-plate-lift: 24vh; --cine-plate-gain: 1.5; }
  .cine-pills a { min-height: 38px; padding: 0 15px; font-size: 0.85rem; }
  .cine-panel { top: 46%; width: calc(100vw - 32px); }
  .cine-panel-1, .cine-panel-3 { top: 54%; }
  .cine-panel-2 { top: 30%; }
  .cine-facts { gap: 12px; }
  .cine-facts dd { font-size: 0.62rem; letter-spacing: 0.06em; }
  /* Edge strips only. At phone widths anything wider is not a frame, it is a
     letterbox with a slit in it. */
  .cine-shutter { width: 20vw; --cine-shutter-rest: 7vw; }
  .cine-shutter b { display: none; }
  /* Clear of the site's sticky mobile CTA bar. */
  .cine-outro { bottom: clamp(92px, 16vh, 150px); width: calc(100vw - 40px); }
  .cine-outro-actions a { min-height: 44px; padding: 0 18px; font-size: 0.88rem; }
  .cine-card { flex-basis: min(82vw, 320px); height: 210px; padding: 20px; }
  .cine-card svg { width: 46px; height: 46px; }
  .cine-controls { top: calc(var(--cine-slider-screen-top) + var(--cine-card-h) + 18px); flex-wrap: wrap; }
  .cine-controls p { display: none; }
  .word-span { --w: 1; transform: none; }
  .char-span { opacity: 1; }
}

/* The stage is a fixed navy field, so the ICE hero chrome reads on it in both
   themes — no data-theme fork is needed or wanted here. */

@media (prefers-reduced-motion: reduce) {
  .cine-track,
  .cine-card,
  .cine-nav,
  .cine-open,
  .cine-pills a {
    transition: none;
  }
}

/* ---------- 13 · carrying the world downward ----------
   The stage is a full-bleed dark field; the sections under it were the site's
   warm-paper editorial bands. The page changed its mind about what it was at a
   single scroll position and the effect died there.

   Everything below runs on the dark field with paper text. The nested cards
   (.usp-step, .usp-role, buttons) keep their own light surfaces on purpose —
   paper cards on a dark field is the same relationship the ticker already has.

   `!important` and the body-class scope are both required, not stylistic:
   `.page-course main .course-section--tint` and friends are (0,2,2) in
   course-typography.css, and this file is linked from the page's head slot,
   which Astro renders BEFORE the site stylesheets. A tie on specificity
   therefore loses on source order. Getting this wrong is what shipped
   `rgb(250,247,242)` text on a `rgb(250,247,242)` background — a contrast ratio
   of exactly 1, caught by tests/theme-audit.spec.mjs. */

body.page-courses-cinema main .usp-band,
body.page-courses-cinema main .course-section--dark-split,
body.page-courses-cinema main .course-apply {
  background: var(--cine-navy-deep) !important;
  color: rgba(250, 247, 242, 0.82) !important;
}

/* A hairline seam between bands so they read as chapters of one field rather
   than one undifferentiated slab. */
body.page-courses-cinema main .course-section--dark-split,
body.page-courses-cinema main .course-apply {
  border-top: 1px solid rgba(250, 247, 242, 0.1);
}

body.page-courses-cinema main .usp-band :is(h2, h3),
body.page-courses-cinema main .course-section--dark-split :is(h2, h3),
body.page-courses-cinema main .course-apply :is(h2, h3) {
  color: var(--cine-paper) !important;
}

body.page-courses-cinema main .usp-band > .wrap > .sec-head > p,
body.page-courses-cinema main .course-section--dark-split .course-section__heading p,
body.page-courses-cinema main .course-section--dark-split .course-note,
body.page-courses-cinema main .course-apply .course-apply__copy > p {
  color: rgba(250, 247, 242, 0.82) !important;
}

/* .course-apply__media's caption is NOT on the dark field — it sits on the
   media frame's own light surface. Recolouring it to paper put cream on cream
   and axe caught it. It keeps its original ink. */

body.page-courses-cinema main .usp-band .course-kicker,
body.page-courses-cinema main .course-section--dark-split .course-kicker,
body.page-courses-cinema main .course-apply .course-kicker {
  color: var(--cine-terracotta) !important;
}

/* The section rules the same hierarchy the stage uses. */
body.page-courses-cinema main :is(.usp-band, .course-section--dark-split, .course-apply) h2::after {
  background: var(--cine-terracotta) !important;
}

/* ---------- boundary-driven cinematic ----------
   The browser owns every pixel of scrolling. JavaScript changes only the
   data-scene value at five wide chapter boundaries; CSS handles one short,
   compositor-only transition and then remains completely still. */
.cine-stage[data-scene] .cine-back {
  opacity: 1;
  transform: none;
}

.cine-stage[data-scene] :is(.cine-title, .cine-intro, .cine-plate, .cine-shutter, .cine-photo, .cine-panel, .cine-slider, .cine-outro, .cine-controls) {
  transition: opacity 260ms ease, transform 420ms cubic-bezier(.22,.7,.2,1);
}

.cine-stage[data-scene] .cine-title,
.cine-stage[data-scene] .cine-intro,
.cine-stage[data-scene] .cine-plate,
.cine-stage[data-scene] .cine-shutter {
  opacity: 0;
  pointer-events: none;
}

.cine-stage[data-scene="0"] .cine-title {
  opacity: 1;
  transform: translate3d(-50%, 0, 0);
}

.cine-stage[data-scene="0"] .cine-intro {
  opacity: 1;
  transform: translate3d(-50%, 0, 0);
  pointer-events: auto;
}

.cine-stage[data-scene="0"] .cine-plate {
  opacity: 1;
  transform: translate3d(-50%, 0, 0) scale(1);
}

.cine-stage[data-scene="0"] .cine-shutter { opacity: 1; }

.cine-stage[data-scene] .cine-photo {
  z-index: 5;
  opacity: 0;
  transform: translate3d(0, 0, 0) scale(1.035);
}

.cine-stage[data-scene] .cine-photo.is-active {
  z-index: 7;
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
}

.cine-stage[data-scene] .cine-veil { opacity: .76; }
.cine-stage[data-scene="0"] .cine-veil,
.cine-stage[data-scene="4"] .cine-veil { opacity: .16; }

.cine-stage[data-scene] .cine-panel,
.cine-stage[data-scene] :is(.cine-panel-1, .cine-panel-2, .cine-panel-3) {
  top: 52%;
  opacity: 0;
  transform: translate3d(-50%, calc(-50% + 26px), 0);
}

.cine-stage[data-scene] .cine-panel.is-live {
  opacity: 1;
  transform: translate3d(-50%, -50%, 0);
}

.cine-stage[data-scene] .word-span {
  --w: 1;
  transform: none;
}

.cine-stage[data-scene] .char-span { opacity: 1; }

.cine-stage[data-scene] .cine-slider,
.cine-stage[data-scene] .cine-outro,
.cine-stage[data-scene] .cine-controls {
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}

.cine-stage[data-scene="4"] .cine-slider {
  top: clamp(112px, 17vh, 170px);
  left: 0;
  visibility: visible;
  opacity: 1;
  transform: none;
}

.cine-stage[data-scene="4"] .cine-outro {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transform: none;
}

.cine-stage[data-scene="4"] .cine-controls {
  top: calc(clamp(112px, 17vh, 170px) + var(--cine-card-h) + 22px);
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
}

.cine-stage[data-scene] .cine-track {
  transition: transform 360ms cubic-bezier(.22,.7,.2,1);
  will-change: auto;
}

.cine-stage.cine-reduced * { transition: none !important; }

@media (max-width: 640px) {
  .cine-stage[data-scene] .cine-panel,
  .cine-stage[data-scene] :is(.cine-panel-1, .cine-panel-2, .cine-panel-3) {
    top: 51%;
    width: calc(100vw - 28px);
  }

  .cine-stage[data-scene] .cine-panel::before { inset: -8% -5%; }
  .cine-stage[data-scene] .cine-code { margin-bottom: 10px; }
  .cine-stage[data-scene] .cine-panel > p { margin-top: 14px; }
  .cine-stage[data-scene] .cine-facts { margin-top: 20px; }
  .cine-stage[data-scene] .cine-open { min-height: 44px; margin-top: 20px; }

  .cine-stage[data-scene="4"] .cine-slider { top: 108px; }
  .cine-stage[data-scene="4"] .cine-controls {
    top: calc(108px + var(--cine-card-h) + 14px);
  }
}

.compare-split-panel {
  display: flex;
  flex-direction: row;
  gap: clamp(24px, 4vw, 64px);
  margin-top: 48px;
  align-items: flex-start;
}

.compare-left {
  width: 35%;
  flex-shrink: 0;
  position: sticky;
  top: 120px;
}

.compare-image-container {
  aspect-ratio: 3/4;
  border-radius: 12px;
  overflow: hidden;
  position: relative;
  background: var(--cine-navy);
}

/* Three stacked frames. Only the incoming one animates: it fades UP on top of
   the outgoing frame, which holds at full opacity for exactly one fade. A
   symmetrical cross-fade would show the panel through both at the midpoint. */
.compare-photo {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  pointer-events: none;
  transform: scale(1.06);
  transition: opacity 620ms cubic-bezier(0.22, 1, 0.36, 1), transform 1400ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}

.compare-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.compare-photo.is-active {
  z-index: 3;
  opacity: 1;
  transform: scale(1);
}

/* Held lit underneath while the incoming frame arrives, then released. */
.compare-photo.is-outgoing {
  z-index: 2;
  opacity: 1;
  transition: none;
}

.compare-row {
  cursor: pointer;
}

/* The row is no longer focusable itself (see ice-programs-cinema.js — it was a
   role="button" wrapping a link). The ring now follows the link inside it, so a
   keyboard user still sees which row they are on. */
.compare-row:focus-within {
  outline: 2px solid var(--cine-terracotta);
  outline-offset: 6px;
}

@media (prefers-reduced-motion: reduce) {
  .compare-photo { transition: none; transform: none; }
  .compare-photo.is-active { transform: none; }
}

.compare-right {
  width: 65%;
}

.compare-row {
  padding: 32px 0;
  border-bottom: 1px solid rgba(250, 247, 242, 0.1);
  cursor: pointer;
  /* 0.8, not 0.6. Row opacity multiplies every child alpha, so a 0.6 row and a
     0.6 label composited to #5e616a on the dark field — 3.12:1, which axe
     failed ten times over. Dimming an inactive row must never take its labels
     below AA; the state is signalled by the heading and the frame, not by
     making the facts unreadable. */
  opacity: 0.8;
  transition: opacity 0.3s ease;
}
.compare-row:first-child {
  border-top: 1px solid rgba(250, 247, 242, 0.1);
}
.compare-row.is-active,
.compare-row:hover {
  opacity: 1;
}

.compare-row-header {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 24px;
}
.compare-index {
  font-family: var(--ice-font-display, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif);
  font-size: 1.5rem;
  color: var(--cine-terracotta);
}
.compare-row-header h3 {
  margin: 0;
  font-family: var(--ice-font-display, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif);
  font-size: clamp(1.8rem, 3vw, 2.5rem);
  font-weight: 400;
}
.compare-row-header h3 a {
  color: var(--cine-paper);
  text-decoration: none;
}

.compare-facts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
.compare-fact-wide {
  grid-column: 1 / -1;
}
.compare-facts dt {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: rgba(250, 247, 242, 0.8);
  margin-bottom: 4px;
}
.compare-facts dd {
  margin: 0;
  font-size: 1rem;
  font-weight: 500;
  color: var(--cine-paper);
}

@media (max-width: 900px) {
  .compare-split-panel {
    flex-direction: column;
  }
  .compare-left {
    width: 100%;
    position: static;
  }
  .compare-image-container {
    aspect-ratio: 16/9;
  }
  .compare-right {
    width: 100%;
  }
}
