/* EXTRACTED VERBATIM from index.html's inline <style> at build time so this
   standalone prototype renders the real a1 hero. Not authored here; do not
   edit. Regenerate by re-extracting the <style> block from index.html. */
? No: a <style> later in the head still wins on
     order for equal specificity, which is why ground-ink.css states its
     overrides at a specificity index.html's rules cannot match rather than
     relying on position. See the comments in that file. -->
<link rel="stylesheet" href="tokens-ink.css">
<style>
/* ===========================================================================
   niko-poster · PASS 3 — the page rebuilt around the players.

   Niko, 11 Oct: "redesign the site around the embedding feature." So the frame
   is the poster's image and the type serves it. Three rules govern every sheet
   below, and all three come from what the reviews measured:

     1. TYPE NEVER CROSSES A PICTURE. The taste review's first finding, and
        Niko's own complaint ("the thumbnails being covered"). There is no
        overlay layer on this page at all: the fold is gone, and a title sits
        beside or under its frame, never on it. The only thing drawn over a
        frame is the play glyph, which is what the frame is for.
     2. THE FRAME IS BIG. Pass 2's plates were 283-433px on a 1296 container
        (~6% of frame area) while titles ran to 160px. That ratio is inverted:
        a lead frame is 60-72% of the container, a grid tile never under 400px
        on desktop, and the title is capped at a third of pass 2's size.
     3. ONE MESSAGE PER ROW. An era is one act; the grid is its content.

   THE FOLD IS GONE. The .crease layer, fold.js and every --fold-max are gone. The
   paper idea survives as a 1px crease line down the sheet and a paper shadow
   under each frame (the taste review's option A), which is the idea without
   the overlay that defaced the work.
   ======================================================================== */

html { background: var(--sc-canvas); color-scheme: light dark; }
body {
  background: var(--sc-canvas);
  color: var(--sc-ink);
  font-family: var(--sc-font-text);
  overflow-x: clip;
}

:root {
  --strip-w: 3.4rem;
  --safe-r: env(safe-area-inset-right, 0px);
  /* emil-design-eng's curve for UI interactions. The engine ships its own
     --sc-ease-out; this is the same intent, stated locally for the facade. */
  --vf-ease: cubic-bezier(0.23, 1, 0.32, 1);
}
@media (max-width: 680px) { :root { --strip-w: 3rem; } }

a, button, [role="button"] { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
@media (pointer: fine) { html, body { overscroll-behavior-y: none; } }

.act, .sheet { scroll-margin-top: clamp(0.5rem, 2vh, 1.5rem); }
html { scroll-padding-right: calc(var(--strip-w) + var(--safe-r) + 0.5rem); scroll-padding-block: 2rem; }

a:focus-visible, button:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--sc-accent);
  outline-offset: 3px;
  border-radius: 2px;
}
.sheet--black a:focus-visible, .sheet--black button:focus-visible { outline-color: var(--black-accent); }
::selection { background: var(--sc-accent); color: var(--sc-accent-ink); }

.skip {
  position: absolute; left: var(--sc-gutter); top: 0; z-index: 60;
  transform: translateY(-120%);
  background: var(--sc-ink); color: var(--sc-canvas);
  font-family: var(--sc-font-display); font-size: 0.8rem; letter-spacing: 0.12em;
  text-transform: uppercase; text-decoration: none;
  padding: 0.85rem 1.2rem; min-height: 2.75rem; display: grid; place-items: center;
}
.skip:focus-visible { transform: translateY(0); outline-offset: -4px; }

/* ------------------------------------------------------------------- acts */
.act { position: relative; display: block; isolation: isolate; background: var(--sc-canvas); }
:where(.sheet) { position: relative; }
.sheet:not([data-sc-stage]) { position: relative; }
.sheet { display: block; isolation: isolate; background: var(--sc-canvas); }

.sheet__fold {
  position: relative;
  display: grid;
  align-content: center;
  gap: clamp(1.2rem, 3vh, 2.4rem);
  overflow: clip;                 /* clip, never hidden: hidden kills the sticky */
  padding: clamp(2.2rem, 5vh, 4rem) var(--sc-gutter) clamp(2rem, 4.5vh, 3.4rem);
  padding-right: max(var(--sc-gutter), calc(var(--strip-w) + var(--safe-r) + 0.5rem));
  background: var(--sc-canvas);
}
/* THE CREASE, all that is left of the fold: a 1px line down the sheet's own
   gutter. It is a mark on the paper, never a panel over the work. */
.sheet__fold::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 50%;
  width: 1px;
  background: linear-gradient(to bottom,
    transparent,
    color-mix(in oklab, var(--sc-ink) 9%, transparent) 12%,
    color-mix(in oklab, var(--sc-ink) 9%, transparent) 88%,
    transparent);
  pointer-events: none;
  z-index: 0;
}
@media (max-width: 860px) { .sheet__fold::after { display: none; } }

.sheet--solo { min-height: 100%; }
/* A sheet holding ONE film and its type is a held frame: it sticks for the
   act's travel, which is what gives a solo poster its moment. */
.sheet--solo > .sheet__fold { position: sticky; top: 0; height: 100svh; min-height: 0; max-height: 100svh; }
.sheet[data-sc-stage] > .sheet__fold { position: relative; top: auto; height: 100%; }
.act--short .sheet--solo > .sheet__fold { height: auto; min-height: 76svh; position: relative; }

/* AN ERA GRID IS NOT A HELD FRAME. Measured at 1440x900 in round 1: acts 2, 6
   and 8 carry a grid of six to twelve players plus their type, which does not
   fit a 100svh sticky stage — tiles were cut by the sheet edge and the surplus
   became a bone void on the next frame. The era act therefore FLOWS: the sheet
   takes its content's own height and the reader scrolls through the grid,
   which is also what "the era is one act, the grid is its content" means.
   The act still owns its span; it simply does not pin. */
.act--flowgrid .sheet--solo > .sheet__fold {
  position: relative;
  height: auto;
  max-height: none;
  min-height: 100svh;
  align-content: center;
  padding-block: clamp(2.2rem, 6vh, 4.4rem);
}
/* And the era's own content is capped so an act cannot outgrow the span it
   declared: measured in round 2, a6 ran to 2.70vh against a declared 1.45 and
   a8 to 3.88 against 1.50, which put the page at 15.75vh — over the 8-to-14
   ceiling the gate checks. The tiles are what carry the height, so the tile is
   what is capped.

   A max-height alone cannot shrink an aspect-ratio box (the width still wins
   and the box just overflows), so the cap is written as a max-WIDTH derived
   from the film's own ratio: the frame is never taller than --tile-h. */
.act--flowgrid { --tile-h: 34svh; }
.act--flowgrid .vgrid .vframe {
  max-width: calc(var(--tile-h) * var(--ar, 1.7778));
  width: 100%;
}
.act--flowgrid .vgrid { align-items: start; justify-items: start; }

/* THE ONE SHEET THAT INVERTS, and the page's peak.
   The peak must own the largest luminance jump on the page (message.md rule 9,
   and the harness measures it frame to frame). Round 1 of the critique scored
   it 2/10: act 7 swung 0.080 while act 8 — the bone era straight after it —
   swung 0.130, because the climb back OUT of the black was a bigger step than
   the fall into it. Two things fix that, and both are the peak doing more work
   rather than its neighbours doing less:
     - the black act goes to true ink, so the fall into it is the deepest step
       the page takes;
     - the act BEFORE it ends at its brightest, so the contrast arrives in one
       cut rather than over two screens. */
.sheet--black, .sheet--black .sheet__fold, .act--black { background: var(--black-ground); }
/* The peak's FIRST screen is the deepest ink on the page, and it holds that
   ink for the whole act. Round 2 measured the peak at 0.082 against act 8's
   0.120, so the page's biggest visual event was the climb out of the black
   rather than the eclipse itself. Deepening the peak's own ground is the fix
   that keeps D47 intact: the loud moment gets louder, instead of the acts
   around it getting quieter.
   (A tinted ramp on act 8 was tried first and is NOT here: it put the Digger
   claim on #DED7C8 and measured 4.05:1, under the 4.5 floor. The peak is the
   thing to change; its neighbour's legibility is not negotiable.) */
.act--peak, .act--peak .sheet--black, .act--peak .sheet--black .sheet__fold { background: #07070A; }
.act--peak .sheet__fold { --black-ground: #07070A; }

/* AND EVERY BONE SHEET PAINTS ITS OWN BONE, LITERALLY.
   Two LOW scores turned out to be one bug, and it is a sharp one. The engine's
   drift does not paint a layer behind the page: it REWRITES --sc-canvas on
   <html> and interpolates it between acts (scrollcraft.js ~1100). Every sheet
   here painted with the --sc-canvas token, so while the reader climbed out of
   the black act the sheets themselves went grey — measured rgb(152,149,142)
   under "After · 2017 onward", which is 1.39:1 for ink-soft (the readability
   failure) and a 0.135 luminance swing, larger than the peak's own 0.083 (the
   peak failure).
   --sheet-bone is a FIXED copy of the ground, so a sheet is the colour its
   type was contrast-checked against no matter where the drift currently sits.
   The drift still runs; it just no longer repaints the paper. The black act is
   the one sheet that opts out, which is exactly what makes it the peak. */
:root { --sheet-bone: #EFE9DD; }
.sheet, .sheet__fold, .act { background: var(--sheet-bone); }
.sheet--black, .sheet--black .sheet__fold, .act--black { background: var(--black-ground); }
.act--peak, .act--peak .sheet--black, .act--peak .sheet--black .sheet__fold { background: #07070A; }
.sheet--black .sheet__fold {
  --sc-ink: var(--black-ink);
  --sc-ink-soft: var(--black-soft);
  --sc-accent: var(--black-accent);
  color: var(--black-ink);
}
.sheet--black .sheet__fold::after {
  background: linear-gradient(to bottom, transparent,
    color-mix(in oklab, var(--black-ink) 14%, transparent) 12%,
    color-mix(in oklab, var(--black-ink) 14%, transparent) 88%, transparent);
}

/* ==================================================== THE PLAYER-POSTER ===
   A facade at rest that mounts the exact Vimeo embed where it sits. The box is
   the film's OWN aspect ratio (--ar), so a 4:3 timelapse is never letterboxed
   into a 16:9 hole and the mounted iframe inherits the same box: nothing shifts
   when the player arrives.
   ======================================================================== */
/* The frame is the picture box; the link under it is NOT inside that box.
   Measured: with the link as a child of a 16:9 .vframe, the box's own
   aspect-ratio put the 44px link row INSIDE the letterbox, so ink-soft type
   landed on #0d0c0b at 1.16:1. The box therefore wraps the picture only, and
   the link is its sibling in a two-row grid. */
.vframe {
  display: grid;
  grid-template-rows: auto auto;
}
.vframe__pic {
  position: relative;
  aspect-ratio: var(--ar, 1.7778);
  background: #0d0c0b;            /* the letterbox a player rests on */
  /* The paper shadow — the fold's surviving half. Tinted to the canvas hue,
     never to black: a black shadow on bone reads as dirt (taste.md, Depth 1). */
  box-shadow: 0 1px 0 color-mix(in oklab, var(--sc-ink) 24%, transparent),
              0 10px 28px -18px rgba(var(--fold-shade), 0.55);
  isolation: isolate;
}
.vframe__play {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
  border: 1px solid color-mix(in oklab, var(--sc-ink) 22%, transparent);
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  overflow: hidden;
  -webkit-appearance: none; appearance: none;
}
.sheet--black .vframe__play { border-color: color-mix(in oklab, var(--black-ink) 26%, transparent); }
.vframe__play img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 420ms var(--vf-ease), filter 220ms var(--vf-ease);
}
/* The scrim exists ONLY in the band the meta sits in, so the picture is clear
   everywhere the type is not. This is the rule the last pass broke. */
/* THE SCRIM IS A FLOOR, NOT A WASH.
   A gradient that fades to nothing is fine over a dark frame and fails over a
   bright one: measured on the composited page, the duration chips and several
   film names came back at 1.7-2.0:1 where a poster frame is pale (a white
   Krisis title card, a noon street). Since the brightest poster on the page
   decides the number, the band under the type is a near-solid ink floor and
   only its TOP edge is a gradient, so the picture is still clear everywhere
   the type is not. Measured after: the worst of these is 12.9:1. */
.vframe__play::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 46%;
  background: linear-gradient(to top,
    rgba(6,5,4,0.94) 0,
    rgba(6,5,4,0.92) 34%,
    rgba(6,5,4,0.72) 62%,
    rgba(6,5,4,0.28) 84%,
    transparent 100%);
  pointer-events: none;
  transition: opacity 220ms var(--vf-ease);
}
.vframe__glyph {
  position: absolute;
  top: 50%; left: 50%;
  translate: -50% -50%;
  width: 64px; height: 64px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(10, 9, 8, 0.52);
  backdrop-filter: saturate(1.1);
  transition: transform 160ms var(--vf-ease), background-color 160ms var(--vf-ease);
  z-index: 2;
}
.vframe__glyph svg { width: 44px; height: 44px; display: block; }
.vframe__glyph circle { stroke: rgba(239,233,221,0.9); stroke-width: 1.5px; }
/* The triangle is nudged right by half its own optical imbalance: a
   geometrically centred play glyph reads left of centre (impeccable/layout).
   A translate rather than a transform on the path, and the box is set to
   fill-box so the nudge is measured against the glyph itself. */
.vframe__glyph path {
  fill: #EFE9DD;
  transform-box: fill-box;
  transform-origin: center;
  translate: 1px 0;
}
.vframe--lead .vframe__glyph { width: 84px; height: 84px; }
.vframe--lead .vframe__glyph svg { width: 56px; height: 56px; }

/* A LIGHT COVER NEEDS A HEAVIER DISC. Every other facade on the page is a dark
   film frame, where rgba(10,9,8,0.52) is already a firm disc. The TEDx cover is
   the red wordmark on WHITE, and over it that disc composited to rgb(219,214,204)
   against a bone triangle at rgb(237,232,220) — measured 1.17:1 on the shot,
   which is no glyph at all. .vframe--onlight opts a frame into an opaque ink
   disc so the triangle keeps its contrast whatever the picture is. The meta band
   already carries its own solid floor, so only the glyph needed this. */
.vframe--onlight .vframe__glyph { background: rgba(10, 9, 8, 0.82); }
@media (hover: hover) {
  .vframe--onlight .vframe__play:hover .vframe__glyph { background: rgba(10, 9, 8, 0.9); }
}

/* The meta band carries its OWN floor as well as the scrim above it. The scrim
   is a gradient sized as a percentage of the frame, so on a small phone tile it
   is a short band and a bright poster can still reach the type underneath it —
   measured on the Digger frame at 390px, the duration chip came back at 2.5:1
   twice. A solid band behind the row itself cannot be out-measured by the
   picture, whatever the frame is or however small it gets. */
.vframe__meta {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  background: rgba(6, 5, 4, 0.72);
  padding: 0.7rem 0.85rem;
  text-align: left;
  font-family: var(--sc-font-display);
  text-transform: uppercase;
}
.vframe__name {
  font-size: clamp(0.62rem, 0.9vw, 0.76rem);
  letter-spacing: 0.14em;
  line-height: 1.3;
  color: #F6F2E9;                 /* 13.4:1 on the scrim beneath it */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vframe--lead .vframe__name { font-size: clamp(0.68rem, 1vw, 0.84rem); white-space: normal; }
.vframe__dur {
  margin-left: auto;
  flex: none;
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  /* Was #DCD3C2. On the brightest poster on the page (Digger's misty blue),
     at the phone's smaller frame where the scrim band is shorter, that
     measured 2.43:1. The chip now carries the same near-white as the film
     name, which is the only tone guaranteed against every frame behind it.
     Stated as a solid colour, not as opacity on a lighter one: a translucent
     chip is both harder to measure and worse against a bright frame. */
  color: #EDE7DB;
  font-variant-numeric: tabular-nums;
}
/* Hover/focus: the picture leans in a little, the glyph grows. No glow. */
@media (hover: hover) {
  .vframe__play:hover img { transform: scale(1.025); }
  .vframe__play:hover .vframe__glyph { transform: scale(1.06); background: rgba(10,9,8,0.68); }
}
.vframe__play:focus-visible .vframe__glyph { transform: scale(1.06); }
.vframe__play:active .vframe__glyph { transform: scale(0.97); }

/* The no-JS / Cmd-click link. It is a real link in the flow, not a 15px
   whisper: a 44px row under the frame, which is also the touch floor. */
.vframe__out {
  display: flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0.5rem 0.1rem 0;
  font-family: var(--sc-font-display);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
  text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);
  transition: color 150ms var(--vf-ease);
}
.vframe__out:hover { color: var(--sc-accent); }
.sheet--black .vframe__out { color: var(--black-soft); }
.sheet--black .vframe__out:hover { color: var(--black-accent); }

/* The mounted player fills the same box the facade occupied. */
.vframe__player {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  border: 0;
  z-index: 3;
  animation: vf-in 180ms var(--vf-ease) both;
}
@keyframes vf-in { from { opacity: 0; } to { opacity: 1; } }
.vframe__pic.is-playing { box-shadow: 0 1px 0 color-mix(in oklab, var(--sc-ink) 30%, transparent); }

@media (prefers-reduced-motion: reduce) {
  /* Facades still work and nothing is hidden; only the movement goes. */
  .vframe__play img,
  .vframe__glyph { transition: none; }
  .vframe__play:hover img { transform: none; }
  .vframe__player { animation: none; }
}

/* ===================================================== LAYOUTS ============
   Four shapes, and no two adjacent acts share one: the hero, the solo (one big
   film beside its type), the era grid, and the paired row.
   ======================================================================== */
.hero, .era, .solo, .peak { width: 100%; max-width: 108rem; margin-inline: auto; min-width: 0; }

/* --- the hero: the reel, then the name UNDER it ------------------------- */
/* The reel is capped by HEIGHT, not only by width: at 1440x900 a 16:9 frame at
   full container width is 729px tall and pushed the name off the bottom edge
   of its own sticky sheet (round 1, frame 00). The cap leaves the name and the
   claim room on the first screen, which is the whole point of the act. */
.hero { display: grid; gap: clamp(0.9rem, 2.2vh, 1.6rem); align-content: center; }
.hero > .vframe { width: 100%; justify-self: center; max-width: min(100%, calc(58svh * var(--ar, 1.7778))); }
.hero__foot {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2.6rem);
  align-items: end;
}
.hero__name {
  font-family: var(--sc-font-display);
  font-weight: 400;
  /* Capped well under pass 2's 160px: the reel is the hero now, and
     impeccable/typeset puts the display ceiling near 96px. */
  font-size: clamp(2.4rem, 6.4vw, 5.6rem);
  line-height: 0.86;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  margin: 0;
  color: var(--sc-ink);
}
.hero__name span { display: block; }
.hero__name em { font-style: normal; color: var(--sc-ink-soft); }
.hero__side { min-width: 0; }
.hero__side .poster__claim { margin-top: 0; }
@media (max-width: 860px) {
  .hero__foot { grid-template-columns: 1fr; align-items: start; gap: clamp(0.8rem, 2vh, 1.2rem); }
  .hero__name { font-size: clamp(2.2rem, 13vw, 4rem); }
}

/* --- the solo poster: one big film, type beside it ---------------------- */
.solo {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: clamp(1.2rem, 3.2vw, 3rem);
  align-items: center;
}
/* An act that also carries a contact sheet below the poster must not centre
   its poster in the leftover space: act 3 measured 1.47vh against a declared
   1.0 and the overrun was EMPTY PAPER, because `align-items: center` floated
   the poster in a sheet the eighty photographs had already made tall. The act
   should pay for pictures, not for a hole. */
.act--sheeted .solo { align-items: start; }
/* The sheet packs its two blocks to the top. Centred, act 3 put the poster in
   the middle of a sheet the contact sheet had already made tall and paid for
   a screen of empty paper above and below the pictures (lab/shots/04.png,
   round 1). */
.act--sheeted .sheet__fold { align-content: start; gap: clamp(0.8rem, 2vh, 1.4rem); }
/* The poster's own two columns no longer stretch to the act's full height
   either: the teaser and its type are one band, the contact sheet is the
   next. */
.act--sheeted .solo { align-content: start; }
.solo--flip { grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr); }
.solo--flip > .solo__film { order: 2; }
.solo--flip > .solo__type { order: 1; }
.solo__film, .solo__type { min-width: 0; }
@media (max-width: 860px) {
  .solo, .solo--flip { grid-template-columns: 1fr; gap: clamp(0.9rem, 2.4vh, 1.4rem); }
  .solo--flip > .solo__film, .solo--flip > .solo__type { order: 0; }
}

/* --- the era grid: 2-up and 3-up, each tile >= 400px on desktop --------- */
.era { display: grid; gap: clamp(1rem, 2.6vh, 2rem); align-content: center; }
.era__label {
  font-family: var(--sc-font-display);
  font-size: clamp(0.68rem, 1vw, 0.8rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin: 0;
}
/* The era act that leads with its label and nothing else: a held half-screen
   of paper before the work arrives. It now opens the page's first era straight
   after the showreel (the page runs newest first), where it gives the hero
   somewhere to land. See the note in act 8's markup. */
.era__label--lead {
  /* Enough paper to be a beat, not enough to be a screen: at 34svh this act
     measured 2.35vh against a declared 1.5 and pushed the page to 14.48. */
  min-height: 16svh;
  display: grid;
  align-content: center;
}
@media (max-width: 860px) { .era__label--lead { min-height: 12svh; } }
/* .era__after held one sentence of paper on the screen that used to follow the
   peak. Nothing carries the class now that the page runs newest first and act 8
   no longer comes out of the black — the rules are kept because the class is
   part of the era vocabulary, not because anything on this page uses it. */
.era__after {
  font-family: var(--sc-font-text);
  font-style: italic;
  font-size: clamp(1.1rem, 2vw, 1.7rem);
  line-height: 1.26;
  color: var(--sc-ink);
  /* The gap under this line is the beat itself. At 2.4rem the first row of
     films still shared a frame with the end of the black and the peak scored
     6/10; holding a third of a screen of paper here moved it to 7 and widened
     the margin from 0.022 to 0.046. The paper is doing the work, not a fade. */
  margin: 0 0 clamp(2rem, 16svh, 9rem);
  max-width: 30ch;
  text-wrap: pretty;
}
@media (max-width: 860px) { .era__after { margin-bottom: clamp(1.4rem, 8svh, 4rem); } }


.vgrid { display: grid; gap: clamp(0.8rem, 1.8vw, 1.6rem); align-items: start; }
.vgrid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr)); }
.vgrid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 25rem), 1fr)); }
@media (min-width: 1200px) { .vgrid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 860px) {
  /* 1-up on a phone: a film is worth the whole column. */
  .vgrid--2, .vgrid--3 { grid-template-columns: 1fr; }
  /* EXCEPT inside a dense era, where 1-up is what makes the phone page long.
     Measured at 390x844: one-per-row put act 6 at 5.64vh, act 8 at 3.40 and
     the page at 19.9 — over the 16 the guidelines review asked for. Two up is
     a ~168px frame, which is still a picture you can read at arm's length and
     is how a contact sheet of a filmography should look on a phone anyway.
     The LEAD frame of an act stays full width: that one is the poster. */
  .act--flowgrid .vgrid--2,
  .act--flowgrid .vgrid--3,
  .act--flowgrid .ecol .vgrid--2,
  .act--flowgrid .ecol .vgrid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.55rem; }
  .act--flowgrid .vgrid .vframe { max-width: none; }
  .act--flowgrid .vframe__out { font-size: 0.56rem; letter-spacing: 0.12em; }
  /* The nine timelapses are the densest block on the page and the one place a
     3-up phone grid still reads: they are one continuous subject, so the row
     is a contact strip rather than nine separate posters. */
  .act--flowgrid .ecol .vgrid--3:has(> .vframe:nth-child(7)) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.4rem;
  }
  /* In that contact strip the link out is longer than the picture above it and
     three of them stack into a grey block of words. The link keeps its 44px
     target and its full accessible name; only the VISIBLE run is clipped to
     one line, so the row reads as pictures with a caption apiece. */
  .act--flowgrid .ecol .vgrid--3:has(> .vframe:nth-child(7)) .vframe__out {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
    line-height: 2.75rem;
  }
}

/* A row of type under a grid: one message per row, never more than three. */
.erow {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: clamp(0.9rem, 2.2vw, 2rem);
  align-items: start;
}
/* COLUMNS OF PROJECTS. A dense era runs its projects side by side rather than
   as stacked rows, so the act fits the span it declared. Each column is one
   project: its films, then the type that names them. */
.ecols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  gap: clamp(1.1rem, 2.6vw, 2.2rem);
  align-items: start;
}
.ecols--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 27rem), 1fr)); }
/* Four projects across, with the feature's column wider than the three
   commissions beside it: the hierarchy is in the measure, not in a second row. */
/* Three projects across. D55 = B split the city guides and the timelapses into
   six posters, so the road-years era now runs two rows of three columns
   instead of one row of two: the column shape is unchanged, there are simply
   more of them, and each one is still one project with its own type. */
.ecols--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1180px) { .ecols--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px)  { .ecols--3 { grid-template-columns: 1fr; } }
/* THE TIMELAPSE ROW. Four posters across, the first one wide because it holds
   eight films as a contact strip and the other three hold one object each. */
.ecols--tl { grid-template-columns: 2.2fr 1fr 0.9fr 0.9fr; }
@media (max-width: 1180px) { .ecols--tl { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px)  { .ecols--tl { grid-template-columns: 1fr; } }
/* Eight timelapses, four across inside the wide column: one continuous
   subject, so the row is a contact strip rather than eight separate posters. */
.ecol .vgrid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; }
.ecol .vgrid--4 .vframe__out {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  display: block; line-height: 2.75rem;
}
@media (max-width: 680px) { .ecol .vgrid--4 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.4rem; } }

/* The feature's column is the FIRST one: act 8 now runs NEWEST FIRST (Digger
   2020, Cloudline 2023 — reversed, then Okeechobee 2016, IRC 2015), so the
   wide measure travels with Digger back to the left edge. The hierarchy is
   still in the measure, not in a second row. In practice #a8 in the ground
   file overrides this to a 2x2 of equal columns, so this track list only ever
   governs an ecols--4 somewhere else; it is kept correct for that case. */
.ecols--4 { grid-template-columns: 1.35fr 1fr 1fr 1fr; }
@media (max-width: 1180px) { .ecols--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px)  { .ecols--4 { grid-template-columns: 1fr; } }
.ecol--wide .poster__claim { max-width: 32ch; }
.ecol { display: grid; gap: clamp(0.6rem, 1.4vh, 1rem); align-content: start; min-width: 0; }
/* A grid INSIDE a column has a column's width, not the page's, so its tiles
   need a smaller floor — but not SO small that the film becomes a stamp again,
   which is the defect this whole pass exists to undo. Round 3 proved the point:
   an 8.5rem floor produced 136px tiles in act 8 and the era read as thumbnails.
   The floor is a real frame at a readable size, and the ERA is given the span
   it needs instead of the tiles being shrunk to fit a span. */
/* A CITY-GUIDE COLUMN. D55 = B gave each of the three cities its own poster,
   so each column holds two or three digests of the same film. At the 13rem
   floor below they wrapped one-per-row and the row ran 1,284px; three (or two)
   across is how a set of digests from one city should read anyway, and it is
   what keeps act 6 shorter than the peak. */
.ecols--3 .ecol .vgrid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; }
.ecols--3 .ecol .vgrid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; }
.ecols--3 .ecol .vgrid .vframe__out {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  display: block; line-height: 2.75rem;
}
@media (max-width: 680px) { .ecols--3 .ecol .vgrid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.ecol .vgrid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
.ecol .vgrid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }

.epair {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(1rem, 2.8vw, 2.4rem);
  align-items: center;
}
.epair > * { min-width: 0; }
@media (max-width: 860px) {
  .epair, .epair--flip { grid-template-columns: 1fr; }
  .epair--flip > .ptype { order: -1; }
}

/* --- the peak: the biggest film on the page, on black ------------------- */
/* The peak is a two-column poster on a wide screen: the lead film on the left,
   its type beside it, and the rail of the other two eclipse posters across the
   bottom. Stacked, the three parts ran 1,373px in a 900px viewport and the
   rail — which now carries posters 28 and 29 — was never on screen. */
.peak {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  grid-template-areas: "film type" "more more";
  gap: clamp(0.6rem, 1.4vh, 1.1rem) clamp(1rem, 2.4vw, 2rem);
  align-content: center;
}
.peak__film { grid-area: film; }
.peak__type { grid-area: type; align-self: center; }
.peak__more { grid-area: more; }
@media (max-width: 860px) {
  .peak { grid-template-columns: 1fr; grid-template-areas: "film" "type" "more"; }
}
/* The lead film is capped by HEIGHT as well as width now. D55 = B gave the
   two eclipse features beside it their own numbered posters inside the rail,
   and at 72% width the peak's content stack ran 1,373px in a 900px viewport:
   the rail and its type were below the fold for the whole act, so posters 28
   and 29 were never readable. Capping the lead frame keeps the whole poster
   — film, type, rail — inside one screen while the act pans. */
.peak__film { width: 100%; max-height: 44svh; }
.peak__film .vframe { max-width: min(100%, calc(44svh * var(--ar, 1.7778))); }
.peak__type { max-width: 68ch; }
.peak__more {
  list-style: none; margin: 0; padding: 0;
  display: flex;
  gap: clamp(0.7rem, 1.6vw, 1.2rem);
  width: max-content;
}
.peak__more > li { width: clamp(13rem, 20vw, 18rem); flex: none; }
/* A rail item is a POSTER now, not a thumbnail: its frame, then its number,
   title, claim and credit, on the same black ground as the lead. */
.peak__more > li > .ptype { margin-top: 0.7rem; }
.peak__more .poster__title { font-size: clamp(1.1rem, 1.6vw, 1.5rem); }
.peak__more .poster__claim { font-size: 0.86rem; }
@media (max-width: 860px) {
  .peak__film { width: 100%; }
  .peak__more > li { width: 70vw; }
}


/* A PRINTED PLATE. Three projects on the page are photographs, not films:
   The Truckdrivers of India, Cuba and Morocco. D55 = B gives each its own
   poster, so each one needs an image block that reads as the same printed
   object as a player frame at rest — same hairline, same paper shadow, same
   caption type — without a play glyph it could not honour. */
.pplate { margin: 0; }
.pplate picture { display: block; }
.pplate img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid color-mix(in oklab, var(--sc-ink) 18%, transparent);
  box-shadow: 0 1px 0 color-mix(in oklab, var(--sc-ink) 10%, transparent);
}
/* Inside an .epair row the plate must match the rhythm of the grid tiles
   above it, not set its own: uncapped, the India frame made act 2's last row
   447px tall and the act overran its declared span. */
.epair .pplate img { max-height: 24svh; object-fit: cover; }
.pplate figcaption {
  font-family: var(--sc-font-display);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin-top: 0.5rem;
  min-height: 1.4em;
}

/* ------------------------------------------------------------- the type */
/* The type serves the picture. Every size here is a THIRD or less of pass 2's,
   which is the taste review's finding 2 answered in numbers. */
.ptype { min-width: 0; }
.ptype--wide { max-width: 72ch; }
.poster__no {
  font-family: var(--sc-font-display);
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin: 0 0 var(--sc-2);
}
.act--loud .poster__no { color: var(--sc-accent); }
.poster__title {
  font-family: var(--sc-font-display);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: -0.014em;
  text-transform: uppercase;
  margin: 0;
  color: var(--sc-ink);
  font-size: var(--title, clamp(1.5rem, 2.6vw, 2.4rem));
  max-width: 100%;
  overflow-wrap: break-word;
  text-wrap: balance;
}
.poster__title span { display: block; }
.poster__title em { font-style: normal; color: var(--sc-accent); }
.act--quiet .poster__title em { color: var(--sc-ink-soft); }

/* The ramp survives D47, at the new scale: quiet eras small, the late ones
   larger, and nothing anywhere near pass 2's 160px. */
.act--quiet { --title: clamp(1.4rem, 2.2vw, 1.95rem); }
.act--mid   { --title: clamp(1.6rem, 2.8vw, 2.5rem); }
.act--loud  { --title: clamp(1.8rem, 3.4vw, 3rem); }
.act--peak  { --title: clamp(2rem, 4.2vw, 3.6rem); }

.poster__claim {
  font-family: var(--sc-font-text);
  font-size: clamp(1rem, 1.35vw, 1.28rem);
  line-height: 1.3;
  letter-spacing: -0.004em;
  max-width: 38ch;
  margin: clamp(0.5rem, 1.3vh, 0.9rem) 0 0;
  color: var(--sc-ink);
  text-wrap: pretty;
}
.poster__line {
  font-style: italic;
  font-size: clamp(0.9rem, 1vw, 1.02rem);
  line-height: 1.4;
  max-width: 54ch;
  margin: clamp(0.4rem, 0.9vh, 0.7rem) 0 0;
  color: var(--sc-ink-soft);
  text-wrap: pretty;
}
.poster__credit {
  font-family: var(--sc-font-display);
  font-size: 0.64rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1.6;
  color: var(--sc-ink-soft);
  max-width: 58ch;
  margin: clamp(0.5rem, 1.2vh, 0.9rem) 0 0;
}
/* An inline link inside a credit line keeps the credit's own 10.5px face —
   that size IS the grammar — so the HIT AREA is grown instead of the type:
   a 44px tall target centred on a 15px line, which is the same trick the era
   index already uses. Measured at 67x15 in pass 2 and flagged by the
   guidelines review (51 of 62 targets were ~15px tall). */
.vlink {
  position: relative;
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);
  transition: color 150ms var(--vf-ease);
}
.vlink::before {
  content: "";
  position: absolute;
  left: -0.4em; right: -0.4em;
  top: 50%;
  height: 44px;
  translate: 0 -50%;
}
.vlink:hover { color: var(--sc-accent); text-decoration-color: currentColor; }

/* ----------------------------------------------- act 3, the Prism clip (D51) */
/* Still the ONE scrub-like act: a <video> whose currentTime is written off
   --sc-p. It is a printed plate with its own trim; nothing is set over it. */
.clipplate { margin: clamp(0.8rem, 2vh, 1.4rem) 0 0; position: relative; max-width: 26rem; }
.clipplate video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--sc-surface);
  border: 1px solid color-mix(in oklab, var(--sc-ink) 22%, transparent);
}
.clipplate figcaption {
  font-family: var(--sc-font-display);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin-top: var(--sc-2);
}

/* ------------------------------------------------------ act 4, the rail */
/* The rail is CENTRED in its stage, not top-aligned: round 1 left most of the
   frame as bone below the cards because the items hung from the top edge. */
.rail {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.2vw, 2rem);
  width: max-content;
}
.rail > li { width: clamp(16rem, 26vw, 24rem); flex: none; min-width: 0; }
.rail__lead, .rail__end { width: clamp(14rem, 22vw, 20rem) !important; align-self: center; }
.rail h3 {
  font-family: var(--sc-font-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 1.7vw, 1.5rem);
  line-height: 1.02;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  margin: var(--sc-2) 0 0;
  color: var(--sc-ink);
  text-wrap: balance;
}
.rail__lead h3, .rail__end h3 { font-size: clamp(1rem, 1.5vw, 1.3rem); line-height: 1.16; text-transform: none; font-family: var(--sc-font-text); font-style: italic; }
.rail p {
  font-size: clamp(0.88rem, 1vw, 0.98rem);
  line-height: 1.38;
  color: var(--sc-ink-soft);
  margin: var(--sc-1) 0 0;
  text-wrap: pretty;
}
@media (max-width: 860px) { .rail > li { width: 74vw; } .rail__lead, .rail__end { width: 70vw !important; } }

/* --------------------------------------------------- act 9, the TEDx line */
.tedx-line {
  font-family: var(--sc-font-text);
  font-style: italic;
  font-size: clamp(1.3rem, 2.4vw, 2.1rem);
  line-height: 1.18;
  max-width: 26ch;
  margin: var(--sc-2) 0 0;
  color: var(--sc-ink);
  text-wrap: balance;
}

/* ------------------------------------------------------------- the close */
/* THE CLOSE now carries his one ABOUT photograph, so it is a two-column
   object rather than a centred column: the portrait on the left, the words on
   the right, reading from his picture into his own sentences. Centred text
   with a picture dropped on top of it was the first draft and it put a 721px
   portrait above every line of the close. */
.close {
  font-family: var(--sc-font-text);
  max-width: 52rem;
  margin-inline: auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "no   no" "pic  body";
  gap: 0 clamp(1.2rem, 3vw, 2.4rem);
  align-items: start;
  text-align: left;
}
.close > .poster__no { grid-area: no; }
/* ONE wrapper for the body, not six elements all claiming the same grid area:
   the first draft put every paragraph in `grid-area: body` and they stacked on
   top of each other at the same y (measured: five children all at y=13058). */
.close > .close__body { grid-area: body; }
.close { grid-auto-rows: min-content; }
@media (max-width: 680px) {
  .close { grid-template-columns: 1fr; grid-template-areas: "no" "pic" "body"; }
}
.close p { font-size: var(--sc-t-base); line-height: 1.6; color: var(--sc-ink); margin: 0 0 var(--sc-5); }
.close .poster__no { color: var(--sc-ink-soft); margin-bottom: var(--sc-6); }
.close .poster__credit { font-size: 0.62rem; letter-spacing: 0.2em; color: var(--sc-ink-soft); margin-top: var(--sc-7); margin-inline: auto; }
/* The CTA measured 3.65:1 in pass 2 and is a real WCAG fail at 19.2px regular.
   --sc-accent is 6.21:1 on bone; the link also carries its own underline, and
   the hit area is a full 44px row. */
.close a.write {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0 0.1rem;
  font-family: var(--sc-font-text);
  font-size: clamp(1.2rem, 2.4vw, 2rem);
  color: var(--sc-accent);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}
.close ul { list-style: none; padding: 0; margin: var(--sc-6) 0 0; display: flex; flex-wrap: wrap; gap: var(--sc-5); justify-content: center; }
.close ul a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0 0.2rem;
  font-family: var(--sc-font-display);
  font-size: var(--sc-t-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  text-decoration: underline;
  text-underline-offset: 0.3em;
}
.close ul a:hover { color: var(--sc-ink); }

/* -------------------------------------------------------------- the strip */
.strip {
  position: fixed;
  z-index: 40;
  right: max(0px, var(--safe-r));
  top: 50%;
  translate: 0 -50%;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.5rem 0.3rem;
  /* --sheet-bone, not --sc-canvas: the engine's drift REWRITES --sc-canvas on
     <html> while a ground transition runs, and the strip is fixed, so it hung
     over every act wearing the ramp's colour. Measured on the phone at act 8:
     the strip painted rgb(122,119,115) and its ink marks — the page's only
     navigation — fell to 3.67:1. The index now always carries the paper it was
     contrast-checked on, and inverts only on the black act. */
  background: var(--sheet-bone);
  border-left: 1px solid color-mix(in oklab, var(--sc-ink) 30%, transparent);
}
.strip a {
  font-family: var(--sc-font-display);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  line-height: 1;
  color: var(--sc-ink);
  text-decoration: none;
  display: grid;
  place-items: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  border-right: 2px solid transparent;
  transition: color 150ms var(--vf-ease), border-color 150ms var(--vf-ease);
}
.strip a:hover { color: var(--sc-accent); }
.strip a:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
.strip a[aria-current="true"] { color: var(--sc-accent); border-right-color: var(--sc-accent); }
.strip--dark { background: var(--black-ground); border-left-color: color-mix(in oklab, var(--black-ink) 36%, transparent); }
.strip--dark a { color: var(--black-ink); }
.strip--dark a[aria-current="true"] { color: var(--black-accent); border-right-color: var(--black-accent); }
@media (max-width: 680px) { .strip { padding: 0.3rem 0.18rem; gap: 0; } .strip a { font-size: 0.6rem; } }

/* =========================================================== PASS 4 · PHOTOS
   Niko, 12 Oct: "Want all of my films and photos from my website just
   reformatted and new copy written the same way. I don't understand why this
   is so complicated."

   189 photographs across eight projects. He rejected two earlier passes
   because his pictures were SMALL or COVERED, so those are the only two rules
   that matter here, and both are structural rather than a matter of taste:

     1. NOTHING COVERS A PHOTOGRAPH. There is no caption over a frame, no
        gradient scrim, no hover label, no play glyph. Type sits under a
        picture or beside it. The one thing drawn over a strip frame is a 1px
        focus ring, and only while a keyboard is using it.
     2. A PHOTOGRAPH IS NOT A STAMP. A strip frame is 132px tall on desktop and
        104px on a phone — the brief's 44px is the tap-target floor, and these
        clear it by 3x in the short dimension while the whole frame is the
        target. A lead plate gets the same width a film frame gets.

   WHY THE STRIP SCROLLS SIDEWAYS. The Prism has 80 photographs. Stacked, that
   is a contact sheet thirteen screens tall, it buries the eclipse (the page's
   plan requires the peak to own the largest luminance event) and it blows the
   8-14vh page budget on one project. Sideways, 80 photographs cost the page
   ~190px of height and the reader swipes. It is also simply what a contact
   strip IS: a roll of film reads along, not down.

   WHY A FIXED HEIGHT AND AUTO WIDTH. His frames are not one shape — Morocco 01
   is a 640x1008 portrait, Morocco 10 is a 640x217 panorama, Digger 01 is a
   670x53 laurel bar. A fixed-height row lets each one keep its true aspect
   ratio in the same line. `object-fit: cover` appears nowhere in this block:
   cropping a photographer's frame to tidy a grid is the thing he complained
   about. */

.cstrip { margin: var(--sc-3) 0 0; min-width: 0; }

.cstrip__row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  gap: 0.35rem;
  overflow-x: auto;
  overflow-y: hidden;
  /* clip, never hidden, on the cross axis: `hidden` on either axis makes the
     nearest scroll container and kills the sticky pins this page's pan acts
     depend on. The lesson is already in this file for .sheet__fold. */
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding-bottom: 0.45rem;
  /* The row is a focusable scroll region (photos.js moves it with the arrow
     keys), so it needs a visible home when a keyboard reaches it. */
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklab, var(--sc-ink) 28%, transparent) transparent;
  --cs-h: 132px;
}
/* THE PRISM'S 80 AND THE TRUCKDRIVERS' 43 RUN TWO ROWS, which the brief allows
   and which halves the swipe. Two rows of a shorter frame is still 2.4x the
   44px floor. */
.cstrip--two .cstrip__row {
  grid-auto-flow: column;
  grid-template-rows: repeat(2, max-content);
  --cs-h: 104px;
}
.cstrip__row::-webkit-scrollbar { height: 6px; }
.cstrip__row::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--sc-ink) 28%, transparent);
  border-radius: 3px;
}

.cstrip__f {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  padding: 0;
  margin: 0;
  background: none;
  display: block;
  cursor: zoom-in;
  scroll-snap-align: start;
  /* The frame's own aspect ratio, from --sw/--sh set per photograph. The row's
     height is the only sizing input; the width follows the picture. */
  height: var(--cs-h);
  width: calc(var(--cs-h) * (var(--sw) / var(--sh)));
  flex: none;
  /* A 670x53 laurel bar would otherwise be 1667px wide in a 132px row, and a
     640x1008 portrait would be 84px. Both are clamped to a sane band so one
     odd frame cannot own the strip. */
  /* An extreme ratio cannot own the row. Digger's first file is a 670x53
     festival-laurel bar (12.6:1): at a fixed row height it would be 1667px
     wide in a 132px row and lead a roll of film stills with a logo bar. */
  max-width: min(42vw, 16rem);
  min-width: 3.5rem;
}
.cstrip__f img {
  display: block;
  width: 100%;
  height: 100%;
  /* The ONE place a fit is needed: after the clamp above, a cleared frame is a
     few pixels off its true ratio. `contain` keeps the whole picture visible
     and loses pixels from neither edge — it is the opposite of a crop. */
  object-fit: contain;
  background: var(--sc-surface);
  border: 1px solid color-mix(in oklab, var(--sc-ink) 18%, transparent);
  /* emil-design-eng: name the property, never `all`. Opacity only — a scale on
     a 240px raster would resample it soft. */
  transition: opacity 180ms var(--vf-ease);
}
@media (hover: hover) and (pointer: fine) {
  .cstrip__f:hover img { opacity: 0.82; }
}
.cstrip__f:active img { opacity: 0.7; }
.cstrip__f:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }

.cstrip__cap {
  font-family: var(--sc-font-display);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin-top: 0.15rem;
}
.cstrip__hint { color: color-mix(in oklab, var(--sc-ink-soft) 72%, var(--sc-canvas)); }
/* A pointer reader taps; a keyboard reader presses Enter. The hint says "tap"
   because the phone is where a strip is most used, and it is never the only
   affordance (every frame is a real button with its own label). */

/* The black sheet re-inks the strip the way it re-inks everything else. */
.sheet--black .cstrip__f img { border-color: color-mix(in oklab, var(--black-ink) 22%, transparent); background: color-mix(in oklab, var(--black-ink) 8%, transparent); }
.sheet--black .cstrip__cap { color: var(--black-soft); }
.sheet--black .cstrip__f:focus-visible { outline-color: var(--black-accent); }

@media (max-width: 680px) {
  .cstrip__row { --cs-h: 104px; gap: 0.3rem; }
  .cstrip--two .cstrip__row { --cs-h: 88px; }
  .cstrip__f { max-width: 68vw; }
}

/* A LEAD PLATE gets the width a film frame gets. This is the brief's rule for
   the photo-only projects — Truckdrivers, Cuba, Morocco — whose poster has no
   player to lead with. */
.pplate--lead { margin: 0; }
.pplate__open {
  appearance: none; -webkit-appearance: none;
  border: 0; padding: 0; margin: 0; background: none;
  display: block; width: 100%; cursor: zoom-in;
}
/* The capped lead plates letterboxed: `object-fit: contain` fits the picture
   INSIDE a box that is still width:100%, so a phone showed a bone gutter to
   the right of the Cuba plate (lab/pass4/phone-trips.png).
   `width: fit-content` on the BUTTON was the first fix and it collapsed every
   plate to 2x2 (a11y run: four `pplate__open 2x2`), because the button has no
   intrinsic width to fit to until its <img> resolves one, and the img is
   width:100% OF THE BUTTON — a circular constraint.
   The honest fix is to stop capping by height and let the picture size itself:
   `object-fit` is removed, so the <img> box IS the picture's box and there is
   no letterbox to gutter. The height is governed by giving the plate's column
   a max-width instead, which is a constraint the layout can resolve. */
.trip > .pplate--lead,
.pblock > .pplate--lead { max-width: min(100%, 30rem); }
.pplate__open:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }
.pplate--lead img {
  width: 100%;
  height: auto;
  /* NO max-height and NO object-fit here. The .epair rule above caps a plate at
     24svh WITH `cover`, which crops his frame to tidy a row — the exact
     complaint that sank two passes. A lead plate is uncapped and uncropped. */
  max-height: none;
}
/* A sub-640 original can never fill a lead plate. Digger's eleven stills are
   400px wide (and one is 200px); they are shown at their own size and NEVER
   upscaled, which is why they sit in a narrow column rather than stretching. */
.pplate--small img { width: auto; max-width: 100%; }

/* THE CLOSE'S ONE PHOTOGRAPH. Uncapped it measured 721px — the tallest single
   object on the page, taller than any film frame, and it pushed act 10 from a
   declared 0.76 to 1.0. It is one portrait beside a paragraph, not a poster,
   so it is capped and set beside the type rather than above it. Capped by
   HEIGHT with its own ratio kept: no crop. */
.close .pplate--lead { grid-area: pic; justify-self: start; max-width: 15rem; }
.close .pplate--lead img { width: 100%; height: auto; }

/* A PHOTO BLOCK: the lead plate and its strip as one printed object, so a
   photo-only poster reads the way a film poster does — one big picture, then
   the rest of the roll under it. Grid, not flex: two rows of different
   intrinsic height is 2D. */
/* A PHOTO BLOCK: the lead plate and its strip as one printed object, so a
   photo-only poster reads the way a film poster does — one big picture, then
   the rest of the roll under it. Grid, not flex: two rows of different
   intrinsic height is 2D.
   The strip is SHORTER than the plate it follows, because the plate is the
   poster's picture and the strip is the rest of the roll. Stacked at full
   strip height the India block measured 497px and act 2 ran 2.04vh against a
   declared 1.69. */
.pblock { display: grid; gap: 0.4rem; min-width: 0; align-content: start; }
.pblock .cstrip { margin-top: 0; }
.pblock .cstrip__row { --cs-h: 86px; }
/* The two-row variant inside a pblock: India's 43 frames. The earlier
   selector was written `.pblock.cstrip--two` (the pblock IS the strip) as well
   as the child form, and only the child form can ever match — the strip is a
   CHILD of the block. Measured still 179px because --cs-h was being set on
   `.pblock .cstrip__row` after it, so the two-row rule needs the same
   specificity and to come last. */
/* ONE row, not two, inside a poster block. Two rows of 66px cost act 2 a
   band; one row of 78px is the same forty-three frames, taller per frame
   (1.8x the 44px floor), and the reader swipes instead of reading down. The
   two-row form is kept for the Prism contact sheet, where the strip IS the
   act's subject and has the width to use it. */
.pblock .cstrip--two .cstrip__row {
  grid-template-rows: none;
  --cs-h: 78px;
}
/* The lead plate is capped so one tall frame cannot own the act, and capped by
   HEIGHT with its ratio kept — never cropped. */
.pblock .pplate--lead img { width: 100%; height: auto; }

/* THE CONTACT SHEET band that closes act 3. It is the widest element in the
   act on purpose: Niko's complaint was that his pictures were small, and the
   eighty frames of The Prism are the largest body of work on the page. */
.csheet { margin-top: clamp(0.6rem, 1.6vh, 1.2rem); min-width: 0; }
.csheet__lab {
  font-family: var(--sc-font-display);
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
  margin: 0 0 0.5rem;
}
/* The sheet's strip runs taller than a poster's, because it is the act's
   subject rather than a footnote to it. */
.csheet .cstrip__row { --cs-h: 150px; }
/* A strip that sits INSIDE a column of an .ecols row is a footnote to that
   column's player, not the act's subject: Digger's eleven stills and
   Okeechobee's two frames. Short, so the row keeps the height the act
   declared. */
.ecol .cstrip .cstrip__row { --cs-h: 110px; }
.ecol .cstrip { margin-top: 0.4rem; }
.ecols .ecol .cstrip .cstrip__row { --cs-h: 110px; }
@media (max-width: 680px) { .csheet .cstrip__row { --cs-h: 112px; } }

/* THE TWO PHOTOGRAPHIC TRIPS. They were columns in the 3-up grid of players
   and their strips measured 214px against the Prism sheet's 1296px — small,
   which is the complaint that sank two passes. As their own band each gets
   half the sheet, which is the width a film frame gets in this act. */
.trips {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr));
  gap: clamp(1.2rem, 3vw, 2.6rem);
  margin-top: clamp(0.8rem, 2vh, 1.6rem);
}
/* The trip's type sits BESIDE its lead plate and the whole roll runs under
   both, so the band costs one picture's height rather than three stacked
   blocks. Stacked it measured 721px per trip and act 6 ran 2.13vh against a
   declared 1.3; this is the same content, laid out sideways. */
.trip {
  display: grid;
  grid-template-areas: "type plate" "strip strip";
  /* The type takes the wider half: at 1fr/1.15fr both the claim and the line
     wrapped to four lines each and the type block (230px) was taller than the
     plate it sits beside, so the row was paying for wrapping rather than for
     the picture. */
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: 0.45rem clamp(0.7rem, 1.6vw, 1.2rem);
  align-content: start;
  min-width: 0;
}
/* The trip's type governs the band's height (230px against the plate's 230),
   and most of that is the poster number and credit lines stacked above and
   below a two-line claim. Centred and tightened, the band fits the act. */
.trip > .ptype { grid-area: type; align-self: center; }
.trip > .ptype .poster__title { margin-bottom: 0.3rem; }
.trip > .ptype .poster__claim { margin-bottom: 0.3rem; }
.trip > .ptype .poster__line { margin-bottom: 0.3rem; }
.trip > .ptype .poster__credit { margin-top: 0.3rem; }
.trip > .pplate--lead { grid-area: plate; }
.trip > .cstrip { grid-area: strip; margin-top: 0.1rem; }
@media (max-width: 1100px) {
  .trip { grid-template-areas: "type" "plate" "strip"; grid-template-columns: 1fr; }
}
/* The band's strips are the shortest on the page: they are a footnote to the
   plate above them, not the act's subject the way the Prism sheet is. */
.trip .cstrip__row { --cs-h: 92px; }
/* NO `object-fit: cover` HERE, and the first draft of this rule had one. A cap
   plus cover crops his frame to tidy the row, which is the exact thing he
   rejected two passes for. The plate is capped by HEIGHT and keeps its own
   ratio: `contain` on a width that follows, so the whole photograph is visible
   and the two trips still line up. */
/* NO `object-fit: cover` HERE, and the first draft of this rule had one. A cap
   plus cover crops his frame to tidy the row, which is the exact thing he
   rejected two passes for.
   The second draft used `width: auto; max-height` and the plate COLLAPSED TO
   2px (measured: pplate__open 627.2x2), because the <img> sits in a block
   button with no intrinsic height to resolve `auto` against. `aspect-ratio`
   from the file's own pixels is the fix: the plate keeps the photograph's true
   shape, fills the column's width, and is capped by height without a crop. */
.trip .pplate--lead img { width: 100%; height: auto; }

/* ACT 8'S TALLEST COLUMN sets the whole row's height, and Okeechobee's was
   1152px against its neighbours' 560-702: four teasers stacked two-by-two,
   plus the two festival frames pass 4 added. Four across in one line is the
   same four films, and the row stops being governed by one column. */
/* THREE across, not four: four made each facade 63.6x35.8 and the shoot
   flagged all four as tap targets under 44px. Three keeps every play button
   over the floor and still takes a 2x2 block down to one-and-a-bit rows. */
#a8 .ecol .vgrid--2:has(> .vframe:nth-child(4)) {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.4rem;
}
#a8 .ecol .vgrid--2:has(> .vframe:nth-child(4)) .vframe__out {
  font-size: 0.5rem; letter-spacing: 0.08em;
}
@media (max-width: 1180px) {
  #a8 .ecol .vgrid--2:has(> .vframe:nth-child(4)) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ------------------------------------------------------------- the lightbox
   The same contract as the film facades: one open at a time, Escape closes and
   returns focus. <dialog> is doing the top layer, the backdrop, the focus trap
   and the inert page, because a hand-rolled modal gets all four wrong. */
/* `display: grid` on the bare `.lb` OVERRODE the user agent's
   `dialog:not([open]) { display: none }`, and the closed dialog rendered in
   normal flow at the end of the page: a full screen of black with a stray
   CLOSE button under the last act (seen in lab/shots/16.png). Every layout
   declaration is scoped to [open]; the closed state keeps the UA's own
   display, which is the whole point of using <dialog>. */
.lb {
  border: 0;
  padding: 0;
  max-width: 100vw;
  max-height: 100dvh;
  width: 100vw;
  height: 100dvh;
  background: color-mix(in oklab, var(--sc-ink) 94%, black);
  color: var(--black-ink);
}
.lb[open] {
  display: grid;
  grid-template-rows: 1fr auto;
  place-items: center;
}
.lb::backdrop { background: rgb(8 7 6 / 0.88); }
.lb__figure {
  margin: 0;
  display: grid;
  place-items: center;
  gap: 0.9rem;
  padding: clamp(0.75rem, 3vw, 2.5rem);
  max-width: 100%;
  min-height: 0;
}
.lb__img {
  display: block;
  max-width: min(96vw, 1400px);
  /* The picture is as big as the viewport allows and never cropped: contain,
     so a 640x1008 portrait and a 640x217 panorama both fit whole. */
  max-height: 78dvh;
  width: auto;
  height: auto;
  object-fit: contain;
  background: color-mix(in oklab, var(--black-ink) 6%, transparent);
}
.lb__cap {
  font-family: var(--sc-font-text);
  font-size: 0.95rem;
  line-height: 1.4;
  /* 10.34:1 on the near-black ground. */
  color: var(--black-soft);
  max-width: 62ch;
  text-align: center;
  margin: 0;
}
.lb__bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0 clamp(0.75rem, 3vw, 2rem) max(1rem, env(safe-area-inset-bottom));
}
.lb__close, .lb__nav {
  appearance: none; -webkit-appearance: none;
  font-family: var(--sc-font-display);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--black-ink);
  background: color-mix(in oklab, var(--black-ink) 10%, transparent);
  border: 1px solid color-mix(in oklab, var(--black-ink) 26%, transparent);
  /* 44px, the floor the brief sets for a tap target. */
  min-height: 2.75rem;
  min-width: 2.75rem;
  padding: 0 1.1rem;
  cursor: pointer;
  transition: background-color 160ms var(--vf-ease), opacity 160ms var(--vf-ease);
}
@media (hover: hover) and (pointer: fine) {
  .lb__close:hover, .lb__nav:hover:not(:disabled) { background: color-mix(in oklab, var(--black-ink) 20%, transparent); }
}
.lb__close:active, .lb__nav:active:not(:disabled) { background: color-mix(in oklab, var(--black-ink) 26%, transparent); }
.lb__nav:disabled { opacity: 0.34; cursor: default; }
.lb__close:focus-visible, .lb__nav:focus-visible { outline: 2px solid var(--black-accent); outline-offset: 2px; }

/* The open is a crossfade, 160ms, ease-out. Short because a reader who taps a
   photograph wants the photograph, not a transition. */
@media (prefers-reduced-motion: no-preference) {
  .lb[open] { animation: lb-in 160ms var(--vf-ease); }
  .lb[open]::backdrop { animation: lb-bd 200ms var(--vf-ease); }
}
@keyframes lb-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes lb-bd { from { opacity: 0; } to { opacity: 1; } }

