/* ============================================================
   Vidzero concept v4. THE SET
   The paired half of the hero. The hero ends on a finished frame; this is the
   set that frame was made in.

   Rebuilt 6 Aug 2026 on Nick's direction. The first version was a full-bleed
   dark photograph, which was wrong three ways: it broke the palette rule (light
   base, dark only where there is a screen, and a photo of a set is not a
   screen), it put two full-bleed dark surfaces back to back so the handoff had
   nothing to land on, and a big picture with copy in the corner is a standard
   block rather than something the idea produced.

   Now: paper ground, and the set arrives as overlapping frames. The two wider
   shots of the set sit BEHIND, and the shot holding the monitor sits in FRONT
   and largest, so the composition is literally the frame with the set around
   it. No rotation: rotated overlapping photos read as a scrapbook, and this
   wants to read as frames pulled off a timeline and laid over each other.
   ============================================================ */

/* ============================================================
   THE LIFT  (replaces the old handoff, on Nick's direction 6 Aug 2026)

   Was: the set scrolled up over the pinned hero. Now: the set is already in
   place and the hero lifts off it, so the finished frame comes away and the
   set was underneath the whole time. That is the concept as a mechanic.

   It is the layer-order trap from the project notes used the right way round.
   We animate the OUTGOING layer and the incoming one is revealed as a
   consequence, so the hero has to sit ABOVE the set, which is the opposite of
   the default paint order for a later sibling.

   No JS. The hero stage is already sticky and unsticks at
   (vf height - stage height). Pulling the set up by exactly one stage height
   puts its top at the viewport top at that same moment, and pinning the set
   for the next stage-height of scrolling means the hero's own scroll-away IS
   the lift. The margin below must therefore stay equal to .vf__stage's height.
   ============================================================ */

.vf { z-index: 5; }                      /* hero paints above the set */

.hold {
  position: relative; z-index: 1;
  /* The wrapper never hit-tests: a plain div still captures clicks across
     its whole transparent box, and this one spans the entire pinned passage
     over the landed wall. The set re-enables itself below; is-gone turns it
     off again once the film is moving. Round 8b, 26 August 2026. */
  pointer-events: none;
  margin-top: -100dvh;                   /* MUST equal .vf__stage height */
}
.hold__runway { height: 100dvh; }        /* what the set is pinned against */
/* The runways are scroll spacers, not surfaces: their invisible boxes cross
   the viewport for the whole pinned passage, and a transparent layer still
   hit-tests. The dwell's box was eating clicks on the landed wall's arrows
   at the mid-height band. Found on the round 8b restore, 26 August 2026. */
.hold__runway, .hold__expand, .hold__dwell, .workhold__runway, .workhold__pull { pointer-events: none; }
/* the pull-wide's scroll length (the hundred). Forgetting this rule left the
   runway at zero height and --pw dividing zero by zero. */
.workhold__pull { height: 110dvh; }

/* sells it as a layer coming away rather than the page scrolling */
.vf__stage { box-shadow: 0 26px 70px -18px rgba(0, 0, 0, .78); }

@media (prefers-reduced-motion: reduce) {
  /* hero.css collapses the spacer and unsticks the stage under reduced motion,
     so there is no lift to register against and the pull-up would overlap. */
  .hold { margin-top: 0; }
  .hold__runway, .hold__expand { display: none; }
  /* The .set overrides are NOT here. They sit after the .set rules further
     down, because a media query has no extra specificity: declared up here they
     lost to the plain .set block below and never applied at all, which is how
     the reduced-motion page ended up with a pinned full-bleed film and no set
     copy for as long as it has. */
}

.set {
  position: sticky; top: 0;
  pointer-events: auto;  /* back on: .hold, its wrapper, is pointer-events none */
  /* --gone drops the paper the moment the film starts shrinking, so the work,
     which is sitting behind this section already built, is revealed by the
     shrink rather than faded in over the top of it */
  background: rgb(var(--paper-rgb) / calc(1 - var(--gone, 0)));
  color: var(--ink);
}
/* and it stops hit-testing too (flow.js toggles the class on the same frame
   as --gone): the transparent body keeps covering the top of the viewport for
   the whole landed dwell, and a transparent top layer still hit-tests, which
   blocked every click on the landed wall's arrows and HUD. Found on the
   round 8b restore, 26 August 2026. */
.set.is-gone { pointer-events: none; }
.set {
  /* min-height 100dvh is load-bearing, not styling. The hero can only finish
     lifting if there is a viewport of scrollable content under it; measured at
     1440 with a 748px set, the page ran out 67px early and the hero never
     fully cleared. It is also right compositionally: this is the section the
     lift reveals, so it should fill the screen it was revealed into. */
  min-height: 100dvh;
  display: flex; align-items: center;
  padding-block: clamp(3.5rem, 8vh, 6rem) clamp(3.5rem, 9vh, 6.5rem);
  overflow: hidden;                       /* the front frame breaks the edge */
  /* so the photographs can measure themselves against the VIEWPORT rather than
     the 1440 container. 100vw counts the scrollbar and would miss by ~7px a
     side. No fixed descendants here, so the contain: layout that comes with
     container-type costs nothing. */
  container-type: inline-size;
}

.set__inner {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--container-pad);
  display: grid;
  grid-template-columns: minmax(0, 0.80fr) minmax(0, 1.20fr);
  gap: clamp(2rem, 4vw, 4rem);
  align-items: center;
}

/* ---- copy ---- */
.set__copy h2 {
  font-family: var(--f-display);
  font-weight: 800;
  font-size: clamp(2rem, 4.1vw, 4.1rem);
  line-height: 0.99;
  letter-spacing: -0.035em;
  max-width: 13ch;
  text-wrap: balance;
}
.set__copy p {
  font-family: var(--f-prose);
  font-size: clamp(1.04rem, 1.18vw, 1.18rem);
  line-height: 1.62;
  color: var(--ink-soft);
  /* 48ch declared but 47 to 53 characters rendered, because a ch is the width
     of "0" and wider than the average letter. It was never the binding
     constraint anyway: the column is. Raised so the declared value is honest.
     The measure is a recorded deviation, see concept-brief.md. */
  max-width: 62ch;
  margin-top: clamp(1.2rem, 2.4vw, 1.8rem);
  text-wrap: pretty;
}

/* the tie back to Vidzero: the argument's last word before the producer
   answers it. Display face at body scale, full ink, so it lands as the
   point rather than another paragraph. */
.set__copy .set__tie {
  margin-top: clamp(1rem, 2vw, 1.4rem);
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1.06rem, 1.25vw, 1.25rem);
  color: var(--ink);
}

/* ---- the overlapping frames ----
   The composition used to stop at the 1440 container, so it never grew past
   1440 no matter how wide the screen: measured at 1920 the film was 26% of the
   viewport and the whole thing filled 49% of the height, marooned in the middle
   with 240px of dead margin each side. It runs to the right edge of the screen
   now. Nick, 7 August 2026: too small, make it more impressive. */
.set__stack {
  position: relative;
  aspect-ratio: 3 / 2;
  /* out of the container and onto the viewport's right edge. cqi, not vw,
     because vw counts the scrollbar and the breakout would miss by ~7px. */
  margin-right: calc(-1 * (var(--container-pad) + max(0px, (100cqi - var(--container)) / 2)));
}
.set__shot { margin: 0; position: absolute;
  transition: transform .55s var(--ease-out), filter .45s var(--ease-out), box-shadow .55s var(--ease-out); }
.set__shot img, .set__shot video {
  display: block; width: 100%; height: 100%;
  object-fit: cover;
  border-radius: var(--r-xs);
  /* offset plus soft blur, never a zero-offset halo */
  box-shadow: 0 18px 44px -12px var(--shadow-deep), 0 3px 10px -4px var(--shadow);
}

/* The finished film. In front, largest, and the only thing moving. It sits on
   the screen's right edge rather than 7% past it, because the stack now ends
   there and running it further would clip the one object the concept is about.
   Its rect is also what the expand starts from, so making it bigger shortens
   that move: the two are the same number. */
.set__shot--film {
  z-index: 3;
  width: 76%; top: 12%; right: 0;
  aspect-ratio: 16 / 9;
}
/* The sets it was made in. Behind, and still. Positions interlock deliberately
   so the three read as one composition rather than three photos near each other.
   The film is right-anchored and wide now, so it starts at 24% across: anything
   sitting further right than that is simply hidden behind it. The tall shot was
   at left 23% and came out 96% covered. */
.set__shot--b {
  z-index: 1;
  width: 50%; bottom: 0; left: 0;
  aspect-ratio: 4 / 3;
}
.set__shot--c {
  z-index: 2;
  width: 26%; top: 0; left: 4%;
  aspect-ratio: 4 / 5;
}

/* ---- the focus pull ----
   The page's hover language, first real use: what you point at resolves and
   comes forward, the rest settle back. Decorative only, nothing depends on it. */
@media (hover: hover) {
  .set__stack:hover .set__shot { filter: saturate(.68) brightness(.9); }
  .set__stack:hover .set__shot:hover {
    filter: saturate(1.06) brightness(1.02);
    transform: scale(1.028); z-index: 4;
  }
}

/* ---- parallax ----
   Scroll-driven, so no scroll listener. The front frame travels most and the
   back frame least, which is what turns the overlap from a flat collage into
   depth. Degrades to static where view() is unsupported. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .set__shot {
      animation: shot-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    .set__shot--film { --drift: 7%; }
    .set__shot--c    { --drift: 4%; }
    .set__shot--b    { --drift: 1.5%; }
  }
}
@keyframes shot-drift {
  from { translate: 0 var(--drift, 0%); }
  to   { translate: 0 calc(var(--drift, 0%) * -1); }
}

/* ---- responsive ---- */
@media (max-width: 1000px) {
  /* single column, so the stack spans the full width and the breakout is just
     the container padding */
  .set__inner { grid-template-columns: 1fr; gap: clamp(2rem, 5vw, 3rem); }
  .set__copy h2 { max-width: 16ch; }
  .set__stack { aspect-ratio: 16 / 10; margin-right: calc(-1 * var(--container-pad)); }
  .set__shot--film { width: 78%; right: 0; top: 10%; }
  .set__shot--b { width: 46%; }
  .set__shot--c { width: 24%; top: 0; left: 2%; }
}

@media (max-width: 640px) {
  /* Two frames with a REAL overlap: at 92% the film covered the photograph
     entirely (Nick's mobile pass, 28 August 2026). The film holds the top
     right, the set photograph holds the bottom left, and both read. */
  .set__stack { aspect-ratio: 4 / 3; }
  .set__shot--film { width: 74%; right: 0; top: 6%; }
  .set__shot--b { width: 52%; bottom: 0; left: -4%; }
  .set__shot--c { display: none; }
}

/* Reduced motion: the reveal primitive in base.css lands this fully visible and
   nothing here animates on its own, so there is nothing to disable. */

/* ============================================================
   WHO'S ON SET  (the guide)
   The photograph is the layout. The annotation dots sit at measured points in
   the frame and the notes sit on paper beside them, because measuring the plate
   showed every zone of it holds both near-black gear and near-white wall, so no
   text placed on the picture clears AA anywhere. The notes carry the words, the
   dots carry the link.
   ============================================================ */

.onset {
  position: relative;
  background: var(--paper);
  color: var(--ink);
  padding-block: clamp(5rem, 12vh, 9rem) clamp(6rem, 14vh, 10rem);
}
.onset__inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--container-pad);
  display: grid;
  grid-template-columns: minmax(0, 38%) minmax(0, 62%);
  grid-template-areas:
    "head  plate"
    "notes plate";
  column-gap: clamp(2rem, 4vw, 4rem);
  row-gap: clamp(2rem, 4vw, 3rem);
  align-items: start;
}

/* ---- head ---- */
.onset__head { grid-area: head; max-width: 62ch; }
.onset__head h2 {
  font-family: var(--f-display); font-weight: 800;
  font-size: clamp(2rem, 4.1vw, 4.1rem);
  line-height: 0.99; letter-spacing: -0.035em; max-width: 14ch;
}
.onset__head p {
  font-family: var(--f-prose);
  font-size: clamp(1.02rem, 1.14vw, 1.14rem);
  line-height: 1.62; color: var(--ink-soft);
  max-width: 58ch; margin-top: clamp(1.2rem, 2.2vw, 1.7rem);
  text-wrap: pretty;
}
.onset__stat { color: var(--ink); }

/* ---- the plate ---- */
.onset__plate { grid-area: plate; position: relative; margin: 0; }
.onset__plate img {
  /* The plate is lazy-loaded, and without a reserved aspect it collapses to
     0px until it arrives: measured 746x0 at 1440, which also means a layout
     shift on load. The ratio matches the 1600x1200 asset. */
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3;
  border-radius: var(--r-xs);
  box-shadow: 0 18px 44px -12px var(--shadow-deep), 0 3px 10px -4px var(--shadow);
}

/* Marker dots. The paper ring is what lets them read on any ground, which
   matters here: point a sits on a bright wall, point c on dark camera body. */
.onset__dot {
  position: absolute; z-index: 2;
  left: var(--x); top: var(--y);
  width: 20px; height: 20px; margin: -10px 0 0 -10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--paper), 0 2px 8px rgba(0,0,0,.45);
  transition: transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
/* The letter is the whole link between a dot and its note, so it shows at every
   width. A key on a diagram is a real ordered reference, not decoration. */
.onset__dot i {
  display: grid; place-items: center; width: 100%; height: 100%;
  font-family: var(--f-display); font-weight: 700; font-size: .62rem;
  font-style: normal; color: var(--paper); line-height: 1;
}
.onset__dot--a { --x: 14%; --y: 41%; }
.onset__dot--b { --x: 35%; --y: 66%; }
.onset__dot--c { --x: 52%; --y: 76%; }

/* ---- the notes ---- */
.onset__notes {
  grid-area: notes; list-style: none; margin: 0; padding: 0;
  display: grid; align-content: start;
  row-gap: clamp(1.6rem, 3vw, 2.4rem);
}
.onset__note {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  column-gap: .8rem; align-items: start;
  font-family: var(--f-prose);
  font-size: clamp(.95rem, 1.02vw, 1.02rem);
  line-height: 1.5; color: var(--ink-soft);
  transition: color var(--dur-2) var(--ease-out);
}
.onset__note b {
  font-family: var(--f-display); font-weight: 700; font-size: .82em;
  color: var(--paper); background: var(--accent);
  width: 1.55em; height: 1.55em; border-radius: 50%;
  display: grid; place-items: center; margin-top: .12em;
}

/* ---- the focus pull, same language as the set ---- */
@media (hover: hover) {
  .onset__notes:hover .onset__note { color: rgba(75, 69, 118, .45); }
  .onset__notes:hover .onset__note:hover { color: var(--ink); }
}
/* The note and its dot are in different subtrees, so a sibling combinator
   cannot reach across. :has() on the section can. */
.onset:has(.onset__note--a:hover) .onset__dot--a,
.onset:has(.onset__note--b:hover) .onset__dot--b,
.onset:has(.onset__note--c:hover) .onset__dot--c {
  transform: scale(1.55);
  box-shadow: 0 0 0 4px var(--paper), 0 3px 12px rgba(0,0,0,.5);
}

/* ---- responsive ---- */
@media (max-width: 1000px) {
  .onset__inner {
    grid-template-columns: 1fr;
    grid-template-areas: "head" "plate" "notes";
  }
  .onset__head h2 { max-width: 16ch; }
  .onset__notes { max-width: 62ch; }
}

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

/* ============================================================
   THE EXPAND: the set becomes the producer, on Nick's direction 6 Aug 2026.

   Was: the producer was its own section with its own video, which just scrolled into
   view. Now ONE film element carries both of its states. It rests in the set's
   composition, holds while the section is pinned, then expands to fill the
   screen and the producer's copy lands on it.

   The resting rect is measured off an empty slot in the stack by flow.js
   (--fx/--fy/--fr/--fb). Never hand-tune it in vw.
   ============================================================ */

/* the second runway: the film expands across this one */
/* room for both phases. Trimmed from 190dvh, Nick 7 August 2026: mostly fine,
   a tiny bit shorter. The film also rests bigger now, so it has less distance
   to cover across it. */
.hold__expand { height: 165dvh; }
/* the third: the producer holds still on the front of it and shrinks away on the back.

   A RUNWAY'S PROGRESS FINISHES ONE VIEWPORT BEFORE THE RUNWAY DOES. flow.js
   maps progress onto (vh - r.top) / r.height, which spans
   [docTop - vh, docTop - vh + height] while the runway itself occupies
   [docTop, docTop + height]. The gap is exactly one viewport, at every height,
   and .set stays pinned across it doing nothing. That is where "it feels stuck
   and I'm scrolling but nothing is happening" came from: measured 1.31v of dead
   scroll after the film stopped growing. The dwell subtracts it back off.
   Measure this from the scroll position where the FILM STOPS MOVING, never from
   a runway's edge: measuring from the edge is what hid it the first time.

   THE THREE HEIGHTS BELOW ARE A SET, do not change one alone:
     .hold__dwell        hold 30 + shrink 60 + a tail of one .set, MINUS the
                         dead viewport above
     .workhold pull-up   shrink 60 + the same tail, so the work pins at the exact
                         scroll position the shrink starts
     .workhold__runway   shrink 60 + 90 of the work landed and held (the
                         round 21 pause: 40 was short enough to scroll past
                         without registering the portfolio at all)
   A sticky section unpins one .set-height BEFORE its wrapper ends, which is
   where the tail comes from, and .set is NOT a viewport tall at every width:
   at 375 it is 1007px against an 812px screen, because the set's stacked copy
   and photographs push past min-height. So the tail is --set-h, written back
   by flow.js from the real measurement, not 100dvh. Hard-coding it left the
   film 174px wide of the monitor at 375 with the handover never firing.

   The producer carries the moving film, so the shrink has to finish before it unpins.
   That makes the shrink exactly (pull-up - --set-h) long, and flow.js derives it
   that way from the live layout rather than trusting a number here.

   Cut from 180/150/180 on 7 August 2026. Nick: the producer stuck for way too long.
   It now holds still for 30dvh rather than 59, and the whole run from the producer
   landing to the work releasing is 130dvh rather than 210. */
.hold__dwell  { height: calc(var(--set-h, 100dvh) + 90dvh - 100dvh); }

/* the slot is measurement only, never painted */
.set__shot--film[data-film-slot] { visibility: hidden; }

.set__screen {
  /* The section can be taller than the viewport at narrow widths (measured 959px
     against an 812px screen at 375). The film has to fill the SCREEN, so it and
     everything over it live in a layer that is exactly one viewport tall,
     anchored to the top of the pinned section. */
  position: absolute; inset: 0 0 auto 0; height: 100dvh;
  z-index: 3; pointer-events: none;
}

/* The box never moves. It is pinned to the measured slot, which is 16/9, and
   the source is 16/9, so object-fit never crops and the whole journey is one
   translate plus one uniform scale: resting in the set's composition, covering
   the viewport for the producer, then sitting exactly on the work's monitor.

   Transform, not inset. Animating inset is a layout change on every frame,
   which is where the page's 0.44 CLS came from, and an inset animation cannot
   land on a 16/9 monitor without the crop changing shape on the way. The radius
   and the shadow are divided by the scale so they render at a constant size
   instead of being blown up with the picture. */
.set__film {
  position: absolute; z-index: 1;
  inset: var(--fy, 8%) var(--fr, -7%) var(--fb, 38%) var(--fx, 35%);
  transform: translate(var(--film-tx, 0px), var(--film-ty, 0px)) scale(var(--film-s, 1));
  transform-origin: 50% 50%;
  will-change: transform;
  overflow: hidden;
  border-radius: calc(2px / var(--film-s, 1));
  opacity: calc(1 - var(--mh, 0));
  box-shadow:
    /* the set's elevation, on paper, gone by the time it is full bleed */
    0 calc(18px / var(--film-s, 1)) calc(44px / var(--film-s, 1)) calc(-12px / var(--film-s, 1))
      rgba(28, 20, 6, calc(.40 * (1 - var(--qb, 0)))),
    0 calc(3px / var(--film-s, 1)) calc(10px / var(--film-s, 1)) calc(-4px / var(--film-s, 1))
      rgba(46, 36, 12, calc(.22 * (1 - var(--qb, 0)))),
    /* the work's, arriving as it becomes a monitor on a set */
    0 calc(40px / var(--film-s, 1)) calc(90px / var(--film-s, 1)) calc(-24px / var(--film-s, 1))
      rgba(0, 0, 0, calc(.9 * var(--mc, 0)));
}
/* No crop to position. The box is 16/9 and so is the source, so cover fits it
   exactly at every point in the move, which is also why the handover onto the work's monitor needs no re-centring: both show the whole frame. */
.set__film video {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 50%;
}

/* PHASE A: the set's content scrolls away while the film holds its place. It
   leaves by moving, not by fading, so it reads as the page carrying on and the
   film staying behind. The film is a sibling of .set__inner, so it is not
   carried along. */
.set__inner {
  transform: translateY(calc(var(--qa, 0) * -145%));   /* 118% left a 90px sliver at 1440 */
  will-change: transform;
  /* 145% clears the viewport at 1440 and 375 but NOT at 1920 or 2000, where
     this block is short and sits low in a tall section: measured 33px and 2px
     of it still on screen. It never showed while the film was full bleed
     because the film paints over it, but the moment the film starts shrinking
     it appears above the work's heading. --gone fires on the same frame the
     shrink starts, while the film is still covering everything. */
  opacity: calc(1 - var(--gone, 0));
}

/* the scrim only earns its place once the film IS the screen, and it lifts on
   the same curve the copy leaves on, so the picture is clean by the time it is
   a monitor sitting on a set */
.set__scrim {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  opacity: calc(max(0, var(--qb, 0) - .55) * 2.2 * (1 - var(--co, 0)));
  background:
    linear-gradient(to top, rgba(6,4,15,.95) 0%, rgba(6,4,15,.90) 38%,
                            rgba(6,4,15,.62) 58%, rgba(6,4,15,.16) 80%,
                            rgba(6,4,15,0) 95%),
    linear-gradient(100deg, rgba(6,4,15,.82) 0%, rgba(6,4,15,.42) 46%,
                            rgba(6,4,15,0) 76%);
}

/* the producer's copy, landing late and fast */
.set__copy2 {
  position: absolute; z-index: 3;
  left: 0; right: 0; bottom: 0;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--container-pad);
  padding-block: 0 clamp(4rem, 12vh, 8rem);
  /* --co leaves on the same frame the film starts shrinking. Nick, 7 August:
     the copy going first and the film moving later read as two separate events
     rather than one move. */
  /* 3.7, not 3.5: 3.5 topped out at 0.98 and the copy never quite arrived */
  opacity: calc(max(0, var(--qb, 0) - .72) * 3.7 * (1 - var(--co, 0)));
  /* none while it is an invisible layer over the film (a transparent top
     layer still hit-tests), but the copy must become REAL once it has
     arrived: it was unselectable for its entire readable life, in both
     motion modes (Nick, 27 August 2026). flow.js toggles is-read. */
  pointer-events: none;
}
.set.is-read .set__copy2 { pointer-events: auto; }
.set__copy2 h2 {
  font-family: var(--f-display); font-weight: 800;
  font-size: clamp(2.1rem, 4.6vw, 4.6rem);
  line-height: 0.98; letter-spacing: -0.035em;
  color: #fff; max-width: 15ch; text-wrap: balance;
}
.set__copy2 p {
  font-family: var(--f-prose);
  font-size: clamp(1.02rem, 1.14vw, 1.14rem);
  line-height: 1.62;
  color: rgba(240, 238, 252, .92);
  max-width: 52ch;
  margin-top: clamp(1.1rem, 2.2vw, 1.6rem);
  text-wrap: pretty;
}

@media (max-width: 700px) {
  /* nothing to re-frame at any width now: a 16/9 box on a 16/9 source has no
     crop to position, at 375 or anywhere else */
  .set__copy2 h2 { max-width: 13ch; }
  .set__copy2 p { max-width: none; }
  .set__scrim {
    background: linear-gradient(to top, rgba(6,4,15,.96) 0%, rgba(6,4,15,.86) 42%,
                                        rgba(6,4,15,.32) 72%, rgba(6,4,15,0) 92%);
  }
}

/* ---- REDUCED MOTION: the set and the producer, stacked ----
   This block has to live HERE, after the rules it overrides, because a media
   query carries no extra specificity. The old override sat at the top of the
   file and lost to the plain .set rule below it, so it never applied: the
   reduced-motion page kept a pinned, full-bleed film with --qb at 1 and
   .set__inner translated away with --qa at 1, which meant "Everyone can make a
   really good video." and all three photographs were invisible to anyone who
   asks for less motion. Found 7 August 2026 by measuring that copy's contrast
   for the first time: 1.0, which is ink on a picture of itself.

   Two states of one section only works while something is driving it. With
   nothing driving it they have to be two blocks, one after the other. */
@media (prefers-reduced-motion: reduce) {
  .set { position: static; display: block; padding-bottom: 0; }
  .set__inner { transform: none; opacity: 1; }
  /* Its own dark ground, and the copy sits UNDER the film rather than on it.
     Absolutely positioned over the film it only works while the film is a whole
     viewport tall: at 375 the film is 211px and the copy is 475px, so most of
     the white type was sitting on paper and measured 1.09. */
  .set__screen {
    position: relative; inset: auto; height: auto;
    margin-top: clamp(3rem, 6vh, 5rem);
    background: var(--screen);
  }
  .set__film {
    position: relative; inset: auto;
    transform: none; width: 100%; aspect-ratio: 16 / 9;
    border-radius: 0;
    box-shadow: none;
  }
  /* the grade exists to carry the copy, and the copy is no longer on the film */
  .set__scrim { display: none; }
  .set__copy2 {
    position: relative; left: auto; right: auto; bottom: auto;
    opacity: 1;
    pointer-events: auto;  /* stacked and always readable, so always real */
    padding-block: clamp(2rem, 4vh, 3rem) clamp(2.5rem, 5vh, 4rem);
  }
  /* the slot is only a measuring device, and there is nothing to measure */
  .set__shot--film[data-film-slot] { display: none; }
}

/* ============================================================
   THE WORK  (tentpole, second scroll set-piece)

   Films on monitors along a set. Moving between them scrubs a generated
   soundstage plate by carousel position, so the travel is a camera move across
   a real-looking space rather than a panel sliding over black. That was the
   whole fault with the old build: nothing in the gap to see.

   The plate is flipped so the camera reads left to right, which is what "next"
   feels like. It is seeked, never played.
   ============================================================ */

/* ---- THE CONSTRAIN: the producer lands here ----
   The expand run backwards. This section sits BEHIND the producer, pulled up over the
   tail of .hold so it pins in the same place, and it is built and fully opaque
   from the first frame. The producer's own film is the thing that shrinks, and the set
   around it was already there: nothing in here fades into place.

   z-index 0, under .hold's 1. That layer order is the whole reason the timing
   works. With the work on top it had to be opaque before anything could move,
   which forced the producer's copy out first and the shrink second, and forced the
   set and the copy to fade in afterwards. Nick called all three, 7 August 2026.

   The pull-up is part of the set of three heights documented on .hold__dwell. */
.workhold { position: relative; z-index: 0; margin-top: calc(-60dvh - var(--set-h, 100dvh)); }
/* 150, not 100: the extra 50 is all landed hold. MC_END derives from the
   layout, so the shrink keeps the same scroll length and the wall then sits
   still and armed for ~90dvh of scroll before the pull begins. That
   stillness is the forced pause on the portfolio (Nick, round 21). */
.workhold__runway { height: 150dvh; }

.work {
  --mon-dur: 1250ms;
  position: sticky; top: 0;
  /* exact, not min-height: a pinned section that is taller than the viewport
     hides its own bottom, and the HUD is at the bottom */
  height: 100dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  background: var(--screen);
  overflow: hidden;
  isolation: isolate;
  /* the producer is on top and hit-tests over this the whole time it is pinned, but
     the arrow-key gate in work.js keys off is-armed, so it is set either way */
  pointer-events: none;
}
.work.is-armed { pointer-events: auto; }

/* The blur filter's <defs>. A 0x0 svg, but still an in-flow grid item, so it
   was silently eating the first row and pushing the head into the 1fr track. */
.work__defs { position: absolute; }

/* ---- the set we travel across ---- */
.work__plate {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* the plate is 720p behind a full-bleed section, so it is deliberately
     pushed back: darker, softer, and never competing with the monitor */
  filter: brightness(.62) saturate(.9);
}
/* Sits BEHIND the monitor (z-index 1 against the stage's 2), so it grades the
   set without touching the picture. The top band is what the argument sits on:
   the plate's upper third is lit wall and truss, and the line measured 2.75
   against it at 1440. */
.work__grade {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 45%, rgba(6,4,15,0) 30%, rgba(6,4,15,.72) 100%),
    linear-gradient(to bottom, rgba(6,4,15,.90) 0%, rgba(6,4,15,.72) 22%, rgba(6,4,15,0) 44%),
    linear-gradient(to top, rgba(6,4,15,.92) 0%, rgba(6,4,15,.35) 34%, rgba(6,4,15,0) 62%);
}

/* ---- the argument ----
   The producer ends on getting someone comfortable enough to say what they actually
   mean. This is the evidence for that and it has to say so, or the page goes
   from an argument straight into a control panel. No fade: it is already here,
   behind the producer's film, and the shrink uncovers it. */
/* the exit dim: flow.js writes --wexit as the count's paper arrives, so the
   outgoing HUD and monitor fall quiet instead of hanging lit over the paper */
.work__head, .work__stage, .work__hud { opacity: calc(1 - var(--wexit, 0) * .75); }

.work__head {
  position: relative; z-index: 3;
  width: 100%; max-width: var(--container); margin-inline: auto;
  /* Every pixel of chrome here comes straight off the monitor, which is height
     bound. Nick, 7 August 2026: the video is too small. But the floor is not
     negotiable: the fixed brand mark runs to y=51 at desktop, and trimming this
     to 5vh put the heading's box through it at 1440, 1366 and 1024. The saving
     comes from the heading fitting on one line instead, which is worth more. */
  padding: clamp(3.8rem, 6.2vh, 4.6rem) var(--container-pad) 0;
}
.work__head h2 {
  font-family: var(--f-display); font-weight: 800;
  font-size: clamp(1.9rem, 3.3vw, 3.4rem);
  line-height: 1.0; letter-spacing: -0.035em;
  /* 30ch, not 24: the heading is 35 characters, and 24ch forced a second line
     at every desktop width for no reason. One line is 48px of monitor back. */
  color: #fff; max-width: 30ch; text-wrap: balance;
}
.work__head p {
  font-family: var(--f-prose);
  font-size: clamp(.98rem, 1.08vw, 1.08rem);
  line-height: 1.55;
  color: rgba(240, 238, 252, .82);
  /* 58, not the 74 the measure rule reads like: Schibsted Grotesk's zero is
     narrow, so 74ch renders about 95 characters a line, well over the band */
  max-width: 58ch;
  margin-top: clamp(.7rem, 1.4vh, 1rem);
  text-wrap: pretty;
}

/* ---- the monitor ---- */
.work__stage {
  position: relative; z-index: 2;
  display: grid; place-items: center;
  padding: clamp(.8rem, 2vh, 1.4rem) var(--container-pad) 0;
  min-width: 0; min-height: 0;
  /* the frame is sized off the row the grid gives it, not off vh. Sizing it in
     vh meant the head and the HUD had to be budgeted for by hand at every
     width, and 1366x768 overflowed. No fixed descendants here, so contain:
     layout coming along with container-type costs nothing. */
  container-type: size;
}
/* Static. It used to carry the constrain transform; the film does that now, and
   this is just the target it lands on. flow.js measures it to build the film's
   final scale and translate, so it must stay a plain untransformed box or the
   measurement reads its own animation back. */
.work__frame {
  /* cap raised from 1180: at 1920 the monitor was 52% of the screen, which is
     the wrong way round, since that is where there is the most room */
  width: min(100cqw, calc(100cqh * 16 / 9), 1560px);
  aspect-ratio: 16 / 9;
}
.work__rail { list-style: none; margin: 0; padding: 0; display: grid; width: 100%; height: 100%; }
.work__mon {
  grid-area: 1 / 1;
  width: 100%; height: 100%;
  border-radius: var(--r-xs);
  overflow: hidden;
  background: #05040d;
  /* elevation declared once: the shadow. The 1px ring that used to sit
     alongside it was a second declaration of the same thing. */
  box-shadow: 0 40px 90px -24px rgba(0,0,0,.9);
  opacity: 0; pointer-events: none;
}
.work__mon.is-live { opacity: 1; pointer-events: auto; }
.work__mon video { display: block; width: 100%; height: 100%; object-fit: cover; }

/* ---- THE CAMERA MOVE ----
   Not a cross-fade. Three moves, the way the original build did it and the way
   you actually move between monitors on a set:
     pull back   the monitor shrinks and the set opens up around it
     travel      it sweeps out of frame while the plate scrubs across the set
     push in     the next one arrives and grows back to fill the screen
   The content swap happens inside the travel while opacity is 0, so you never
   see it change. That was the fault with the cross-fade version: no continuity,
   so it read as jumping a frame. */
.work__rail { will-change: transform, opacity, filter; }
/* Horizontal-only blur. CSS blur() is isotropic and cannot streak sideways, so
   the sweep needs an SVG feGaussianBlur with a zero vertical deviation. */
.work.is-moving .work__rail { filter: url(#mon-mblur); }

.work.is-moving .work__plate { filter: brightness(.95) saturate(1.05); }

.work.is-next .work__rail  { animation: mon-next var(--mon-dur) cubic-bezier(.62,.02,.32,1) both; }
.work.is-prev .work__rail  { animation: mon-prev var(--mon-dur) cubic-bezier(.62,.02,.32,1) both; }

@keyframes mon-next {
  0%   { transform: scale(1)   translateX(0);     opacity: 1; }
  26%  { transform: scale(.30) translateX(0);     opacity: 1; }
  47%  { transform: scale(.30) translateX(-150%); opacity: 1; }
  50%  { transform: scale(.30) translateX(-170%); opacity: 0; }
  53%  { transform: scale(.30) translateX(170%);  opacity: 0; }
  56%  { transform: scale(.30) translateX(150%);  opacity: 1; }
  74%  { transform: scale(.30) translateX(0);     opacity: 1; }
  100% { transform: scale(1)   translateX(0);     opacity: 1; }
}
@keyframes mon-prev {
  0%   { transform: scale(1)   translateX(0);     opacity: 1; }
  26%  { transform: scale(.30) translateX(0);     opacity: 1; }
  47%  { transform: scale(.30) translateX(150%);  opacity: 1; }
  50%  { transform: scale(.30) translateX(170%);  opacity: 0; }
  53%  { transform: scale(.30) translateX(-170%); opacity: 0; }
  56%  { transform: scale(.30) translateX(-150%); opacity: 1; }
  74%  { transform: scale(.30) translateX(0);     opacity: 1; }
  100% { transform: scale(1)   translateX(0);     opacity: 1; }
}

/* ---- hud ---- */
.work__hud {
  position: relative; z-index: 3;
  max-width: var(--container); width: 100%;
  margin-inline: auto;
  padding: clamp(.8rem, 1.8vh, 1.2rem) var(--container-pad) clamp(1.4rem, 3.2vh, 2.2rem);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "meta watch" "dots nav";
  gap: clamp(.5rem, 1.2vh, .85rem) 1.5rem;
  align-items: end;
}
.work__meta { grid-area: meta; min-width: 0; }
/* machine data, so Azeret Mono per the type decision of 30 July. Space Mono and
   wide-tracked micro-labels are banned; this was both. */
.work__idx {
  font-family: var(--f-machine); font-size: .72rem; letter-spacing: .05em;
  color: var(--on-dark-soft);
  /* the index changes under a scramble, so the digits have to hold their
     column or the slate twitches on every advance */
  font-variant-numeric: tabular-nums;
}
.work__idx b { color: var(--accent-on-dark); font-weight: 400; }
/* a slate under the picture, not the section's heading. It was set at 3.2rem
   as an h2, which is why the section read as a widget with a big film name and
   no argument. */
.work__name {
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1.25rem, 1.7vw, 1.7rem);
  line-height: 1.06; letter-spacing: -.025em; color: #fff;
  margin-top: .3rem;
}
.work__cat {
  display: block; margin-top: .2rem;
  font-family: var(--f-prose); font-size: .93rem; color: rgba(240,238,252,.72);
}

/* ---- the focus pull, the set's hover language carried onto the controls ----
   What you point at resolves and comes forward; its neighbours settle back. The
   lift-plus-shadow and the plain background swap that used to be here are both
   on the named list of library defaults that fail. Still owed: Nick confirming
   the focus pull as the page-level language, per concept-brief.md. */
.work__watch {
  grid-area: watch; align-self: end;
  font-family: var(--f-display); font-weight: 700; font-size: .95rem;
  color: var(--screen); background: rgba(236, 230, 215, .82);
  border: 0; border-radius: var(--r-pill);
  padding: .72em 1.5em; cursor: pointer;
  letter-spacing: 0;
  transition: background var(--dur-1) var(--ease-out),
              letter-spacing var(--dur-1) var(--ease-out),
              box-shadow var(--dur-1) var(--ease-out);
}
/* no halo: a zero-offset glow is decoration, not elevation. The paper resolves
   from soft to solid and the label sharpens, which is the focus pull. */
.work__watch:hover, .work__watch:focus-visible {
  background: var(--paper);
  letter-spacing: .012em;
}

.work__nav { grid-area: nav; display: flex; gap: .6rem; justify-self: end; }
.work__arrow {
  width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center;
  background: transparent; color: rgba(239,234,250,.62);
  border: 1px solid rgba(239,234,250,.22);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.work__arrow svg {
  width: 21px; height: 21px;
  fill: none; stroke: currentColor; stroke-width: 1.75;
  stroke-linecap: round; stroke-linejoin: round;
}
/* the pair settles back so the one you point at is the one in focus */
@media (hover: hover) {
  .work__nav:hover .work__arrow { color: rgba(239,234,250,.34); border-color: rgba(239,234,250,.12); }
}
.work__nav .work__arrow:hover, .work__arrow:focus-visible {
  color: #fff; border-color: rgba(239,234,250,.75);
}

/* Hit areas, not dashes. The dash is 26x3, which is nowhere near a touch
   target, so the button is 44x44 with the dash drawn inside it. Nine of those
   plus the 8px gaps needs 460px, which does not exist at 375, so below 561
   work.js builds them as spans and the arrows carry navigation. */
.work__dots { grid-area: dots; list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 8px; }
.work__dots li { display: flex; }
.work__dot {
  width: 44px; height: 44px; border: 0; padding: 0; margin: 0; background: none;
  display: grid; place-items: center;
  cursor: pointer;
}
.work__dot::before {
  content: ""; display: block; width: 26px; height: 3px; border-radius: 2px;
  background: rgba(239,234,250,.26);
  transition: background var(--dur-1) var(--ease-out), width var(--dur-1) var(--ease-out);
}
.work__dot.is-on::before { background: var(--accent); width: 34px; }
.work__dot:hover::before, .work__dot:focus-visible::before { background: rgba(239,234,250,.7); }
/* presentation only below 561: no sub-44 control, so nothing to mis-tap */
.work__dots[data-static] { gap: .45rem; }
.work__dots[data-static] .work__dot { width: 26px; height: 12px; cursor: default; }

@media (max-width: 860px) {
  /* the watch button and the arrows sit on one row rather than stacking into
     four, because four rows of chrome leaves the monitor nothing */
  .work__hud {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "meta meta" "watch nav" "dots dots";
    justify-items: start;
    row-gap: clamp(.6rem, 1.4vh, .9rem);
  }
  .work__nav { justify-self: end; }
  .work__head { padding-top: clamp(2.6rem, 5.5vh, 4rem); }
  .work__head p { max-width: none; }
}

@media (max-width: 560px) {
  /* clears the fixed menu button, which sits to y=49 at 375 and was catching
     the top-right corner of the heading's box by 5px */
  .work__head { padding-top: clamp(4rem, 8vh, 5rem); }
  .work__head h2 { max-width: 18ch; }
  .work__head p { font-size: .95rem; line-height: 1.5; }
  .work__arrow { width: 44px; height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .work { --mon-dur: 1ms; }
  .work.is-next .work__rail, .work.is-prev .work__rail { animation: none; }
  /* no scroll driving, so the join cannot run. The work becomes a plain section
     sitting under the producer at its landed state. */
  .workhold { margin-top: 0; }
  .workhold__runway, .workhold__pull, .hold__dwell { display: none; }
  .work { position: static; }
}
