/* ============================================================
   hal whitman, site implementation v2.0
   Strict token mode. Every value resolves to a token in
   tokens.css. No raw hex. Film frames stay at --radius-frame.
   ============================================================ */

*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:var(--s-7)}
body{
  margin:0;background:var(--paper);color:var(--ink-2);
  font-family:var(--mono);font-weight:var(--w-light);
  font-size:var(--t-body);line-height:var(--lh-body);
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
}
h1,h2,h3{font-weight:var(--w-thin);color:var(--ink);margin:0}
p{margin:0 0 var(--s-4)}
img{display:block;max-width:100%;height:auto}
::selection{background:var(--i);color:var(--paper)}
:focus-visible{outline:2px solid var(--rim);outline-offset:3px}

.skip{position:absolute;left:var(--s-3);top:calc(var(--s-3) * -4);
  background:var(--ink);color:var(--paper);padding:var(--s-3);
  font-size:var(--t-meta);letter-spacing:var(--tr-meta);z-index:100;
  transition:top var(--dur-1) var(--ease-out)}
.skip:focus{top:var(--s-3)}

/* ============================================================
   RAIL, the navigation header. Off screen until the reader
   starts moving; nothing stands in the corner waiting to be
   found.
   ============================================================ */

/* The nav is a header now, not a column: one row across the top, at
   every width. It overlays rather than occupies, so nothing below it
   has to reserve space for a bar that is usually not there. */
.rail{
  position:fixed;top:0;left:0;right:0;
  display:flex;flex-direction:row;align-items:center;
  flex-wrap:wrap;gap:var(--s-5);
  padding:var(--s-3) var(--page-margin);
  background:var(--paper);z-index:50;
  border-bottom:var(--hair) solid var(--rule);
}
.rail-mark{display:block;width:var(--s-4);height:var(--s-4)}
.rail-mark rect{fill:none;stroke:var(--ink-3);stroke-width:1}
.rail-mark line{stroke:var(--i);stroke-width:2;transition:stroke var(--dur-1) var(--ease-out)}
.rail-id{border:0;display:flex;align-items:center;gap:var(--s-3);text-decoration:none}
.rail-word{display:block;
  font-size:var(--t-meta);letter-spacing:var(--tr-meta);color:var(--ink);
  line-height:var(--lh-meta)}

.rail-nav{position:relative}
.rail-nav ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:row;gap:var(--s-5)}
/* the gauge reads across the header rather than down a column, and
   loses the rule that separated it from the rail's stacked blocks */
.rail .gauge{display:flex;gap:var(--s-4);border-top:0;padding-top:0;
  margin-left:auto}
/* the tick marked position in a vertical list; a row does not have one */
.rail-tick{display:none}
.rail-nav a{font-size:var(--t-meta);letter-spacing:var(--tr-meta);
  color:var(--ink-2);text-decoration:none;border:0;
  display:flex;align-items:center;min-height:var(--target-min);
  transition:color var(--dur-1) var(--ease-out)}
.rail-nav a:hover{color:var(--i)}
.rail-nav a[aria-current="page"]{color:var(--ink)}
.rail-nav li:nth-child(4n+1){--i:var(--i-1)}
.rail-nav li:nth-child(4n+2){--i:var(--i-2)}
.rail-nav li:nth-child(4n+3){--i:var(--i-3)}
.rail-nav li:nth-child(4n+4){--i:var(--i-4)}

.rail-tick{position:absolute;left:calc(var(--s-4) * -1);top:0;
  width:var(--s-3);height:2px;background:var(--i);opacity:0;
  transition:transform var(--dur-2) var(--ease-out),opacity var(--dur-1) var(--ease-out)}
.rail-tick.on{opacity:1}

/* While the viewport is still on the hero, the rail becomes a
   scrim over the image instead of a white panel beside it, and its
   type switches to the lit set. Everything stays readable because
   the scrim was measured against the brightest part of the frame. */
/* --rail-scrim is a left-to-right gradient built for a vertical rail.
   A header spans the frame, so it takes the solid --on-image ground
   instead: same 78% coal, measured to hold over the brightest frame. */
html.at-hero .rail{background:var(--on-image);border-bottom-color:transparent}
html.at-hero .rail-word,
html.at-hero .rail-nav a[aria-current="page"]{color:var(--lit)}
/* --lit, not --lit-2. Measured against the --on-image plate composited
   over the brightest region of the closing frame: --lit holds 6.61:1
   there, --lit-2 only 2.97:1, which is why the header could not be
   read over the film. --lit-2 stays a dimming for marks, not for type
   that has to survive the worst frame under it. */
html.at-hero .rail-nav a,
html.at-hero .svc-toggle,
html.at-hero .gauge{color:var(--lit)}
html.at-hero .gauge{border-top-color:transparent}
html.at-hero .gauge b{color:var(--lit)}
html.at-hero .gauge var{color:var(--lit)}
html.at-hero .rail-mark rect{stroke:var(--lit-2)}
html.at-hero .svc-toggle .box{border-color:var(--lit-2)}
.rail,.rail-word,.rail-nav a,.svc-toggle,.gauge{
  transition:background var(--dur-2) var(--ease-out),color var(--dur-2) var(--ease-out)}

/* The header stands on every page. It is fixed, so it costs no space,
   and it is the only navigation there is: nothing to roll into, nothing
   to scroll for, nothing to find. The single exception is below. */

/* The title sequence owns the screen until the film comes to rest, so
   the header is held back and arrives with the closing frame. Scoped
   to .has-build, which only the index carries, and to .js, so a reader
   without scripting or with reduced motion gets the navigation
   immediately and unconditionally. Same specificity as the open rules
   above and deliberately placed after them, so scrolling during the
   sequence cannot prise the nav open early. Keep these below;
   reordering re-opens that hole. */
html.js.has-build:not(.is-final) .rail{
  opacity:0;visibility:hidden;
  transition:opacity var(--dur-2) var(--ease-out),visibility 0s var(--dur-2);
}
/* Once the film rests, the header comes out and STAYS out. It is the
   end of the sequence, so the page stops withholding. Everywhere else
   the header waits for a scroll, which is why this is .has-build only.
   --dur-3, not the build's --seq-* tokens: those live on .build and the
   rail is not inside it, so they would not resolve here. */
html.js.has-build.is-final .rail{
  opacity:1;visibility:visible;
  transition:opacity var(--dur-3) var(--ease-out);
}

/* No left offset any more. The rail used to hold this column open at
   its own width; now it is a panel that overlays on demand, so nothing
   lives in that space and the content starts at the page margin like
   everything else. The RIGHT gutter stays: the exposed-service
   callouts are really in it. */
.shell{min-width:0}
@media (min-width:1024px){
  .shell{padding-right:calc(var(--svc-gutter) * 1.75)}
}

/* the header is a row at every width; narrow screens only tighten it
   and drop the diagnostics, which have nowhere to sit on a phone */
@media (max-width:1023px){
  .rail{gap:var(--s-4)}
  .rail-nav ul{gap:var(--s-4)}
  .rail-nav a{font-size:var(--t-micro)}
  .rail .gauge{display:none}
}
@media (max-width:479px){
  .rail{gap:var(--s-3)}
  .rail-nav ul{gap:var(--s-3)}
  .rail-word{display:none}   /* the mark alone carries it at this size */
}

/* ============================================================
   LAYOUT. Gutter model from the reference.
   ============================================================ */
.wrap{max-width:var(--container);padding-inline:var(--page-margin);margin-inline:0}
@media (min-width:1440px){.wrap{max-width:var(--container-wide)}}

.axis{position:relative;padding-left:var(--s-5)}
.axis::before{content:"";position:absolute;left:0;top:0;
  width:var(--hair);height:100%;background:var(--rule)}
@media (max-width:479px){.axis{padding-left:var(--s-4)}}

section{padding-block:var(--s-8)}
@media (max-width:767px){section{padding-block:var(--s-7)}
  /* The header is fixed and overlays, so the first thing on a page has
     to clear it. --s-7 is 64 against a 76px bar, which put the opening
     kicker underneath it on phones; the first section keeps the full
     --s-8 rhythm, which is what every width above 767 already had.
     Hero pages open on a .plate or .hero and are meant to run under
     the bar, so this names .wrap only. */
  main > section.wrap:first-child{padding-top:var(--s-8)}}

/* the reference's column model is 1 / 2 / 4 / 6, never 12.
   It is expressed through .split, .lay-duo, .lay-trio and .lay-offset. */
/* The other half of the same rule: a grid ITEM also defaults to
   min-width:auto, so it can grow past a track that was told it may
   shrink. The pair is what lets .scroll actually scroll its table
   instead of the table widening the page. */
.split > *,.g2 > *,.g4 > *,.g6 > *,.frames > *,.still-grid > *,
.lay-duo > *,.lay-trio > *,.lay-offset > *{min-width:0}

/* Every track here is minmax(0,1fr), never a bare 1fr. A grid child
   defaults to min-width:auto, so anything it cannot break, a typed
   nowrap line, a table, a long address, becomes the track's minimum
   and pushes the column wider than the page. minmax(0,...) lets the
   track shrink and the child clip or wrap instead, which is what put
   a horizontal scrollbar on phones at 320. The desktop .split already
   did this; the rest did not. */
.split{display:grid;gap:var(--s-5);grid-template-columns:minmax(0,1fr)}
@media (min-width:1024px){.split{grid-template-columns:minmax(0,2fr) minmax(0,6fr);gap:var(--s-6)}}

/* type roles */
/* The page headline, set the way the film page sets the film: display
   size at the film title's own weight. --w-bold is what the title
   carries wherever it appears as text, so a section now opens on the
   same note rather than a lighter one. The film page is unchanged by
   this: its h1 already holds a .film-title at the same 700. */
/* The headline is sized to the column it actually sits in, not to a
   share of the window. .axis is the column, so 100cqi IS the room, and
   the browser measures it rather than the stylesheet guessing at page
   margins and gutters that change with the breakpoint. This face sets
   at 0.66em per character and display tracking adds 0.16em, measured
   off the render, not assumed: 0.82em each. The size at which the
   longest word exactly fills the column is 100cqi / (chars * 0.82),
   and --chars is that word, not the whole line, because spaces are
   where a headline is allowed to break. min() takes that only when it is
   the smaller of the two, so a headline is the size the page asked for
   until its own column cannot hold it, which on this site is the word
   "screenings" on a phone. --chars is stamped by the builder. */
.axis{container-type:inline-size}
.t-display{
  font-size:min(var(--t-display), calc(100cqi / (var(--chars,8) * 0.82)));
  font-weight:var(--w-bold);
  letter-spacing:var(--tr-display);line-height:var(--lh-display);color:var(--ink)}
.t-title{font-size:var(--t-title);font-weight:var(--w-thin);
  letter-spacing:var(--tr-title);line-height:var(--lh-title);color:var(--ink)}
.t-lead{font-size:var(--t-lead);font-weight:var(--w-light);
  line-height:var(--lh-head);letter-spacing:var(--tr-tight);
  max-width:var(--measure-mono);color:var(--ink-2)}
.t-micro{font-size:var(--t-micro);letter-spacing:var(--tr-meta);color:var(--ink-2)}
.prose{font-family:var(--prose);font-weight:var(--w-reg);font-size:var(--t-body);
  line-height:1.75;letter-spacing:var(--tr-body);max-width:var(--measure-prose)}
.ph{color:var(--ink-2);border-bottom:var(--hair) dotted var(--ink-3)}

.mark{display:block;margin-bottom:var(--s-4);
  font-size:var(--t-micro);letter-spacing:var(--tr-meta)}
.mark .slash{color:var(--i);margin-right:var(--s-2)}
.mark .label{color:var(--ink)}

/* ============================================================
   EXPOSED SERVICES, M-23.
   The Centre Pompidou puts its ducts, lifts and structure on the
   outside and colour-codes them so the building explains itself
   from the street. This site does the same with its own
   construction: the type, space, motion, colour and markup that
   build each element are drawn on the outside, in the margin, in
   the service colour. It is the film's subject applied to its own
   container. The interior is on the exterior. Nothing is hidden.
   ============================================================ */
.exposed{position:relative}

.callout{
  font-family:var(--mono);font-size:var(--t-nano);
  letter-spacing:var(--tr-meta);line-height:1.5;
  color:var(--svc);border-left:2px solid var(--svc);
  padding-left:var(--s-2);margin-block:var(--s-2);
  max-width:var(--callout-w);
}
.callout b{font-weight:var(--w-reg);display:block;margin-bottom:2px}
.callout code{font-family:var(--mono);font-size:var(--t-nano);
  color:var(--ink-2);display:block;white-space:pre-wrap;word-break:break-word}

.callout[data-svc="type"]     {--svc:var(--svc-type)}
.callout[data-svc="space"]    {--svc:var(--svc-space)}
.callout[data-svc="motion"]   {--svc:var(--svc-motion)}
.callout[data-svc="color"]    {--svc:var(--svc-color)}
.callout[data-svc="structure"]{--svc:var(--svc-structure)}

/* On wide screens the callouts move OUT of the content and into
   the right margin, joined to their element by a leader line.
   Below 1440 they stay inline: still exposed, just stacked. */
@media (min-width:1440px){
  .exposed > .callout{
    position:absolute;left:calc(100% + var(--s-6));width:var(--callout-w);
    margin:0;
  }
  .exposed > .callout:nth-of-type(2){top:var(--s-7)}
  .exposed > .callout:nth-of-type(3){top:calc(var(--s-7) * 2)}
  .exposed > .callout:nth-of-type(4){top:calc(var(--s-7) * 3)}
}

/* ============================================================
   SERVICE RUNS, M-25.
   Every callout is physically connected to the thing it describes
   by an orthogonal run: out of the annotation, across the void,
   and into the element. Right angles, not curves, because this is
   conduit. Paths are MEASURED from both boxes at runtime, so a run
   always lands on its target. Solid, not dashed: the dashed pattern
   competed with the photography and was removed.
   ============================================================ */
.runs{position:fixed;inset:0;pointer-events:none;z-index:0;
  overflow:visible}
html.svc-off .runs{display:none}
.runs path{fill:none;stroke:var(--svc);stroke-width:1;
  vector-effect:non-scaling-stroke}
.runs rect{fill:var(--paper);stroke:var(--svc);stroke-width:1}
.runs [data-svc="type"]     {--svc:var(--svc-type)}
.runs [data-svc="space"]    {--svc:var(--svc-space)}
.runs [data-svc="motion"]   {--svc:var(--svc-motion)}
.runs [data-svc="color"]    {--svc:var(--svc-color)}
.runs [data-svc="structure"]{--svc:var(--svc-structure)}

@media (prefers-reduced-motion: no-preference){
  /* the run draws itself from the annotation toward the element,
     then the terminal lands. It reads as a connection being made. */
  .js .runs path{stroke-dasharray:var(--len);stroke-dashoffset:var(--len);
    animation:route 620ms var(--ease-out) forwards}
  .js .runs rect{opacity:0;animation:fadeIn 240ms var(--ease-out) forwards 520ms}
  @keyframes route{to{stroke-dashoffset:0}}
}
@media (prefers-reduced-motion: reduce){
  .runs path{stroke-dasharray:none;stroke-dashoffset:0}
  .runs rect{opacity:1}
}

/* the toggle in the rail */
.svc-toggle{display:flex;align-items:center;gap:var(--s-2);
  font-size:var(--t-micro);letter-spacing:var(--tr-meta);
  color:var(--ink-2);background:none;border:0;padding:var(--s-2) 0;
  min-height:var(--target-min);cursor:pointer;text-align:left;
  font-family:var(--mono)}
.svc-toggle:hover{color:var(--ink)}
.svc-toggle .box{width:10px;height:10px;border:var(--hair) solid var(--ink-3);
  flex:0 0 auto;transition:background var(--dur-1) var(--ease-out)}
.svc-toggle[aria-pressed="true"] .box{background:var(--svc-structure);
  border-color:var(--svc-structure)}

/* structure hidden: the site closes up. An option, not the
   default. The default state of this site is exposed. */
html.svc-off .callout,html.svc-off .legend{display:none}

/* the service legend, the Pompidou key */
.legend{display:flex;flex-wrap:wrap;gap:var(--s-4);
  padding:var(--s-3) 0;border-top:var(--hair) solid var(--rule);
  border-bottom:var(--hair) solid var(--rule);
  font-size:var(--t-nano);letter-spacing:var(--tr-meta);
  list-style:none;margin:0}
.legend li{display:inline-flex;align-items:center;gap:var(--s-2);color:var(--ink-2)}
.legend i{width:var(--s-3);height:2px;background:var(--k);display:block}
.legend li:nth-child(1){--k:var(--svc-type)}
.legend li:nth-child(2){--k:var(--svc-space)}
.legend li:nth-child(3){--k:var(--svc-motion)}
.legend li:nth-child(4){--k:var(--svc-color)}
.legend li:nth-child(5){--k:var(--svc-structure)}

/* ============================================================
   FULL BLEED / INNER, the reference's width model.
   --page-margin is the frame; these two step outside and inside it.
   ============================================================ */
.bleed{width:var(--full-bleed);margin-inline:calc(var(--page-margin) * -1);max-width:none}
.inner{width:var(--inner);margin-inline:auto}

/* ============================================================
   COLUMN UTILITIES, the reference's 1 / 2 / 4 / 6 model.
   Declared as real utilities so the grid is usable in markup,
   not just implied by the component layouts.
   ============================================================ */
.g2{display:grid;gap:var(--gutter);grid-template-columns:repeat(2,minmax(0,1fr))}
.g4{display:grid;gap:var(--gutter);grid-template-columns:repeat(4,minmax(0,1fr))}
.g6{display:grid;gap:var(--gutter);grid-template-columns:repeat(6,minmax(0,1fr))}
@media (max-width:1023px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}
  .g6{grid-template-columns:repeat(3,1fr)}}
@media (max-width:767px){.g2,.g4,.g6{grid-template-columns:minmax(0,1fr)}}

/* ============================================================
   PER-FILM HUE. The reference gives every project its own colour
   and lets the ground breathe with it. Here the hue comes from the
   film's own photography, and the pulse is the same 8s as the
   exposed services: the building and the film breathe together.
   ============================================================ */
.film-ground{
  background:hsl(var(--film-hue) var(--film-sat) var(--film-lum));
}
@media (prefers-reduced-motion: no-preference){
  .js .film-ground{animation:groundPulse var(--dur-pulse) infinite ease}
  @keyframes groundPulse{
    0%,100%{background:hsl(var(--film-hue) var(--film-sat) var(--film-lum))}
    50%{background:hsl(var(--film-hue) var(--film-sat) var(--film-lum-pulse))}
  }
}

/* The writing tokens live at the root, not on .build. Plates type too,
   and a var() that fails to resolve does not fall back: it voids the
   whole animation shorthand, silently, so the line never appears. */
:root{
  --type-dur:   1368ms;  /* per line, whatever its length          */
  --caret-rate: 1060ms;  /* off the round second, so it reads live */
  --caret-w:    1ch;     /* a block cursor, one character wide     */
}

/* ============================================================
   PLATE. A still with its words placed into its own light.
   The cell and the colour set are not chosen by eye: they come
   from scripts/lumen/measure.py, which grids each frame and
   tests the WORST pixel of every cell against both sets. The
   reading is printed beside the type, the way this site prints
   the rest of its construction.
   Nothing is scrimmed. If a frame cannot carry type anywhere,
   that is the measurement telling you to use another frame.
   ============================================================ */
/* a frame owns the full width, like the hero: it negates the shell's
   right gutter exactly, which is the only offset the shell still has */
main > .plate,main > .frames,main > .sheet{margin-inline:0}
@media (min-width:1024px){
  main > .plate,main > .frames,main > .sheet{
    margin-right:calc(var(--svc-gutter) * -1.75)}
}
.plate{position:relative;height:100svh;min-height:520px;overflow:hidden;
  background:var(--coal)}
.plate img{width:100%;height:100%;object-fit:cover;display:block}
.plate__type{position:absolute;inset:0;display:grid;
  grid-template-columns:repeat(6,1fr);grid-template-rows:repeat(4,1fr);
  padding:var(--s-5);pointer-events:none}
.plate__zone{grid-column:var(--col);grid-row:var(--row);align-self:center;
  display:flex;flex-direction:column;gap:var(--s-3)}
.plate__zone > *{pointer-events:auto}
.set-lit{color:var(--lit)}
.set-ink{color:var(--ink)}
/* .type sets the small register for every written line and is declared
   after the type roles, so a plate's title has to take its size back */
.plate .t-title{font-size:var(--t-title);font-weight:var(--w-thin);
  line-height:1.1}
.plate .reading{white-space:nowrap}
.reading{font-size:var(--t-nano);letter-spacing:var(--tr-meta);opacity:.72;margin:0}

/* two frames side by side, parted by a hairline and nothing else */
.frames{display:grid;gap:var(--hair);background:var(--rule)}
@media (min-width:768px){.frames{grid-template-columns:repeat(2,minmax(0,1fr))}}
.frames .plate{height:70svh;min-height:380px}

/* a page of words in the same register as the frames */
.sheet{background:var(--coal);color:var(--lit);
  padding:var(--s-8) var(--page-margin)}
.sheet .col{max-width:var(--measure-mono);display:flex;
  flex-direction:column;gap:var(--s-5)}
.sheet h2{font-size:var(--t-meta);font-weight:var(--w-reg);
  letter-spacing:var(--tr-meta);color:var(--lit-2);margin:0}
.sheet p{margin:0;color:var(--lit)}
.sheet .ph{color:var(--lit-2);border-bottom-color:var(--lit-2)}
.sheet a{color:var(--lit)}
.sheet .note{color:var(--lit-2);font-size:var(--t-nano);
  letter-spacing:var(--tr-meta)}

/* ============================================================
   HERO
   ============================================================ */
.hero{position:relative}
.video-hero{position:relative;width:100%;aspect-ratio:1.896/1;
  overflow:hidden;background:var(--surface);border-radius:var(--radius-frame)}

/* full-bleed, full-height hero: edge to edge, top to fold. The frame
   sits behind it, so the image is never competed with. */
/* The hero is the only element that leaves the building envelope
   entirely: it spans the full viewport, edge to edge and top to
   fold. The shell's offsets are negated exactly, so no 100vw and
   therefore no scrollbar overflow. */
/* The shell starts at the viewport edge now, so the hero has nothing to
   pull back from on the left: a negative margin here would simply crop
   32px off the frame. Only the right gutter is still negated, because
   the shell really does reserve that for the callouts. */
.hero--full{margin-top:0;margin-inline:0}
@media (min-width:1024px){
  .hero--full{
    margin-right:calc(var(--svc-gutter) * -1.75);
  }
}
.hero--full .video-hero{
  aspect-ratio:auto;
  height:100vh;
  height:100svh;
  min-height:520px;
  width:100%;
}
/* nothing to clear any more: the rail overlays rather than occupies */
@media (min-width:1024px){
  .hero--full .hero-block{max-width:none}
}
.hero--full .video-hero img,.hero--full .video-hero video{object-position:center}
@media (max-width:767px){
  .hero--full .video-hero{height:82svh;min-height:420px}
}
.video-hero video,.video-hero img{width:100%;height:100%;object-fit:cover;display:block}

/* ============================================================
   THE BUILD, hero title sequence.
   Black holds, the line fades up on it, the line clears and the
   morph starts. Three beats, one timeline. --seq-start is the
   single source of truth for when the film takes over: site.js
   reads this value rather than repeating the number.
   ============================================================ */
/* .video-hero is qualified here because the shared film-frame rule
   further down paints every frame --paper, and the build opens on
   black. Same weight loses to source order; this outranks it. */
.video-hero.build{
  background:var(--coal);
  /* every writing duration is 25% longer than it was: --seq-in,
     --seq-out and --type-dur. The black and the hold are not writing
     and are unchanged, so the words are slower without the sequence
     sitting still for longer. --seq-start is derived, not chosen:
     black + in + hold. */
  --seq-black: 1000ms;   /* black alone                        */
  --seq-in:    2149ms;   /* the line writes itself             */
  --seq-hold:   600ms;   /* a beat, fully formed               */
  --seq-out:   1563ms;   /* the line unwrites                  */
  /* black + in + hold + OUT. The film used to start while the line was
     still clearing, which was a nice overlap when clearing took 800ms.
     At 1563ms the line is still unwriting 1.5s into the film, on top of
     the first caption, and they share a cell. So the film now waits for
     a clean black frame. Lead-in is 5.3s. */
  --seq-start: 5312ms;

  /* the caption clock. --cap-step is one face plus one morph, so it
     is HOLD + MORPH in scripts/morph/render.py: change it there and
     it changes here. --cap-in/--cap-out are the window inside each
     hold, kept clear of both morphs and of the opening line. */
  --cap-step: 5.2s;
  --cap-in:   1s;
  --cap-out:  2.9s;      /* to the end of the face's own hold */

  /* the finale. --finale-at is a point on the video's own clock: the
     moment the closing dissolve completes, so the title writes onto a
     frame that has already settled. site.js reads it. */
  --finale-at: 15s;
  --fin-gap:  400ms;     /* beat between the title and the credit */
}

/* PHONES: the same beats, six tenths of the time. 5.3s of black is a
   long time to hold a phone and watch nothing, so the black is halved
   and the line's three parts are scaled together by 0.6.

   Together is the point: typeInOut's stops, 49.84% and 63.75%, are
   --seq-in and --seq-in + --seq-hold as a share of all three, so
   scaling them by ONE factor leaves both ratios exactly where they
   are and the keyframes need no retiming. --seq-start stays derived,
   black + in + hold + out, so the film still waits for a clean black
   frame rather than starting under a line that is still clearing.
   Lead-in is 3.1s. The video's own clock, --cap-* and --finale-at,
   is untouched: it belongs to the film, not to the opening. */
@media (max-width:767px){
  .video-hero.build{
    --seq-black:  500ms;
    --seq-in:    1289ms;
    --seq-hold:   360ms;
    --seq-out:    938ms;
    --seq-start: 3087ms;
  }
}
.voices{
  position:absolute;inset:0;z-index:2;
  display:flex;align-items:center;
  padding:var(--s-8) var(--page-margin);
  pointer-events:none;
}
/* the typed lines start at the page margin, like all other content:
   there is no rail occupying that space to clear any more. */
/* No axis rule in the build. Elsewhere the hairline is what content
   hangs off; here the caret is already marking the line, and a second
   vertical mark beside it just reads as clutter. */
.voices .axis::before{display:none}
/* every line occupies the same cell, so the captions speak from
   exactly where the opening line left off rather than beside it */
.voices .axis{display:grid}
/* centred, not stretched: a stretched row would make every line as tall
   as the tallest one, and the caret is a border on that box, so it
   would stand two and a half times the height of its own text. */
.voices .axis > *{grid-area:1/1;align-self:center}
/* Every line in the build that writes itself out. This is the one place
   on the site set like code rather than like editorial type: no
   tracking, regular weight, and a block cursor instead of a rule. The
   site's --tr-meta tracking is what makes small type read as a label,
   and a label is exactly what these are not. */
.type{
  margin:0;justify-self:start;color:var(--lit);
  font-size:var(--t-meta);font-weight:var(--w-reg);
  --type-track:var(--tr-body);            /* code is not tracked */
  letter-spacing:var(--type-track);line-height:var(--lh-meta);
  /* the typed width: one monospace advance plus its tracking, per
     character. --chars is written into each line by the builder, and
     the tracking is read from the same token that sets it, so the two
     can never drift apart. */
  --type-w:calc(var(--chars) * (1ch + var(--type-track)));
  white-space:nowrap;overflow:hidden;
  /* The caret: a terminal block sitting at the write head, one
     character wide. content-box so it hangs outside the typed width
     instead of eating the last glyph. Transparent until a line is
     being written, so the opening black stays empty for its second. */
  box-sizing:content-box;
  border-right:var(--caret-w) solid transparent;
}
/* the opening line is on black and needs nothing. the captions are on
   the film, and the scrim is gone, so each carries the sanctioned
   --on-image plate instead: the same 78% coal as the rail and the
   dodge chip, measured to hold over the brightest frame in the morph.
   A plate, not a gradient: it darkens the words, never the face.
   The plate rides the inner span so that a line clipped to zero width
   leaves nothing behind, not a bare sliver of its own ground. */
.cap > span{
  display:inline-block;
  background:var(--on-image);
  padding:var(--s-2) var(--s-3);
  border-radius:var(--radius-sm);
}
.voices-line > span,.fin-title > span,.fin-credit > span{display:inline-block}

/* ---- the finale ------------------------------------------------------
   The pair sits either side of the face on the closing frame: the title
   in the lit curtain on the left, the credit in the dark behind the
   head. Each is centred on its own quarter point.

   Centred by its FINAL width, not by transform: the box is what the
   typing animates, so centring it live would slide the words sideways
   as they were written. Anchoring the left edge at (centre - half the
   typed width) puts the finished line dead centre and still lets it
   write itself left to right, which is the whole point. */
.finale{position:absolute;inset:0;z-index:2;pointer-events:none}
.finale .type{
  position:absolute;top:50%;transform:translateY(-50%);
  justify-self:auto;
}
.fin-title { left:calc(25% - var(--type-w) / 2) }
.fin-credit{ left:calc(75% - var(--type-w) / 2) }

/* The curtain is the one light ground in the build, so the title takes
   the ink set there while everything else on film stays lit. Measured
   over the title's own box on the closing frame, not estimated:
   ink on curtain 6.30:1 mean, 5.48:1 at its darkest pixel. The lit set
   would be 2.40:1 in the same box, which is why it cannot be used.
   Over the head at 75% the numbers invert: lit 15.51:1, ink 1.03:1. */
@media (min-width:768px){
  .fin-title{color:var(--ink)}
  /* unless the film never got to the closing frame: over the opening
     face there is no curtain to hold ink, and the lit set is what
     survives. Set by site.js when the rest seek does not land. */
  html.no-rest-frame .fin-title{color:var(--lit)}
}
/* narrow screens crop the frame to the head and lose the curtain
   entirely, so the pair stacks in the dark and stays lit. */
@media (max-width:767px){
  .finale{display:flex;flex-direction:column;align-items:center;
    justify-content:center;gap:var(--s-4);padding-inline:var(--page-margin)}
  .finale .type{position:static;transform:none;left:auto}
}
@media (max-width:767px){
  .voices{padding-block:var(--s-7)}
}

/* Without JS there is no sequence to run: the poster, the line and
   the chip are simply all present. The build only hides things once
   something is able to reveal them again. */
.js .build .hero-morph,
.js .build .dodge{opacity:0;transition:opacity var(--seq-out) var(--ease-out)}
.js .build .voices-line{width:0}
.js .build.is-playing .hero-morph,
.js .build.is-playing .dodge{opacity:1}
/* captions are switched by the video clock, not by a timeline of
   their own, so they cannot drift off the face they name. each types
   itself in and unwrites itself on the way out, stepped once per
   character so the reveal lands glyph by glyph rather than sliding. */
.js .build .cap{
  width:0;
  transition:width var(--type-dur) steps(var(--chars));
}
.js .build .cap.is-on{width:calc(var(--type-w) + var(--s-3) * 2)}
/* the finale holds at nothing until the film has come to rest */
.js .build .fin-title,
.js .build .fin-credit{width:0}

/* ---- the faces: three from the film, dissolving one into the next ----
   the sequence is a rendered 17.5s video, not a runtime effect. the warp
   is gone; each face is held clean through its caption and then crosses
   into the next over 1.6s, the same dissolve the build already used to
   arrive on the key art. the poster is the first face, so a still frame
   is what shows before it plays and what remains under reduced motion.
   source and re-render: scripts/morph/render_dissolve.py */
.hero-morph{background:var(--ink)}
.video-hero::after{content:"";position:absolute;inset:0;
  background:var(--scrim);pointer-events:none}
/* the morph carries no scrim. nothing is set over it, so the only
   thing a gradient would do is dim the faces. the dodge chip brings
   its own --on-image ground, and the rail keeps --rail-scrim. */
.hero--full .video-hero::after{background:none}
.hero-block{position:absolute;left:0;bottom:0;z-index:2;
  padding:var(--s-7) 0 var(--s-8) var(--page-margin);
  width:100%;max-width:var(--container)}
.hero--full .hero-block{padding-bottom:var(--s-9)}
/* type over photography uses the lit set: the ground there is dark */
.hero-block .t-display,.hero-block .mark .label{color:var(--lit)}
.hero-block .t-lead,.hero-block .ph{color:var(--lit-2)}
@media (max-width:1023px){
  .hero-block{position:static;padding:var(--s-5) var(--page-margin) 0;max-width:none}
  .hero-block .t-display,.hero-block .mark .label{color:var(--ink)}
  .hero-block .t-lead,.hero-block .ph{color:var(--ink-2)}
}

/* ============================================================
   THE DODGE, M-26.
   A control labelled "stop it" that cannot be caught. It reads as
   navigation and behaves like the thing the film is about: the
   more attention you give it, the harder it works to stay ahead
   of you.

   It is pointer-events:none, so "never clickable" is a guarantee
   of physics, not a race the cursor might win. It is aria-hidden
   and is not a link, because a dead control in the accessibility
   tree is a barrier rather than a joke: a keyboard or screen
   reader user would be handed something that announces as
   navigation and does nothing. The nuisance is for people who
   can watch it run.
   ============================================================ */
.dodge{
  position:absolute;left:0;top:0;z-index:3;
  pointer-events:none;user-select:none;
  font-family:var(--mono);font-size:var(--t-meta);font-weight:var(--w-reg);
  letter-spacing:var(--tr-meta);line-height:1;
  color:var(--lit);
  background:var(--on-image);
  border:var(--hair) solid var(--lit-2);
  border-radius:var(--radius-sm);
  padding:var(--s-2) var(--s-3);
  white-space:nowrap;
  will-change:transform;
}
.dodge .br{color:var(--lit-2)}
/* opacity, never display:none. A zeroed box cannot be clamped. */
.dodge{opacity:1;transition:opacity var(--dur-2) var(--ease-out)}

/* no pointer, no chase: it drifts instead of fleeing */
@media (hover: none){
  .dodge{transition:transform 2.4s var(--ease-out)}
}
/* motion sensitivity outranks the gag: it simply sits there */
@media (prefers-reduced-motion: reduce){
  .dodge{transition:none}
}

/* ============================================================
   INDEX LIST, numbered. The reference numbers a real sequence,
   so numbering is legitimate here: it is the filmography order.
   ============================================================ */
.work-list{list-style:none;margin:0;padding:0;
  border-top:var(--hair) solid var(--rule)}
.work-row{border-bottom:var(--hair) solid var(--rule)}
.work-row a{display:grid;gap:var(--gutter);align-items:baseline;
  grid-template-columns:3rem 1fr auto;
  padding-block:var(--s-4);text-decoration:none;border:0;
  transition:padding-left var(--dur-2) var(--ease-out)}
.work-row a:hover,.work-row a:focus-visible{padding-left:var(--s-3)}
.work-num{font-size:var(--t-micro);letter-spacing:var(--tr-meta);color:var(--ink-3)}
.work-title{font-size:var(--t-head);font-weight:var(--w-thin);
  letter-spacing:var(--tr-head);line-height:var(--lh-head);color:var(--ink);
  transition:color var(--dur-1) var(--ease-out)}
.work-row a:hover .work-title,.work-row a:focus-visible .work-title{color:var(--i)}
.work-kind{font-size:var(--t-micro);letter-spacing:var(--tr-meta);color:var(--ink-2)}
.work-row:nth-child(4n+1){--i:var(--i-1)} .work-row:nth-child(4n+2){--i:var(--i-2)}
.work-row:nth-child(4n+3){--i:var(--i-3)} .work-row:nth-child(4n+4){--i:var(--i-4)}
@media (max-width:767px){
  .work-row a{grid-template-columns:2.5rem 1fr}
  .work-kind{grid-column:2}
}

/* ============================================================
   MEDIA
   ============================================================ */
/* opaque grounds so the frame never shows through a frame */
.video-hero,.video-inline,.work-still,.fig img,.fig video{background:var(--paper)}

.fig{margin:0}
.fig img,.fig video{width:100%;aspect-ratio:1.896/1;object-fit:cover;
  border-radius:var(--radius-frame)}
.fig figcaption{margin-top:var(--s-2);font-size:var(--t-micro);
  letter-spacing:var(--tr-meta);color:var(--ink-2)}

.lay-duo{display:grid;gap:var(--gutter);grid-template-columns:repeat(2,minmax(0,1fr))}
.lay-trio{display:grid;gap:var(--gutter);grid-template-columns:repeat(3,minmax(0,1fr))}
.lay-offset{display:grid;gap:var(--gutter);grid-template-columns:repeat(6,minmax(0,1fr))}
.lay-offset > :first-child{grid-column:1 / span 4}
.lay-offset > :last-child{grid-column:5 / span 2;align-self:end}
@media (max-width:767px){
  .lay-duo,.lay-trio,.lay-offset{grid-template-columns:1fr}
  .lay-offset > :first-child,.lay-offset > :last-child{grid-column:auto}
}

.video-inline{position:relative;width:100%;aspect-ratio:16/9;
  background:var(--surface);border:var(--hair) solid var(--rule);
  border-radius:var(--radius);overflow:hidden}
.video-inline video,.video-inline iframe{position:absolute;inset:0;
  width:100%;height:100%;border:0;object-fit:cover}
.video-caption{margin-top:var(--s-3);font-size:var(--t-micro);
  letter-spacing:var(--tr-meta);color:var(--ink-2)}

/* ============================================================
   CARDS, CHIPS, BUTTONS
   ============================================================ */
.card{background:var(--surface);border-radius:var(--radius);
  padding:var(--s-5);border-left:2px solid var(--i)}

.row-card{background:var(--surface);border-radius:var(--radius);
  border-left:2px solid var(--i);
  padding:var(--s-4) var(--s-5);display:flex;flex-wrap:wrap;
  align-items:center;gap:var(--s-4)}
.row-card + .row-card{margin-top:var(--s-2)}
.row-card .rc-title{flex:1 1 16ch;font-size:var(--t-lead);
  font-weight:var(--w-thin);letter-spacing:var(--tr-tight);color:var(--ink)}
.row-card .rc-meta{display:flex;flex-wrap:wrap;gap:var(--s-3);
  font-size:var(--t-micro);letter-spacing:var(--tr-meta);color:var(--ink-2)}
.row-card .rc-act{margin-left:auto}
.row-card:nth-child(4n+1){--i:var(--i-1)} .row-card:nth-child(4n+2){--i:var(--i-2)}
.row-card:nth-child(4n+3){--i:var(--i-3)} .row-card:nth-child(4n+4){--i:var(--i-4)}
@media (max-width:767px){.row-card{flex-direction:column;align-items:flex-start}
  .row-card .rc-act{margin-left:0}}

.chip{display:inline-block;border:var(--hair) solid var(--i);
  border-radius:var(--radius-sm);padding:var(--s-2) var(--s-3);margin:0 var(--s-1);
  font-family:var(--mono);font-size:var(--t-micro);letter-spacing:var(--tr-meta);
  line-height:1.2;color:var(--i);text-decoration:none;
  transition:background var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out)}
a.chip:hover,a.chip:focus-visible{background:var(--i);color:var(--paper)}
.chip--pending{border-style:dashed}
.chip-row{display:flex;flex-wrap:wrap;gap:var(--s-2);align-items:center;
  margin:0 0 var(--s-4);padding:0;list-style:none}
.chip-row > li:nth-child(4n+1){--i:var(--i-1)} .chip-row > li:nth-child(4n+2){--i:var(--i-2)}
.chip-row > li:nth-child(4n+3){--i:var(--i-3)} .chip-row > li:nth-child(4n+4){--i:var(--i-4)}

.btn{display:inline-flex;align-items:center;gap:var(--s-2);
  font-family:var(--mono);font-size:var(--t-meta);font-weight:var(--w-reg);
  letter-spacing:var(--tr-meta);color:var(--ink);background:none;border:0;
  border-bottom:var(--hair) solid var(--i);border-radius:var(--radius-sm);
  padding:var(--s-3) 0 var(--s-2);min-height:var(--target-min);cursor:pointer;
  text-decoration:none;transition:background var(--dur-1) var(--ease-out),
  color var(--dur-1) var(--ease-out)}
.btn--bracket{border-bottom:0;padding:var(--s-3) var(--s-4);
  border:var(--hair) solid var(--i);color:var(--i)}
.btn.btn--bracket:hover,.btn.btn--bracket:focus-visible{background:var(--i);color:var(--paper)}
.btn.btn--bracket:hover .br,.btn.btn--bracket:focus-visible .br{color:var(--paper)}
.btn .br{color:var(--ink-3)}
.btn[aria-disabled="true"]{color:var(--ink-3);border-color:var(--rule);
  cursor:not-allowed;pointer-events:none}

/* ============================================================
   TABLES, FORM, SUPPORT
   ============================================================ */
table{width:100%;border-collapse:collapse;font-size:var(--t-micro);
  font-variant-numeric:tabular-nums}
th{text-align:left;font-weight:var(--w-light);color:var(--ink-2);
  padding:var(--s-2) var(--s-4) var(--s-2) 0;vertical-align:top;width:40%;
  border-bottom:var(--hair) solid var(--rule);letter-spacing:var(--tr-meta)}
td{color:var(--ink);padding:var(--s-2) 0;vertical-align:top;
  border-bottom:var(--hair) solid var(--rule)}
thead th{color:var(--ink);width:auto}
.scroll{overflow-x:auto}

.form{max-width:var(--measure-mono);display:flex;flex-direction:column;gap:var(--s-5)}
.field{display:flex;flex-direction:column;gap:var(--s-2)}
.field label{font-size:var(--t-micro);letter-spacing:var(--tr-meta);color:var(--ink-2)}
.field input,.field textarea{font-family:var(--mono);font-size:var(--t-body);
  font-weight:var(--w-light);background:var(--paper);color:var(--ink);border:0;
  border-bottom:var(--hair) solid var(--ink-3);border-radius:var(--radius-sm);
  padding:var(--s-2) 0;min-height:var(--target-min);
  transition:border-color var(--dur-1) var(--ease-out)}
.field textarea{resize:vertical;line-height:var(--lh-body)}
.field input:focus,.field textarea:focus{outline:0;border-bottom-color:var(--rim)}
.field.is-error input,.field.is-error textarea{border-bottom-color:var(--err)}
.field .msg{font-size:var(--t-micro);color:var(--err)}
.form-status{font-size:var(--t-meta);letter-spacing:var(--tr-meta);color:var(--ok)}
.form-status[hidden]{display:none}

.support{border-left:2px solid var(--i);padding-left:var(--s-4);
  max-width:var(--measure-mono);font-size:var(--t-micro);color:var(--ink-2)}
.pull{border-left:2px solid var(--i);padding-left:var(--s-5);max-width:20ch}

/* ============================================================
   FAQ. The questions answered in the open, set as the list it
   is: term and definition, not a widget. Nothing collapses,
   because an answer hidden behind a control is an answer a
   reader has to work for and an answer engine has to guess at.
   ============================================================ */
.faq{margin:0}
.faq dt{
  margin-top:var(--s-5);
  font-size:var(--t-meta);letter-spacing:var(--tr-meta);
  line-height:var(--lh-meta);color:var(--ink);
}
.faq dt:first-child{margin-top:0}
.faq dd{
  margin:var(--s-2) 0 0;
  font-size:var(--t-micro);letter-spacing:var(--tr-meta);
  line-height:var(--lh-body);color:var(--ink-2);
  max-width:var(--measure-mono);
}

/* ============================================================
   FOOTER
   ============================================================ */
.site-foot{padding-block:var(--s-7) var(--s-8);
  border-top:var(--hair) solid var(--rule);
  font-size:var(--t-micro);letter-spacing:var(--tr-meta);color:var(--ink-2)}
.foot-grid{display:grid;gap:var(--s-6);
  grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin-bottom:var(--s-6)}
.foot-grid h2{font-size:var(--t-nano);letter-spacing:var(--tr-meta);
  color:var(--ink-2);font-weight:var(--w-reg);margin-bottom:var(--s-3)}
.foot-grid ul{list-style:none;margin:0;padding:0;display:flex;
  flex-direction:column;gap:var(--s-2)}
.foot-grid a,.site-foot a{color:var(--i);text-decoration:none;
  border-bottom:var(--hair) solid var(--i);display:inline-block;
  transition:background var(--dur-1) var(--ease-out),color var(--dur-1) var(--ease-out)}
.foot-grid a:hover,.site-foot a:hover{background:var(--i);color:var(--paper)}
.foot-grid > div:nth-child(4n+1){--i:var(--i-1)} .foot-grid > div:nth-child(4n+2){--i:var(--i-2)}
.foot-grid > div:nth-child(4n+3){--i:var(--i-3)} .foot-grid > div:nth-child(4n+4){--i:var(--i-4)}
.foot-sum{max-width:var(--measure-prose);color:var(--ink-2);
  font-size:var(--t-micro);line-height:1.7}
.soc-list{display:flex;flex-direction:column;gap:var(--s-2);list-style:none;margin:0;padding:0}
.soc-list span[aria-disabled="true"]{color:var(--ink-3)}

.i1{--i:var(--i-1)} .i2{--i:var(--i-2)} .i3{--i:var(--i-3)} .i4{--i:var(--i-4)}
section:nth-of-type(4n+1){--i:var(--i-1)} section:nth-of-type(4n+2){--i:var(--i-2)}
section:nth-of-type(4n+3){--i:var(--i-3)} section:nth-of-type(4n+4){--i:var(--i-4)}

/* ============================================================
   MOTION. Timing and easing from the reference.
   ============================================================ */
@media (prefers-reduced-motion: no-preference){
  .rise{opacity:0;transform:translateY(var(--s-2));
    transition:opacity var(--dur-3) var(--ease-out),transform var(--dur-3) var(--ease-out)}
  .rise.is-in{opacity:1;transform:none}

  .js .lead-in{opacity:0;animation:fadeIn var(--dur-3) forwards var(--ease-out)}
  .js .lead-in--1{animation-delay:60ms}
  .js .lead-in--2{animation-delay:120ms}
  .js .lead-in--3{animation-delay:180ms}
  .js .lead-in--4{animation-delay:240ms}

  /* the reference's page image transition: the frame is revealed
     rather than faded, wiping up over 500ms on the in-out quint.
     Applied to every film frame, not just the hero. */
  /* the build hero is excluded: it runs its own title sequence, and
     a wipe that ends at opacity 1 would reveal the film early. */
  .js .video-hero:not(.build) img,.js .video-hero:not(.build) video,
  .js .fig img,.js .work-still img{
    animation:pageImageTransition var(--dur-3) forwards var(--ease)}
  .js .callout{animation:pulse var(--dur-pulse) infinite ease}

  /* the line: up on the black, held, then cleared as the film starts.
     the stops are --seq-in and --seq-in+--seq-hold as a share of the
     whole; they are written out, so retime the stops if you retime
     the tokens on .build. */
  /* The opening line writes itself, is held, then unwrites back to
     where it started, character by character in both directions. It
     does not fade: a line that fades has been switched off, a line
     that deletes has been taken back, and taking it back is the point.
     The caret runs four blinks, enough to outlast the line, and its
     last one ends transparent so nothing is left blinking on the film.
     Stops are --seq-in and --seq-in+--seq-hold as a share of the whole;
     retime them if you retime the tokens on .build. */
  .js .build .voices-line{
    animation:
      typeInOut calc(var(--seq-in) + var(--seq-hold) + var(--seq-out))
        steps(var(--chars)) var(--seq-black) forwards,
      caret var(--caret-rate) steps(2,jump-none) var(--seq-black) 5 forwards;
  }
  .js .build .cap.is-on{
    animation:caret var(--caret-rate) steps(2,jump-none) infinite}

  /* plate type writes itself on the same idiom as the build, staggered
     by --at so a zone reads top line first */
  .js .plate .type{width:0}
  .js .plate .type{
    animation:
      typeIn var(--type-dur) steps(var(--chars)) var(--at,0ms) forwards,
      caret var(--caret-rate) steps(2,jump-none) var(--at,0ms) 4 forwards}

  /* the title types, then hands the caret to the credit beside it, so
     only one line is ever being written. the credit keeps its caret
     blinking afterwards, the way a prompt sits waiting. */
  .js.is-final .fin-title{
    animation:
      typeIn var(--type-dur) steps(var(--chars)) forwards,
      caret var(--caret-rate) steps(2,jump-none) 1 forwards}
  .js.is-final .fin-credit{
    animation:
      typeIn var(--type-dur) steps(var(--chars))
        calc(var(--type-dur) + var(--fin-gap)) forwards,
      caret var(--caret-rate) steps(2,jump-none)
        calc(var(--type-dur) + var(--fin-gap)) infinite}

  /* typeIn writes and stays: the closing title is the one thing on the
     page that is not taken back. typeInOut writes, holds, unwrites. */
  @keyframes typeIn{from{width:0}to{width:var(--type-w)}}
  /* compositor-only equivalents, used by everything except the hero
     sequence, which is a single element and can afford the layout */
  @keyframes typeClip{
    from{clip-path:inset(0 100% 0 0)}
    to  {clip-path:inset(0 0 0 0)}
  }
  @keyframes caretRun{
    from{transform:translateX(0);opacity:1}
    to  {transform:translateX(var(--type-w));opacity:1}
  }
  @keyframes caretBlink{
    from{opacity:1}
    to  {opacity:0}
  }
  /* stops are --seq-in and --seq-in+--seq-hold as a share of the whole
     2149 + 600 + 1563: retime these if you retime those tokens */
  @keyframes typeInOut{
    0%     {width:0}
    49.84% {width:var(--type-w)}
    63.75% {width:var(--type-w)}
    100%   {width:0}
  }
  @keyframes caret{
    from{border-right-color:var(--lit)}
    to  {border-right-color:transparent}
  }


  @keyframes fadeIn{from{opacity:0;transform:translateY(var(--s-2))}to{opacity:1;transform:none}}
  @keyframes pageImageTransition{
    from{clip-path:inset(100% 0 0 0);opacity:0}
    to{clip-path:inset(0 0 0 0);opacity:1}
  }
  @keyframes fadeOut{from{opacity:1}to{opacity:0}}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.72}}
}
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  .rise{opacity:1;transform:none}
  /* nothing types and nothing plays: the line is simply set, whole,
     over the poster frame. width must be released too, or the line
     would sit clipped to nothing with no animation to open it. */
  .js .build .voices-line{opacity:1;width:auto}
  .js .build .cap.is-on{transition:none}
  .js .build .fin-title,.js .build .fin-credit{width:auto}
}

/* ============================================================
   EXOSKELETON, M-24. v2.2
   The Pompidou does not hint at its structure, it hangs the
   structure outside the skin and paints the services so the
   street can read the building. This is that, literally:
   a steel frame with node joints, live gridlines, and live
   instrumentation. The dashed service ducts were removed at the
   owner's direction: they competed with the photography.
   All of it is decoration in the accessibility sense: the whole
   layer is aria-hidden and pointer-events:none. It never
   intercepts a click and never reaches a screen reader.
   ============================================================ */
/* The frame sits BEHIND the content. The structure is still fully
   exposed against the paper, but photography occludes it: a still
   is the one thing on this site that is never competed with. */
.frame{position:fixed;inset:0;pointer-events:none;z-index:-1;
  contain:strict}
html.svc-off .frame{display:none}

/* --- structural columns, the building's bays ---------------- */
.frame .col{position:absolute;top:0;bottom:0;width:var(--hair);
  background:var(--rule);transform-origin:top}
.frame .col--major{background:var(--ink-3);width:var(--hair)}

/* --- node joints where members cross ------------------------ */
.frame .node{position:absolute;width:var(--node);height:var(--node);
  border:var(--hair) solid var(--ink-3);background:var(--paper);
  transform:translate(-50%,-50%) rotate(45deg)}

/* --- beams at section boundaries ---------------------------- */
.beam{position:relative}
.beam::before{content:"";position:absolute;left:0;right:0;top:0;
  height:var(--beam);background:var(--ink-3);opacity:.35;
  transform-origin:left;pointer-events:none}

/* --- instrumentation readout, the plant control panel ------- */
.gauge{font-size:var(--t-nano);letter-spacing:var(--tr-meta);
  color:var(--ink-2);border-top:var(--hair) solid var(--rule);
  padding-top:var(--s-2);display:grid;gap:2px}
/* --ink-2, not --ink-3: tokens.css marks --ink-3 as 3.23:1 and NOT
   text, and this is text. --ink-2 is the 7.61:1 secondary. */
.gauge b{font-weight:var(--w-reg);color:var(--ink-2);display:inline-block;min-width:5.5em}
.gauge var{font-style:normal;color:var(--svc-structure);font-variant-numeric:tabular-nums}
html.svc-off .gauge{display:none}

/* --- live code panels, the drawings pinned to the facade ---- */
.code-panel{position:relative;background:var(--surface);
  border-left:var(--beam) solid var(--svc,var(--svc-structure));
  border-radius:0 var(--radius-sm) var(--radius-sm) 0;
  padding:var(--s-3) var(--s-4);margin-block:var(--s-3);
  overflow:hidden}
.code-panel[data-svc="type"]     {--svc:var(--svc-type)}
.code-panel[data-svc="space"]    {--svc:var(--svc-space)}
.code-panel[data-svc="motion"]   {--svc:var(--svc-motion)}
.code-panel[data-svc="color"]    {--svc:var(--svc-color)}
.code-panel[data-svc="structure"]{--svc:var(--svc-structure)}
.code-panel figcaption{font-size:var(--t-nano);letter-spacing:var(--tr-meta);
  color:var(--svc);margin-bottom:var(--s-2)}
.code-panel pre{margin:0;font-family:var(--mono);font-size:var(--t-micro);
  line-height:1.6;color:var(--ink-2);white-space:pre-wrap;word-break:break-word}
.code-panel .k{color:var(--svc)}
.code-panel .n{color:var(--ink);font-variant-numeric:tabular-nums}
html.svc-off .code-panel{display:none}

/* the scan line that reads the drawing as it enters */
.code-panel::after{content:"";position:absolute;left:0;right:0;top:0;
  height:var(--beam);background:var(--svc);opacity:0;pointer-events:none}

/* --- hover inspection, the box model drawn on the outside --- */
#probe{position:fixed;pointer-events:none;z-index:45;
  border:var(--hair) dashed var(--svc-color);display:none}
#probe::before{content:attr(data-box);position:absolute;left:0;bottom:100%;
  font-family:var(--mono);font-size:var(--t-nano);letter-spacing:var(--tr-meta);
  color:var(--paper);background:var(--svc-color);padding:2px 6px;
  white-space:nowrap;border-radius:var(--radius-sm) var(--radius-sm) 0 0}
#probe .pad{position:absolute;inset:0;border:var(--hair) dashed var(--svc-space);
  opacity:.7}
html.svc-off #probe{display:none !important}

@media (prefers-reduced-motion: no-preference){
  /* the frame draws itself in on load, bottom up, like erection */
  .js .frame .col{animation:drawCol var(--draw) var(--ease-out) forwards;
    transform:scaleY(0)}
  .js .frame .node{animation:fadeIn var(--dur-2) var(--ease-out) forwards;
    opacity:0;animation-delay:var(--draw)}
  .js .beam::before{animation:drawBeam var(--dur-3) var(--ease-out) forwards;
    transform:scaleX(0)}
  .js .code-panel::after{animation:scan var(--dur-3) var(--ease) forwards}

  @keyframes drawCol{to{transform:scaleY(1)}}
  @keyframes drawBeam{to{transform:scaleX(1)}}
  @keyframes scan{
    0%{opacity:1;transform:translateY(0)}
    90%{opacity:1;transform:translateY(var(--s-8))}
    100%{opacity:0;transform:translateY(var(--s-8))}
  }
}
@media (prefers-reduced-motion: reduce){
  /* structure stays visible, nothing moves */
  .frame .col{transform:none}
  .beam::before{transform:none}
}

/* ============================================================
   LIGHTBOX. A press still is an asset: a programmer needs to see
   it at size before deciding to download it.
   Built on <dialog>, so the browser supplies the modal focus
   trap, the Escape key and the inert background. Writing those
   by hand is how a lightbox ends up unreachable by keyboard.
   ============================================================ */
.zoom{
  display:block;width:100%;padding:0;border:0;background:none;
  cursor:zoom-in;color:inherit;
}
.zoom img{display:block;width:100%;height:auto}

.lightbox{
  border:0;padding:0;max-width:100vw;max-height:100vh;
  width:100%;height:100%;
  background:var(--coal);color:var(--lit);
}
.lightbox::backdrop{background:var(--coal)}
.lightbox img{
  width:100%;height:100%;object-fit:contain;display:block;
  background:var(--coal);
}
.lightbox__close{
  position:fixed;top:var(--s-4);right:var(--s-4);
  width:var(--target-min);height:var(--target-min);
  display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:var(--t-lead);line-height:1;
  color:var(--lit);background:var(--on-image);
  border:var(--hair) solid var(--lit-2);border-radius:var(--radius-sm);
  cursor:pointer;
}
.lightbox__close:hover{color:var(--i);border-color:var(--i)}
@media (prefers-reduced-motion: no-preference){
  .lightbox[open]{animation:fadeIn var(--dur-2) var(--ease-out)}
}

/* ============================================================
   REPLAY. The title sequence runs once per load, so there has to
   be a way back to it that is not the reload button. Set in the
   nav's own register, one step quieter: it is a way back to
   something already seen, not a destination.
   Only the page that HAS a sequence shows it.
   ============================================================ */
.rail-replay{display:none}
html.has-build .rail-replay{
  display:flex;align-items:center;gap:var(--s-2);
  min-height:var(--target-min);padding:0;
  font-family:var(--mono);font-size:var(--t-micro);
  letter-spacing:var(--tr-meta);
  color:var(--ink-3);background:none;border:0;cursor:pointer;
  transition:color var(--dur-1) var(--ease-out);
}
.rail-replay .slash{color:var(--i-3)}
.rail-replay:hover,.rail-replay:focus-visible{color:var(--ink)}
html.at-hero .rail-replay{color:var(--lit-2)}
html.at-hero .rail-replay:hover,
html.at-hero .rail-replay:focus-visible{color:var(--lit)}

/* A visitor who has already seen the sequence this session opens on its
   end state. Nothing types and nothing plays: the classes that would
   normally animate are neutralised rather than removed, so the replay
   control can put them all back by dropping .is-seen. */
.js .build.is-seen .voices-line{animation:none;width:0}
.js .build.is-seen .cap{transition:none}
.js .build.is-seen .fin-title,
.js .build.is-seen .fin-credit{animation:none;width:auto}

/* ============================================================
   STILL GRID. Two up and full width, so a still is read at
   roughly twice the size it had inside the .split column, which
   is what a press page is for. Each carries its own caption,
   its measured weight and its own download: looking at a picture
   in one place and fetching it in another was the old way.
   ============================================================ */
.still-grid{display:grid;gap:var(--gutter)}
@media (min-width:768px){.still-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.fig-bar{
  display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-3);
  margin-top:var(--s-2);
  font-size:var(--t-nano);letter-spacing:var(--tr-meta);color:var(--ink-2);
}
.fig-meta{color:var(--ink-3)}
.fig-bar .btn{margin-left:auto;font-size:var(--t-nano)}

/* ============================================================
   TYPE ON. Every short line on the site writes itself the way
   the hero does; anything that wraps is revealed instead,
   because a width animation cannot type a paragraph. Both are
   held until the element is in view, so this covers the load and
   the scroll with one mechanism.
   ============================================================ */
.js .type-line{
  position:relative;white-space:nowrap;
}
/* the reveal is a CLIP, not a width. Animating width relayouts the page
   on every frame of every typing line, which is what was stealing
   frames from the video. clip-path and transform stay on the
   compositor and never touch layout. */
.js .type-line > span{
  display:inline-block;
  clip-path:inset(0 100% 0 0);
}
.js .type-line::after{
  content:"";position:absolute;left:0;top:0;bottom:0;
  width:var(--caret-w);background:currentColor;opacity:0;
}
.js .type-line.is-typed > span{
  animation:typeClip var(--td,var(--type-dur)) steps(var(--chars)) forwards;
}
.js .type-line.is-typed::after{
  animation:caretRun var(--td,var(--type-dur)) steps(var(--chars)) forwards,
            caretBlink var(--caret-rate) steps(2,jump-none) 2 forwards;
}
.js .fade-line{
  opacity:0;transform:translateY(var(--s-2));
  transition:opacity var(--dur-3) var(--ease-out),
             transform var(--dur-3) var(--ease-out);
}
.js .fade-line.is-typed{opacity:1;transform:none}
@media (prefers-reduced-motion: reduce){
  .js .type-line > span{clip-path:none}
  .js .type-line::after{display:none}
  .js .fade-line{opacity:1;transform:none}
}

/* ============================================================
   CHIP NAV. The film page's section links: spread the full
   measure and stuck to the top, so the four anchors stay
   reachable through a long page instead of scrolling away.
   It parks BELOW the site header rather than at zero, because
   the header is fixed and would otherwise sit on top of it.
   ============================================================ */
.chip-nav{
  position:sticky;
  top:calc(var(--s-3) * 2 + var(--target-min));   /* the header's own height */
  z-index:40;
  display:flex;gap:var(--s-2);
  /* The bleed is the PAGE MARGIN, not a spacing step. The bar sits
     inside the section's own padding, so bleeding by exactly that
     reaches the edge of the frame and never passes it. --s-3 is 16px
     against a 12px margin on phones, which hung 4px off the right of
     every screen narrower than 768 and took the whole page with it. */
  margin-inline:calc(var(--page-margin) * -1);
  padding:var(--s-2) var(--page-margin);
  background:var(--paper);
  border-bottom:var(--hair) solid var(--rule);
}
.chip-nav > li{flex:1 1 0}
.chip-nav .chip{display:flex;justify-content:center;width:100%}
@media (max-width:479px){
  .chip-nav{gap:var(--hair)}
  .chip-nav .chip{padding-inline:var(--s-2)}
}

/* ============================================================
   TYPE BLOCK. A paragraph typed line by real line: each rendered
   line becomes its own single-line box, and they run in sequence
   off --at. The line boxes are rebuilt on resize, because where
   the browser breaks a line is not a constant.
   ============================================================ */
.js .type-block .type-row{
  display:block;white-space:nowrap;overflow:hidden;
  box-sizing:content-box;width:0;
  /* A row is typed out to the width its line box measured. When a line
     carries something that cannot be broken and is wider than the
     column, an address, a long title, that measurement is wider than
     the container, and a row animating out to it takes the page with
     it. The row may never be wider than what holds it: the text is
     clipped already, so the cap costs nothing and closes the last way
     a typed line could put a scrollbar on a phone. */
  max-width:100%;
  border-right:var(--caret-w) solid transparent;
}
.js .type-block.is-typed .type-row{
  animation:typeIn var(--td,var(--type-dur)) steps(var(--chars)) var(--at) forwards,
            caret var(--caret-rate) steps(2,jump-none) var(--at) 1 forwards;
}
@media (prefers-reduced-motion: reduce){
  .js .type-block .type-row > span{clip-path:none}
  .js .type-block .type-row::after{display:none}
}

/* the film's title, set bold wherever it appears as text. 700 is loaded
   for this: the family shipped 200/300/400 only, so a bold declaration
   would have been synthesised or silently ignored. */
.film-title{font-weight:var(--w-bold)}
