/* ═══════════════════════════════════════════════════════════════════════════
   MARBLELENS · SECTIONS
   One book, different chapters (§12). Every chapter shares the typography,
   spacing, paper and physics; each performs its OWN mechanic:

     OPENING    draw · write · assemble
     AI CONTENT cut · assemble · generate · transform
     BUILD      grid · align · build · reflow
     HEADSHOTS  crop · frame · mount · reveal
     STORY      sequence · track · edit · play
     WORK       mount · stack · peel · present
     PROCESS    stamp · pin · mark · advance
     PRICING    layer · lift · unfold
     CLOSER     the book closes

   THE ACCENT SYSTEM IS PHYSICAL (§6, §8). A chapter's ground is its accent
   mixed into the paper at a few percent — so the accent ENTERS as a tinted
   sheet. It EXITS because the next chapter's own sheet is physically TORN over
   the top of it (.paper-cover). No gradient soup, no colour bleeding forward:
   at any scroll position the eye reads BASE + ONE ACCENT.
   ═══════════════════════════════════════════════════════════════════════════ */

/* PACING (§34). The page must not run at full intensity end to end: chapters
   whose whole argument is one visual event get less air around them, so the
   creative chapters and the breath between them are felt as different weights
   rather than as an even column of blocks. */
.section.tight { padding-block: calc(var(--sec-pad-y) * 0.62); }
.section.tight .section-head { margin-bottom: calc(var(--sec-gap) * 0.8); }

/* ── THE TINTED GROUND + THE TORN COVER ────────────────────────────────────*/
.section, .closer, .opening {
  --sec-bg: var(--paper);
  background: var(--sec-bg);
  position: relative;
  isolation: isolate;
}
/* an accented chapter tints its own stock. 3.5% — enough to feel like a
   different sheet was loaded, never enough to read as "a coloured section". */
.section[data-accent] { --sec-bg: color-mix(in srgb, var(--accent) 3.5%, var(--paper)); }
.closer { --sec-bg: var(--paper); }           /* accents gone, book closed */

/* the fresh sheet that covers the chapter above. The ragged boundary lives
   ENTIRELY ABOVE the section's own straight top edge, so the two meet
   seamlessly in the same colour and only the rip is visible. */
.paper-cover { position: absolute; left: 0; right: 0; top: 0; height: 1px; z-index: 0; pointer-events: none; }
.paper-cover .torn {
  position: absolute; left: -5%; right: -5%;
  top: -46px; height: 132px;
  background: var(--sec-bg);
}
.paper-cover .torn[data-tear="0"] { filter: url(#tear0); }
.paper-cover .torn[data-tear="1"] { filter: url(#tear1); }
.paper-cover .torn[data-tear="2"] { filter: url(#tear2); }
.paper-cover .torn[data-tear="3"] { filter: url(#tear3); }
/* content always sits above the ground layer */
.section > .container, .section > .canvas-stage, .closer > .container { position: relative; z-index: 1; }

/* the fibre highlight along a fresh rip — the pale pulp of torn stock */
.paper-cover::after {
  content: ""; position: absolute; left: -5%; right: -5%; top: -8px; height: 3px;
  background: linear-gradient(var(--paper-hi), transparent);
  opacity: 0.55; filter: blur(0.5px);
}

/* ═══ 1 · OPENING — THE APPROVED LANDING TAKE, THEN THE REAL PAGE ══════════
   The old opening was a two-column composition: a headline beside a coded
   "living surface" that posed between an editorial collage and a squared page.
   The approved take does that argument properly and photographically, so this
   whole block is now the presentation of ONE FRAME and the registration of real
   DOM against it.

   TWO ELEMENTS, ONE GRID CELL, ONE WIDTH VARIABLE. .lp-stage (the canvas) and
   .lp-ui (the real page) are siblings at grid-area 1/1 reading the same --lp-w.
   That is what makes registration exact with nothing measured in JS — and the
   UI has to be a SIBLING, because the stage clips and an overlay inside a
   clipping box could never reflow into stacked rows on a phone. */
/* ── THE TRAVEL ───────────────────────────────────────────────────────────
   THE OPENING IS SCRUBBED, NOT PLAYED. The visitor drives the assembly: forward
   builds it, backward un-builds it through the identical frames, stopping
   freezes on the exact frame, and a jump resolves straight to it. There is no
   clock, no playhead and no video, so there is nothing that can keep running,
   restart or replay — the frame on screen is a pure function of where the page
   is scrolled to. Same architecture as .canvas-stage / .canvas-sticky below.

   THE CONSTANT IS vh PER SECOND OF TRAVEL, and it is the one already tuned on
   the AI CONTENT beat: 44.0 vh/s desktop, which puts ~2.5 frames under a wheel
   notch — the difference between dragging a timeline and moving through one.
   The take is 6.00s, so travel is 264vh and the section is that plus the one
   viewport the pin occupies. Re-derive all three breakpoints together if this
   changes; scaling one alone silently makes the phone scrub at a different
   speed from the desktop. */
.opening {
  position: relative;
  height: 364vh;                 /* 264vh travel (44.0 vh/s × 6.00s) + 100vh pin */
  /* the take's own paper, sampled from the frame's edge (#e9e2d5 at the last
     frame, #ede3d7 at the first) rather than --paper (#ece6da). The difference
     is under 2%, which is the whole point: the film is CONTAINED, not cropped,
     and the few pixels of page either side of it have to be the same sheet or
     the containment reads as a letterbox. */
  background: #ebe4d8;
}
.lp-pin {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  display: grid; place-items: center;
  overflow: hidden;
  /* --lp-w LIVES HERE, NOT ON .lp. The scroll cue is a sibling of the frame
     rather than a child (it is page furniture, not part of the composition) and
     still has to be placed against the frame's edges. Declared on .lp it is
     invisible to the cue — the calc silently fails to resolve and the cue drops
     to the middle of the section, which is exactly what happened once. */
  --lp-w: min(100vw, calc(100svh * 16 / 9));
}

/* ── THE BOX ──────────────────────────────────────────────────────────────
   One 16:9 box, as large as the viewport allows, and every coordinate below is
   a percentage of it. CONTAIN, NOT COVER: the composition runs edge to edge —
   the runner enters at the very top, the verbs sit at the very bottom, the
   collage reaches the right margin — so cover would crop away the parts of the
   take that carry the idea. Against a matched paper the letterbox is invisible.

   container-type gives .lp-ui its own cqw, so every registered coordinate is a
   fraction of the FRAME rather than of the viewport. A vw unit would decouple
   the type from the box it has to sit inside the moment height is what binds. */
.lp {
  position: relative;
  width: var(--lp-w); aspect-ratio: 16 / 9;
  display: grid;
}
.lp-stage { grid-area: 1 / 1; position: relative; overflow: hidden; }
.lp-stage .ml-seq { display: block; width: 100%; height: auto; }
.lp-ui {
  grid-area: 1 / 1; position: relative;
  container-type: inline-size;
  /* --ch is the fraction of the SOURCE FRAME HEIGHT this box is showing. 1 on
     desktop (the whole frame); on a phone the box is a top-anchored band and
     every y coordinate below is divided by it. */
  --ch: 1;
}

/* ── REGISTRATION ─────────────────────────────────────────────────────────
   Percentages measured off the take's final frame, not chosen:

     title   x  4.30-57.81   y 24.31-71.53
     lede    x  4.30-40.62   y 72.92-78.47
     button  x  4.30-18.59   y 84.72-91.94
     alt     x 25.78-34.22   y 85.83-89.31
     verbs   x  4.45-20.50   y 93.33-98.06
     scroll  x 86.88-96.33   y 93.33-96.39

   y is written as `calc(<n>% / var(--ch))` so one set of numbers serves the
   full frame and the phone band. */
/* .bridge-ui carries the same registered children, so it needs the same rule.
   Without it the bridge's verbs were position:static and simply flowed to the
   top of the box — measured at y 62 instead of y 801, i.e. under the header
   rather than on the composition's bottom-left. `top`/`left` are inert on a
   static element, which is exactly the kind of failure that looks like a
   z-index problem and is not one. */
.lp-ui > *, .bridge-ui > * { position: absolute; margin: 0; }

/* THE BOX IS THE MEASURED INK BOX, and that is the thing to get right — a
   transparent element is never seen, but its box is what a focus ring draws and
   what a pointer hits, so it has to be the generated headline's own extent.
   Type metrics inside it are set to the take's rhythm rather than chased to the
   glyph: the frame is 56.25cqw tall, the take's line pitch is 15.8% of that
   (8.89cqw) and its cap height 12.8% (7.2cqw), which at Playfair's ~0.70
   cap ratio is a 10.28cqw face on a 0.865 line. Three lines then measure 47.4%
   of frame height against the 47.22% measured off the frame. */
.lp-title {
  left: 4.3%; width: 53.6%;
  top: calc(24.31% / var(--ch)); height: calc(47.22% / var(--ch));
  font-family: var(--serif); font-weight: 700;
  font-size: 10.28cqw; line-height: 0.865; letter-spacing: -0.015em;
  color: var(--ink);
}
/* NEVER LET A LINE WRAP. "motion, code" measures 53.5% of frame width, which is
   close enough to any width cap that a font fallback or a hinting difference
   can push it onto a second line — and a wrap does not degrade the block, it
   destroys it: every line below shifts by a full pitch and the registration is
   gone. The line breaks are authored in the markup; nothing else may break. */
.lp-title .line { display: block; white-space: nowrap; }
.lp-title .accent-word { color: var(--accent); font-style: italic; }

.lp-lede {
  left: 4.3%; width: 26%;
  top: calc(72.92% / var(--ch));
  /* the take's own label metric: 27 characters spanning 25.7% of frame width in
     mono at --ls-label works back to a 1.3cqw face. Sized to the frame like
     everything else here, never to the viewport. */
  font-family: var(--mono); font-size: 1.3cqw;
  letter-spacing: var(--ls-label); text-transform: uppercase;
  color: var(--graphite);
}

.lp-cta { position: absolute; left: 0; right: 0; top: 0; bottom: 0; pointer-events: none; }
.lp-cta > * { position: absolute; pointer-events: auto; }
.lp-btn {
  left: 4.3%; width: 14.3%;
  top: calc(84.72% / var(--ch)); height: calc(7.22% / var(--ch));
  display: grid; place-items: center;
  font-family: var(--sans); font-weight: 600; font-size: 1.75cqw;
  border: 0; border-radius: var(--radius-sm);
  background: var(--accent); color: var(--paper-hi);
  cursor: pointer;
}
.lp-alt {
  left: 25.78%; width: 8.5%;
  top: calc(85.83% / var(--ch)); height: calc(3.48% / var(--ch));
  display: grid; place-items: center;
  font-family: var(--sans); font-weight: 600; font-size: 1.75cqw;
  color: var(--ink); text-decoration: none;
}

/* ── .lp-ghost — REGISTERED OVER PIXELS THE TAKE STILL HOLDS ──────────────
   The take never stops painting the headline, the lede, the Start creating
   button or See what it makes. So these four elements must be real DOM — real
   text, real focus, real routes, real screen-reader output — WITHOUT painting a
   second copy of type that is already on screen. Transparent ink is the whole
   mechanism: exactly one visible rendering of every word (the generated one),
   nothing to double, and nothing that can visibly "swap" at activation, because
   activation changes no pixel at all.

   `color: transparent` rather than opacity/visibility deliberately — the
   element stays hit-testable, focusable and readable by assistive tech, which
   `visibility: hidden` and `opacity: 0` (plus its usual pointer-events removal)
   would all cost. */
.lp-ghost { color: transparent; }
.lp-ghost.lp-btn { background: transparent; box-shadow: none; }
.lp-ghost .accent-word { color: transparent; }

/* interaction still has to be legible, and it cannot be legible in ink that is
   not there — so hover and focus are drawn AROUND the generated glyphs, never
   over them. A ring on the button's own measured box reads as the generated
   button lighting up, because it is exactly the generated button's edge. */
.lp-ui[data-live="1"] .lp-ghost:is(:hover, :focus-visible) {
  outline: 1.5px solid var(--accent-ink); outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.lp-ui[data-live="1"] .lp-btn:is(:hover, :focus-visible) { outline-offset: 0; }
/* nothing in here is reachable until the frame it is registered against is the
   frame on screen. A control that is hittable before it is legible is worse
   than no control. */
.lp-ui[data-live="0"] { pointer-events: none; }
.lp-ui[data-live="0"] :is(a, button) { visibility: hidden; }

/* ── THE VACATED HALF — REAL VISIBLE INK ──────────────────────────────────
   DRAW / WRITE / ASSEMBLE and SCROLL are painted by the take and then removed
   by it (measured: 9.4% ink down to nothing across the last 0.3s), so real DOM
   paints them at full strength with nothing underneath to double. --ui-in is
   the staged handoff value written by js/opening.js; it only ever runs after
   the generated ink has gone. */
.lp-verbs {
  left: 4.45%; top: calc(93.33% / var(--ch));
  /* measured off the frame the take last paints them in: DRAW 4.6-8.1%,
     WRITE 10.7-14.5%, ASSEMBLE 16.9-23.7% — a 1.2cqw face on a 2.5cqw gap. */
  display: flex; gap: 2.5cqw;
  padding: 0; list-style: none;
  font-family: var(--mono); font-size: 1.2cqw;
  letter-spacing: var(--ls-label); color: var(--graphite);
  opacity: var(--ui-in, 0);
}
.lp-verbs li { position: relative; }
.lp-verbs li + li::before {
  content: ""; position: absolute; left: -1.4cqw; top: 50%;
  width: 0.25cqw; height: 1px; background: currentColor;
}

.scrollcue {
  position: absolute; z-index: 2;
  right: calc(50% - var(--lp-w) / 2 + var(--lp-w) * 0.0367);
  bottom: calc(50% - var(--lp-w) * 0.5625 / 2 + var(--lp-w) * 0.5625 * 0.0194);
  display: flex; align-items: center; gap: 0.6em;
  font-family: var(--mono); font-size: 0.58rem;
  letter-spacing: var(--ls-label); color: var(--graphite-2);
  text-decoration: none;
  /* ON AT REST, GONE THE MOMENT THE SCRUB MOVES. It used to ride --ui-in with
     the rest of the vacated chrome, which was right for a film that played
     itself: the cue belonged at the end, pointing out of the beat. Driven by
     the visitor it belongs at the START, because it is the only thing telling
     them the assembly is theirs to move. js/opening.js takes it out by u 0.05,
     which is also well before the take paints its own SCROLL mark at u 0.65. */
  opacity: var(--cue, 1);
  transition: opacity var(--dur) var(--ease-out);
}
/* app.js retires the cue once the visitor has actually started scrolling; two
   classes beat one custom property, so this stays the winning rule */
.scrollcue.gone { opacity: 0; }
.scrollcue i { display: block; width: 1px; height: 22px; background: currentColor;
  transform-origin: top; animation: cue 2.1s var(--ease-out) infinite; }
@keyframes cue { 0%,100% { transform: scaleY(0.3); opacity: 0.4; } 50% { transform: scaleY(1); opacity: 1; } }

/* ── NO SEQUENCE, NO GHOSTS ───────────────────────────────────────────────
   Registration is only meaningful against a frame that exists. If the manifest
   or the frames cannot be fetched, transparent type over an empty box is an
   invisible homepage — so js/opening.js drops the whole registration and this
   leaves ordinary ink on paper. It is the same shape the narrow breakpoint
   renders, which is why it needs so little: a stack of real, legible DOM. */
.lp-fallback { --lp-w: min(100vw, 78rem); aspect-ratio: auto; display: block;
  padding: clamp(6rem, 16vh, 9rem) var(--gutter) clamp(3rem, 8vh, 5rem); }
.lp-fallback .lp-stage { display: none; }
.lp-fallback .lp-ui { display: block; }
.lp-fallback .lp-ui > *, .lp-fallback .lp-cta > * { position: static; }
.lp-fallback .lp-title {
  width: auto; font-size: var(--t-hero); line-height: var(--lh-tight); color: var(--ink);
}
.lp-fallback .lp-title .accent-word { color: var(--accent); }
.lp-fallback .lp-lede { width: auto; margin-top: var(--s-5); font-size: var(--t-label); color: var(--graphite); }
.lp-fallback .lp-cta { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-4); margin-top: var(--s-6); inset: auto; }
.lp-fallback .lp-btn { width: auto; height: auto; padding: 0.85em 1.4em;
  font-size: var(--t-body); background: var(--accent); color: var(--paper-hi); }
.lp-fallback .lp-alt { width: auto; height: auto; font-size: var(--t-body); color: var(--ink);
  text-decoration: underline; text-underline-offset: 0.35em; }
.lp-fallback .lp-verbs { margin-top: var(--s-6); gap: clamp(0.9rem, 2vw, 1.8rem);
  font-size: var(--t-label); opacity: 1; }
.lp-fallback .lp-verbs li + li::before { left: -0.9rem; width: 3px; }

/* ── NARROW SCREENS REFLOW, THEY DO NOT SHRINK (≤ 900px) ──────────────────
   A 16:9 film contained in a portrait viewport is a thin strip over a lot of
   empty paper, and the desktop answer — register everything against the frame —
   would then be a photograph of a website at about a third of readable size.
   That is the exact failure this breakpoint exists to avoid.

   So the frame becomes a TOP-ANCHORED BAND of its own top 73% (aspect 2.44),
   and that number is not a taste value: the take's baked lede begins at y
   72.92%, so 73% is the last row that keeps the runner, the whole headline and
   the whole ochre sweep while leaving every baked word below it OUT OF FRAME.
   Everything the crop drops — the lede, both buttons — is then supplied by real
   DOM at a real size, in a real reading order, with nothing duplicated: the
   generated copies are not on screen to duplicate.

   The headline alone stays registered over the band, because it is the one
   piece of type the crop keeps. --ch: 0.7307 rescales its y for the band. */
@media (max-width: 900px) {
  /* 35.54 vh/s × 6.00s = 213vh travel, + the pin — the same rate the AI CONTENT
     beat uses at this width. Derived with the desktop number, never alone. */
  .opening { height: 313vh; }
  .lp-pin {
    place-items: stretch;
    /* ── START, NOT CENTRE — this is what registers the seam on narrow ──────
       Centred, the band's y depends on how tall the DOM rows under it happen to
       be, because the whole group is centred as one. The bridge pin has no such
       rows, so it centred a lone band and the same picture landed 66px lower on
       a tablet and 49px lower on a phone at the exact handover. Measured, both
       bands were the right SIZE (768x316 / 390x160) and the wrong PLACE.

       Anchored to the top with a shared pad, both bands start at the same
       offset by construction and the rows simply flow beneath. --pin-pad is
       read by .bridge-pin too, so the two can never drift apart again. */
    align-content: start;
    --pin-pad: clamp(5rem, 13vh, 7rem);
    padding-block: var(--pin-pad) clamp(3rem, 8vh, 4.5rem);
    --lp-w: 100vw;
  }
  .lp {
    width: 100%; aspect-ratio: auto;
    grid-template-rows: auto auto auto auto;
    row-gap: clamp(1.1rem, 3.5vw, 1.9rem);
  }
  /* the band's aspect is the frame's DIVIDED by the crop it keeps:
     1.7778 / 0.7307 = 2.433. It must match `aspect.narrow` in js/opening.js —
     that value is what makes the player take a 73%-tall window, and this one is
     what gives the window somewhere to land. THE BRIDGE SHARES THIS EXACT
     NUMBER (.bridge-stage below, and aspect.narrow in js/bridge.js): the two
     beats hand over on one geometry, so all four places move together. */
  .lp-stage { grid-area: 1 / 1; aspect-ratio: 2.433; }
  .lp-ui { display: contents; --ch: 0.7307; }

  /* the one registered element left: it sits in the band's own cell */
  .lp-title { grid-area: 1 / 1; position: absolute; }

  /* everything else becomes ordinary, readable, full-size DOM */
  .lp-lede, .lp-cta, .lp-verbs { position: static; left: auto; top: auto; width: auto; }
  .lp-lede {
    grid-row: 2; padding-inline: var(--gutter);
    font-size: var(--t-label);
  }
  .lp-cta {
    grid-row: 3; padding-inline: var(--gutter);
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-4);
    inset: auto; pointer-events: auto;
  }
  .lp-cta > * { position: static; }
  .lp-btn {
    width: auto; height: auto;
    padding: 0.85em 1.4em; font-size: var(--t-body);
    background: var(--accent); color: var(--paper-hi);
  }
  .lp-alt {
    width: auto; height: auto;
    font-size: var(--t-body); color: var(--ink);
    text-decoration: underline; text-underline-offset: 0.35em;
    text-decoration-color: var(--line);
  }
  /* ── UN-GHOSTING, AND WHY IT HAS TO BE WRITTEN LIKE THIS ────────────────
     Below this breakpoint the crop leaves the take's own lede and buttons out
     of frame, so there are no generated pixels for these three to hide behind:
     they are ordinary controls and must carry real ink. The obvious way to say
     that — a plain `.lp-btn { background: var(--accent) }` in here — SILENTLY
     LOSES, because `.lp-ghost.lp-btn` is two classes to its one and specificity
     beats source order. That shipped once as a "Start creating" with no fill
     and near-invisible text on every phone and tablet. Match the ghost rule's
     specificity or it does not apply. Only .lp-title stays ghosted here, since
     the headline is the one piece of type the band keeps. */
  .lp-lede.lp-ghost { color: var(--graphite); }
  .lp-btn.lp-ghost  { color: var(--paper-hi); background: var(--accent); }
  .lp-alt.lp-ghost  { color: var(--ink); }
  .lp-ui[data-live="0"] :is(a, button) { visibility: visible; }
  .lp-cta { pointer-events: auto; }

  /* THE PHONE ASSEMBLES TOO, ON THE TAKE'S OWN BEAT. These rows are outside the
     film, so left alone they would be a finished page sitting under a blank
     canvas for the four seconds before the artwork catches up. --ui-body is
     written by js/opening.js across the exact window in which the take paints
     its own lede and buttons (u 0.65-0.72), so the words arrive here at the
     moment they arrive on desktop — where they are the generated pixels
     themselves. Same beat, different mechanism. */
  .lp-lede, .lp-cta, .lp-verbs { opacity: var(--ui-body, 1); }
  .lp-verbs {
    grid-row: 4; padding-inline: var(--gutter);
    gap: clamp(0.9rem, 4vw, 1.8rem); font-size: var(--t-label);
    /* NO `opacity: 1` HERE. It was overriding the --ui-body rule above on equal
       specificity by being later in the block, so the verbs were the one row
       that sat there fully lit under a blank canvas from the very first frame
       while the lede and buttons correctly waited for the take's own beat. */
  }
  .lp-verbs li + li::before { left: calc(-0.5 * clamp(0.9rem, 4vw, 1.8rem) - 1px); width: 3px; }
  .scrollcue { display: none; }
}
/* 31.59 vh/s × 6.00s = 190vh travel, + the pin. Third of the three rates. */
@media (max-width: 560px) { .opening { height: 290vh; } }

/* ═══ 1b · THE PAINT BRIDGE — the homepage becomes AI CONTENT ══════════════
   The approved bridge take, scrubbed on the same transport as everything else.
   It carries no typography of its own in the DOM: the landing's registered type
   is in the pin above and has scrolled away, AI CONTENT's real section head is
   below and has not arrived, and the take clears its own generated words off the
   top of frame before its last sampled second.

   THE SPAN IS SHORT ON PURPOSE. 130vh of travel against the opening's 264vh —
   about 27 vh/s rather than the 44 vh/s the chapters use. A join is not a
   chapter: it has to feel like the page giving way, so every wheel notch has to
   move real paint. At 130vh over 60 frames a 100px notch advances ~5 frames.
   The take's own static head was trimmed in extraction rather than being given
   cheap scroll here, so there is no dead travel at either end to absorb. */
.bridge {
  position: relative;
  /* ── THE SEAM IS A HANDOVER AT ONE SHARED RESTING POSITION ────────────────
     Both pins park their 16:9 canvas at exactly the same place — measured
     1440x810 at x 0, y 45 in a 900px viewport, because both are a contained
     16:9 box centred in a 100svh sticky pin. The boxes were never the problem.

     THE PROBLEM WAS A FULL VIEWPORT OF SCROLL BETWEEN THEM. .opening's travel
     ended at scrollY 2376 while .bridge's pin could not engage until its own top
     reached 0 at scrollY 3276 — a 900px gap in which the outgoing frame slid
     from y 45 up to y -955 while the incoming one climbed from y 845 to y 45.
     Same image, same size, never on screen together: the seam read as the page
     scrolling off and a new one arriving, which is exactly what it must not do.

     Pulling .bridge up by one pin height makes its sticky engage on the very
     scroll offset where .opening's travel ends, so the handover happens with
     both canvases at that shared resting position and neither ever moves. It
     also removes 900px of dead scroll. The -100vh fallback matters on phones,
     where svh and vh differ — the margin has to cancel the pin height in the
     SAME unit the pin was sized in. */
  margin-top: -100vh;
  margin-top: -100svh;
  /* 70vh travel + 100vh pin. The beat is 2.54s now — it stops on the boundary
     frame the owner chose partway through the take rather than running to the
     take's own end — so the span came down with it (it was 130vh for 4.64s).
     Same ~27 vh/s as before: a join has to give way under every notch, and a
     span sized for a longer beat would be dead travel at the tail. */
  /* 190vh travel + 100vh pin, at the usual ~26 vh/s for 7.31s of motion
     (1.23-8.54s of the 070542 take).

     IT STOPS AT SOURCE FRAME 206 ON PURPOSE. From frame 210 the take begins
     baking its OWN page chrome — measured, nav ink jumps 1.9% -> 4.3-7.5% — and
     those last frames carry a second MARBLELENS/HOME/ABOUT/CONTACT bar under the
     real header plus garbled generated type ("Esserl, one idea coonc, adive.").
     Ending on 206 leaves the settled bottle composition, which has no baked
     chrome at all, and the real AI CONTENT chapter below supplies the heading in
     real DOM as it always has. No still was substituted to achieve this; the
     span simply ends on an approved frame. */
  height: 290vh;
  /* NO BACKGROUND ON THE SECTION — it would cover the beat above it.
     .bridge is positioned and comes after .opening in the DOM, so with the
     negative margin its own background painted ABOVE .lp-pin's canvas for the
     whole overlapping viewport: the landing's last screenful of film rendered
     as flat paper. Measured in the hit-test stack as `bridge` sitting above
     `ml-seq ml-landing`. The ground belongs to the pin, and only once the pin
     has actually taken over. */
}
.bridge.is-armed .bridge-pin { background: #ebe4d8; }
.bridge-pin {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  display: grid; place-items: center;
  overflow: hidden;
  --bw: min(100vw, calc(100svh * 16 / 9));
}
/* invisible until its pin has actually reached the resting position — see the
   ownership block in js/bridge.js. Unpinned it sits partway down the screen
   during the landing's last viewport and would paint over the live beat. */
/* 2.0, not 16/9 — the take's baked browser chrome is cut at extraction, so the
   shipped frame is 1280x640. Must match wideAspect in js/bridge.js. */
.bridge-stage { position: relative; width: var(--bw); aspect-ratio: 2; overflow: hidden; visibility: hidden; }
.bridge.is-armed .bridge-stage { visibility: visible; }

/* the same cell and the same box as the stage, so the verbs sit on exactly the
   coordinates they occupied a pixel earlier in the landing's pin. Everything
   about their look comes from .lp-verbs — one definition, two pins. */
.bridge-stage, .bridge-ui { grid-area: 1 / 1; }
.bridge-ui {
  position: relative; width: var(--bw); aspect-ratio: 16 / 9;
  container-type: inline-size;
  --ch: 1;
  pointer-events: none;
  visibility: hidden;
}
.bridge.is-armed .bridge-ui { visibility: visible; }
.bridge-stage .ml-bridge { display: block; width: 100%; height: auto; }

/* ── NARROW SCREENS CROP WIDTH, NOT HEIGHT — the opposite of the opening ──
   The landing take's band keeps its top 73% because its subject is laid out
   ACROSS the frame. This take's subject TRAVELS DOWN it: the shear, the drips
   and the window opening are all vertical events, so height is the axis that
   must survive and width is what gives.

   A full-height 70% centre crop, pushed 5% left so the take's own kicker and
   "Watch one idea come alive." (which start at x 0.12) stay inside it while the
   drips and the torn window (out to x 0.82) stay in too. 1.7778 x 0.70 = 1.2444,
   and that number must match `aspect.narrow` in js/bridge.js. */
@media (max-width: 900px) {
  .bridge { height: 265vh; }
  .bridge-pin {
    --bw: 100vw;
    /* the landing's narrow placement, to the pixel — same anchor, same pad */
    place-items: stretch; align-content: start;
    --pin-pad: clamp(5rem, 13vh, 7rem);
    padding-block: var(--pin-pad) clamp(3rem, 8vh, 4.5rem);
  }
  /* the pour-and-bottle window — see narrowAspect in js/bridge.js */
  .bridge-stage { width: 100%; aspect-ratio: 1.16; }
  /* narrow puts the verbs in flow under the band, exactly as the landing does,
     so the row keeps its place across the seam here too */
  .bridge-ui { display: contents; --ch: 0.7307; }
  .bridge-ui .lp-verbs { grid-row: 2; }
}
@media (max-width: 560px) { .bridge { height: 250vh; } }
/* the continuation is 3.25s against the bridge's 2.71s, so it gets proportional
   travel at the same ~26 vh/s. Same three breakpoints, derived together. */
/* 120vh travel, deliberately TIGHTER than the ~26 vh/s the other beats use.
   This take is 6.96s but its motion is gentle — peak interframe delta 1.33
   against 12-24 elsewhere — so at the house rate it would be 1.8 screens of
   almost no visible change, which is dead scroll by any other name. Compressed
   to ~17 vh/s every notch moves the pour and the fill visibly. */


/* nothing to scrub, nothing to travel through — same law as the opening */
.bridge.is-static { height: auto; }
.bridge.is-static .bridge-pin { position: static; height: auto; }
.bridge.bridge-off { display: none; }
/* SEQUENCE OWNERSHIP. The two sections now overlap by one viewport, so once the
   bridge has moved off its first frame the landing pin is still down there
   sliding away underneath it. The bridge paints over it (later in DOM order,
   both sticky, both z-index auto) but "covered" is not "gone" — a rounding gap
   at an edge would show a second composition drifting. Hidden from the first
   step instead, and restored the moment progress returns to 0, so reverse is
   unaffected: this is read from progress, never latched. */
/* whoever owns the ground hides every beat above it, so nothing older is left
   sliding away underneath. Explicit per beat rather than clever: there are two
   of them and the list is the whole story. */
.bridge-owns .lp-stage { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .bridge { height: auto; }
  .bridge-pin { position: static; height: auto; }
}

/* ── DROP THE PIN WHEN THERE IS NOTHING TO SCRUB ──────────────────────────
   Reduced motion gets the finished composition and a live page immediately
   (js/opening.js seeks straight to the last frame), and the fallback has no
   sequence at all. In both cases the travel would be three empty screens the
   visitor has to scroll past to reach the site — the same trap .canvas-stage
   avoids with .is-static. Height collapses to the content. */
.opening.is-static { height: auto; }
.opening.is-static .lp-pin { position: static; height: auto; }
.lp-fallback-host { height: auto !important; }
.lp-fallback-host .lp-pin { position: static; height: auto; overflow: visible; }
@media (prefers-reduced-motion: reduce) {
  .opening { height: auto; }
  .lp-pin { position: static; height: auto; }
}


/* ═══ SHARED · the media window ════════════════════════════════════════════
   Every picture on the page is mounted behind a paper window. Until its file
   decodes the window keeps a developing-tray hatch — never a broken image, so
   this ships before the footage exists (§40, media slots ship empty). */
.mw {
  position: relative; overflow: hidden;
  background: var(--paper-deep); border-radius: var(--radius-sm);
  box-shadow: var(--sh-press);
}
.mw::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background:
    radial-gradient(120% 90% at 30% 0%, rgba(255,255,255,0.22), transparent 60%),
    repeating-linear-gradient(115deg, var(--paper-deep) 0 10px, var(--paper-lo) 10px 20px);
  animation: develop 2.4s var(--ease-out) infinite alternate;
}
.mw.loaded::after { display: none; }
.mw img, .mw video {
  position: relative; z-index: 1; width: 100%; height: 100%;
  object-fit: cover; opacity: 0;
  /* stepped, not a smooth dissolve — a print arriving, in held frames */
  transition: opacity 0.5s steps(5, end);
}
.mw.loaded img, .mw.loaded video { opacity: 1; }

/* ═══ 2 · AI CONTENT → BUILD YOUR WEBSITE — THE FILM ═══════════════════════
   ONE APPROVED TAKE, SCRUBBED. Everything this beat shows is decoded source
   pixels from hf_20260815_103834 (30.04s, 721 frames), extracted into four
   sequences by site/tools/extract-film.py and driven by js/film.js.

   WHAT THIS BLOCK REPLACED. It used to be ~600 lines describing a CODED
   depiction of the same idea: a graphite construction on a 2D canvas, a
   photograph arriving in six torn <div>s with their own crossfade, a <video>
   under them, reference scraps that grew into a headline, and a drawn "website"
   of bars and cards — plus a second set of rules whose only job was to take all
   of that back out again once the approved take was mounted (.has-seq,
   .shot01-owns). With the take carrying the entire arc, none of it has anything
   left to depict, and every one of those layers was a way for retired imagery
   to sit underneath the frame. They are gone from the markup, so there is
   nothing here to hide.

   TWO CUSTOM PROPERTIES, BOTH PURE FUNCTIONS OF SCROLL. js/film.js writes
   --hand on .canvas-stage (so the sheet and the page overlay both inherit one
   value); the transport writes nothing but pixels. CSS owns every look. */

.canvas-stage {
  --hand: 0;
  position: relative;
  /* THE TRAVEL, AND WHY IT IS 375vh SHORTER THAN THE BEAT IT REPLACED.
     The old beat ran 1075vh for 24.44 cost-seconds — 44.0 vh per second, at a
     flat 10fps, which put ~2.5 frames under a wheel notch.

     This take is LONGER (30.04s against 28.48s) and this span is SHORTER, and
     both come from the same measurement: half of this film is held. Eight
     static runs total 14.82s, so the extractor samples motion-adaptively (2fps
     in a hold, 20fps in the burger assembly) and the preset weights each beat
     by its own frame count. The result is that every shipped frame owns an
     equal slice of scroll and a 2.6s hold costs five frames of travel instead
     of twenty-six. There is no dead region left to pay for.

     700vh = 600vh of travel over 24.2 cost units — 28.9 vh per unit, and ~2.5vh
     (≈22px at a 900px viewport) under every one of the 226 frames. The whole
     film is now 6.4 fewer screen-heights than the beat that showed less of it.
     Re-derive all three breakpoints together if anything changes; scaling one
     alone silently makes the phone scrub at a different speed from the desktop. */
  height: 700vh;
  margin-top: var(--sec-gap);
}
.canvas-travel { position: absolute; inset: 0; pointer-events: none; }
.canvas-sticky {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  /* THE SHEET'S WIDTH LIVES HERE, not on .canvas-frame, because .cv-hand has to
     resolve to exactly the same number. It is a sibling of the sheet sharing the
     sheet's grid cell (see .cv-hand), which is what lets the same element be a
     pixel-registered overlay on desktop and real stacked rows on a phone — but
     only if both read one definition of the width. Change it in one place. */
  --sheet-w: min(100%, calc((100vh - 11.5rem) * 4 / 3), 1080px);
  /* auto rows, not 1fr: a 1fr first row pushes the legend to the floor and
     leaves a dead band under the sheet on short/narrow screens */
  display: grid; grid-template-rows: auto auto;
  align-content: center; justify-items: center;
  gap: clamp(0.8rem, 2vh, 1.6rem);
  padding: clamp(4.2rem, 9vh, 6rem) var(--gutter) clamp(1.2rem, 4vh, 2.4rem);
}
/* the film and the page it becomes occupy ONE cell; the legend follows */
.canvas-sticky > .canvas-frame,
.canvas-sticky > .cv-hand   { grid-area: 1 / 1; }
.canvas-sticky > .canvas-legend { grid-area: 2 / 1; }

/* ── THE SHEET ────────────────────────────────────────────────────────────
   A real sheet on a table: paper stock, a cut edge, and a shadow that falls the
   way a mounted print's does.

   4/3 IS THE SOURCE'S OWN SHAPE (1112x834 = 1.3333), so the frame fills this
   box exactly and there is no letterbox at any width — nothing is stretched,
   nothing is cropped, and no band of paper can show through at an edge. The
   previous beat's takes were 1.360 and this box was 4/3, and that 1.96% gap is
   what every registration comment in the old block was working around.

   NO CUTTING-MAT GRID, NO --lift. The mat existed to be drawn ON; the film
   covers this box at every progress, so a printed grid underneath it is a layer
   that can only ever be revealed by a mistake. The lift belonged to a sheet
   that reacted to a coded animation. */
.canvas-frame {
  position: relative; margin: 0;
  width: var(--sheet-w);
  aspect-ratio: 4 / 3;
  background: var(--paper-hi);
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.5) inset,
    0 28px 56px -30px rgba(28, 20, 10, 0.56),
    0 6px 14px -10px rgba(28, 20, 10, 0.4);
}

/* ── THE TAKE ─────────────────────────────────────────────────────────────
   SCOPED TO THIS SHEET, because .ml-seq is not unique to it — the same engine
   mounts the landing opening. Left unscoped this block reaches into that
   canvas and applies a centring and an opacity gate that belong here, and the
   symptom is a landing that looks blank while its canvas is provably full of
   pixels. This file has been bitten by that collision twice.

   NO `opacity: var(--seq)` ANY MORE. That gate existed so the take could be
   faded in over the coded layers it replaced. There are no coded layers. The
   film is simply the sheet's content, present from its first frame — which also
   removes the failure where the beat opened at zero alpha on top of the
   composition it was supposed to have replaced. */
.canvas-frame .ml-seq {
  position: relative; display: block;
  width: 100%; height: auto;
  z-index: 1; pointer-events: none;
}

/* ── THE HANDOFF · generated website → actual website ─────────────────────
   The take ends by reorganising its own material into a finished website and
   then holding on it for 2.6s. The DOM's job in that window is to make that
   page REAL — focusable, routable, readable by assistive tech — without
   painting a second copy of anything.

   THIS TAKE BAKES ITS OWN TYPE, WHICH THE PREVIOUS ONE DID NOT, and that is the
   whole reason this block works differently. Shot 03 left deliberate blank
   zones for DOM words. This take sets the kicker, the headline and the button
   itself, in pixels. So the DOM here is TRANSPARENT INK on the take's own
   glyphs (.cv-ghost) rather than visible type in a gap: exactly one visible
   rendering of every word, nothing to double, and an activation that changes no
   pixel at all — it only makes the page live. Identical mechanism to .lp-ghost
   on the landing opening, which solves the identical problem.

   EVERY NUMBER BELOW WAS MEASURED OFF THE TAKE'S FINAL FRAME (source frame 721,
   1112x834), by an ink scan of the left half at a luma threshold of paper-38:

     kicker    "05 BUILD YOUR WEBSITE"   x 10.70-26.08%   y 23.38-24.46%
     headline  "Ideas become"            x  9.26-45.86%   y 31.77-38.61%
               "living websites."        x  9.26-48.83%   y 41.01-49.64%
     CTA box   "Build Your Website ->"   x  8.63-27.34%   y 56.59-63.07%

   The card the take draws on the right (x 44.96-100%) is a depiction of a
   CLIENT's website, not this site's navigation — the real navigation is the
   page header, which stays live and hittable through the entire pin. Nothing is
   registered over the card, for the same reason nothing draws a box here: a
   second copy offset by a few pixels reads as a sticker.

   TYPE SCALES IN cqw, NOT vw. These are percentages of the CANVAS, and the
   canvas is min(100%, (100vh - 11.5rem) * 4/3, 1080px) — on a short laptop it is
   driven by viewport HEIGHT, and a vw-based size decouples from the very box it
   has to sit inside. inline-size containment makes 1cqw exactly 1% of the
   canvas width, which is the unit every measurement above is already in. */
.cv-hand {
  /* grid-area 1/1 puts it in the sheet's own cell (see .canvas-sticky), so it
     inherits the sheet's centring for free. Same width + the source's aspect =
     exactly the canvas box, with nothing measured and nothing to drift. */
  width: var(--sheet-w);
  aspect-ratio: 4 / 3;
  align-self: center; justify-self: center;
  position: relative;
  container-type: inline-size;
  z-index: 6; pointer-events: none;
  color: var(--ink);
}
.cv-hand > * { position: absolute; margin: 0; }

/* ── .cv-ghost — REGISTERED ON PIXELS THE TAKE HOLDS ──────────────────────
   `color: transparent` rather than opacity or visibility, deliberately: the
   element stays hit-testable, focusable and readable by assistive tech, which
   `visibility: hidden` and `opacity: 0` would both cost. */
.cv-ghost { color: transparent; }
/* interaction still has to be legible, and it cannot be legible in ink that is
   not there — so hover and focus are drawn AROUND the generated glyphs, never
   over them. A ring on the take's own measured box reads as the generated
   button lighting up, because it is exactly the generated button's edge. */
.cv-hand[data-live="1"] .cv-ghost:is(:hover, :focus-visible) {
  outline: 1.5px solid var(--accent-ink); outline-offset: 0.5cqw;
  border-radius: var(--radius-sm);
}

/* ── THE ACTIVATION IS SEQUENTIAL, AND ON DESKTOP IT IS ONLY PRESENCE ─────
   --hand runs 0→1 across the hold (js/film.js). Each element takes its own
   slice of that one value, so the page becomes live in reading order. No
   transform, no blur, no fade-UP: the frame underneath is a photograph of
   paper. It is a pure function of scroll, so scrubbing back un-resolves it in
   the same order — which is why there is no transition either. */
.cv-hand > *            { opacity: clamp(0, calc((var(--hand, 0) - 0.00) / 0.34), 1); }
.cv-hand .hand-title    { opacity: clamp(0, calc((var(--hand, 0) - 0.22) / 0.34), 1); }
.cv-hand .hand-cta      { opacity: clamp(0, calc((var(--hand, 0) - 0.50) / 0.34), 1); }

.cv-hand .hand-kicker {
  left: 10.7%; top: 22.9%;
  font-family: var(--mono); font-size: 1.05cqw; line-height: 1;
  letter-spacing: 0.2em; text-transform: uppercase;
}
/* 7.3cqw is back-solved from the baked cap height: line one measures 6.84% of
   834px = 57px, and this serif's cap height is ~0.70em. The 0.95 line-height is
   the measured pitch between the two baked lines (9.24% of frame height = 77px)
   over that same em. Re-measure if the take is ever regenerated. */
.cv-hand .hand-title {
  left: 9.26%; right: 48%; top: 30.4%;
  font-family: var(--serif); font-weight: 500;
  font-size: 7.3cqw; line-height: 0.95; letter-spacing: -0.005em;
}
.cv-hand .hand-title span { display: block; }
/* THE OUTLINE IS THE TAKE'S — this only fills it. It is a real <button>, so it
   has to shed every default a button carries (background, border, radius, the
   UA font) or it paints a second control on top of the one the take drew.
   pointer-events is withheld until the reveal has actually finished: a control
   you cannot see but can still click is worse than no control, and during the
   scrub this element is somewhere between absent and present. */
.cv-hand .hand-cta {
  left: 8.63%; right: 72.66%; top: 56.59%; height: 6.48%;
  display: flex; align-items: center; justify-content: center; gap: 0.6em;
  background: none; border: 0; border-radius: 0; padding: 0; color: transparent;
  font-family: var(--sans); font-weight: 600; font-size: 1.35cqw;
  letter-spacing: 0.01em; cursor: pointer;
}
.cv-hand[data-live="1"] .hand-cta { pointer-events: auto; }

/* ══ NARROW SCREENS · ONE MECHANISM, NOT TWO ══════════════════════════════
   THIS BLOCK ONCE DISSOLVED .cv-hand INTO REAL ROWS UNDER THE FRAME, on the
   assumption that the take's own type is unreadable at phone size the way the
   landing opening's is. MEASURED, IT IS NOT. The baked headline's cap height is
   6.84% of frame height, the frame is always the full viewport width, and the
   frame is 4:3 — so the headline renders at 0.0513 x viewport width: 39px at
   768, 20px at 390, 16px even at 320. Visible DOM copies of those words under
   the frame were therefore the same sentence twice on screen at once, which is
   the one thing this handoff forbids. The ghost stays a ghost at every width.

   WHAT ACTUALLY CHANGES ON A PHONE IS THE BOX AND THE TAP TARGET. The gutter
   goes, so the frame is as large as a 4:3 crop of a 390px viewport can be; and
   the take's button is 73x19 CSS px there, well under a 44px target, so the
   control keeps its measured box for ink and focus and grows its HIT AREA
   around it.

   AND THE REFLOW STILL HAPPENS — one screen later, on purpose. "Generated
   website becomes actual website" resolves inside the frame on a wide screen,
   where the frame is big enough to be a page. On a phone it resolves at the pin
   release, into .live-site (css/build.css): real reading-order reflow, eyebrow
   → headline → media → copy → control, at real reading sizes. Same handoff, and
   the honest mechanism for each width, rather than a miniature page pretending
   to be an interface. */
@media (max-width: 820px) {
  /* full bleed: at 390px a 4:3 frame is 292px tall, and every pixel of gutter
     is taken off both the width AND, at 3:4, off that height */
  .canvas-sticky { padding-inline: 0; padding-top: 4.4rem; }
  .canvas-legend { padding-inline: var(--gutter); }

  /* the take's button is 18.7% x 6.48% of the frame — 73x19px at 390. The ink
     and the focus ring stay on that measured box (anything else draws a control
     the take did not draw); only the hit area grows, centred, to a real target.
     Nothing else in the sheet is hittable, so the overspill costs nothing. */
  .cv-hand .hand-cta::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: 100%; height: 100%; min-width: 44px; min-height: 44px;
  }
}

/* ── METADATA · annotation, never the storyteller ─────────────────────────
   It has no business sitting on the finished page, so it is gone by the time
   the handoff resolves — driven by the same one value as the handoff itself. */
.cv-meta {
  position: absolute; z-index: 6; right: 10px; bottom: 8px;
  font-family: var(--mono); font-size: 0.55rem;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--graphite); opacity: calc(0.55 * (1 - var(--hand, 0)));
  background: color-mix(in srgb, var(--paper-hi) 74%, transparent);
  padding: 0.24em 0.5em;
}
@media (max-width: 820px) { .cv-meta { display: none; } }

/* the offer, in words, for anyone who never watches the animation */
.canvas-legend {
  max-width: 68ch; text-align: center;
  font-size: var(--t-small); color: var(--graphite);
}
.canvas-legend b { color: var(--ink-2); font-weight: 600; }

/* the same ~29 vh per cost unit, re-derived per breakpoint rather than scaled
   by eye — a phone viewport is shorter, so the same vh buys fewer pixels */
@media (max-width: 900px) { .canvas-stage { height: 620vh; } }
@media (max-width: 420px) { .canvas-stage { height: 560vh; } }

/* NOTHING TO SCRUB, NOTHING TO TRAVEL THROUGH. Reduced motion gets the take's
   resolved final composition with the page already live inside it (js/film.js
   seeks straight to 1), and the sequence-unavailable path gets the legend. In
   both cases the pin would otherwise be seven empty screens to scroll past. */
.canvas-stage.is-static { height: auto; }
.canvas-stage.is-static .canvas-sticky { position: static; height: auto; padding-block: 2rem; }
@media (prefers-reduced-motion: reduce) {
  .canvas-stage { height: auto; }
  .canvas-sticky { position: static; height: auto; padding-block: 2rem; }
}

/* ═══ 3 · BUILD — retained pieces ═════════════════════════════════════════
   The chapter's own composition now lives in css/build.css. What stays here
   is only what other things still use. */

/* ── STARTING DIRECTIONS (§24) — curated systems, not marketplace cards ────*/
.directions { margin-top: var(--s-7); }
.directions .h3 { margin-top: var(--s-2); margin-bottom: var(--s-5); }
.dir-grid { list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--col-gap); grid-template-columns: repeat(5, 1fr); }
.dir {
  padding: var(--s-4) var(--s-4) var(--s-5); cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-mount),
              box-shadow var(--dur-fast) var(--ease-out);
}
.dir:hover, .dir:focus-visible { transform: translateY(-4px); box-shadow: var(--sh-lift); }
.dir.picked { box-shadow: var(--sh-lift), 0 0 0 2px var(--accent); }
.dir-k { font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.18em;
  color: var(--graphite-2); display: block; margin-bottom: var(--s-3); }
.dir b { display: block; font-family: var(--serif); font-size: 1.02rem;
  color: var(--ink); line-height: 1.25; }
@media (max-width: 1100px) { .dir-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 700px)  { .dir-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 1000px) { .proof { grid-template-columns: 1fr; } }
@media (max-width: 420px) {
  /* one per row on a phone, so the cards become a LIST rather than five tall
     empty boxes — with their descriptions gone there is nothing to fill them */
  .dir-grid { grid-template-columns: 1fr; gap: 0.5rem; }
  .dir { display: flex; align-items: baseline; gap: var(--s-3);
    padding: var(--s-3) var(--s-4); }
  .dir-k { margin-bottom: 0; }
  .dir b { font-size: 1rem; }
  .dir:hover, .dir:focus-visible { transform: none; }
}

/* ═══ 4 · HEADSHOTS — CROP / FRAME / MOUNT / REVEAL ════════════════════════
   The quiet chapter (§25). Contact-sheet language, crop marks, one frame
   selected and mounted. Deliberately NOT another assembly sequence. */
.contact { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center; }
.contact-sheet {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-3);
  padding: var(--s-3); background: var(--paper-hi);
  border: 1px solid var(--line); box-shadow: var(--sh-mount);
}
.contact-sheet .frame { margin: 0; position: relative; }
.contact-sheet .frame .mw { aspect-ratio: 4 / 5; filter: saturate(0.5) contrast(0.95); }
/* the editor's crop marks, drawn in on the chosen frame */
.contact-sheet .crop { position: absolute; inset: 6px; z-index: 2; pointer-events: none;
  opacity: 0; transition: opacity var(--dur) var(--ease-out); }
.contact-sheet .crop::before, .contact-sheet .crop::after {
  content: ""; position: absolute; width: 16px; height: 16px; border: 1.5px solid var(--accent);
}
.contact-sheet .crop::before { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.contact-sheet .crop::after { bottom: 0; right: 0; border-left: 0; border-top: 0; }
.contact-sheet .frame.sel .mw { filter: none; }
.contact-sheet .frame.sel .crop { opacity: 1; transition-delay: 0.5s; }
.contact-sheet .frame.sel::after {
  content: "SELECT"; position: absolute; z-index: 3; left: 6px; bottom: 6px;
  font-family: var(--mono); font-size: 0.52rem; letter-spacing: 0.16em;
  color: var(--paper-hi); background: var(--accent); padding: 0.25em 0.5em;
  opacity: 0; animation: mountPart 0.3s steps(3, end) 0.7s forwards;
}
.contact-mount .mw, .contact-mount .plate { aspect-ratio: 4 / 5; }
/* the selected frame, enlarged and physically mounted on board */
.contact-mount .mounted {
  padding: clamp(10px, 1.6vw, 18px); background: var(--paper-hi);
  border: 1px solid var(--line); box-shadow: var(--sh-lift);
  border-radius: var(--radius-sm);
}
.contact-mount .tag { display: block; margin-top: var(--s-3); }
@media (max-width: 860px) { .contact { grid-template-columns: 1fr; } }

/* ═══ 5 · STORY / VIDEO — SEQUENCE / TRACK / EDIT / PLAY ═══════════════════
   Storyboard strips mounted on a board, resolving into motion (§26). */
.story-flow { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
.board { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.board .cell {
  position: relative; display: grid; grid-template-columns: auto 1fr;
  gap: var(--s-3); align-items: center;
  padding: var(--s-2); background: var(--paper-hi);
  border: 1px solid var(--line); box-shadow: var(--sh-pin);
}
/* the strip is TRACKED — each cell steps in from the left, like film advancing */
.board .cell[data-in] { transform: translateX(-16px); }
.board .cell[data-in].in { transform: none; }
.board .cn { font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.14em;
  color: var(--graphite-2); writing-mode: vertical-rl; padding: 0.4em 0; }
.board .cell-img { aspect-ratio: 16 / 9; }
.board .cell-img .mw { height: 100%; }
.story-play .reel, .story-play .mw { aspect-ratio: 16 / 10; }
.story-play .reel { padding: 0; }
.story-play .tag { display: block; margin-top: var(--s-3); }
/* sprocket holes down the edge of the finished sequence */
.story-play { position: relative; }
.story-play::before {
  content: ""; position: absolute; left: -14px; top: 0; bottom: 0; width: 6px;
  background-image: repeating-linear-gradient(var(--graphite-2) 0 7px, transparent 7px 18px);
  opacity: 0.32; border-radius: 3px;
}
@media (max-width: 860px) {
  .story-flow { grid-template-columns: 1fr; }
  .story-play::before { display: none; }
}

/* ═══ 6 · SELECTED WORK — MOUNT / STACK / PEEL / PRESENT ═══════════════════
   A private viewing table, not a card grid (§27). Pieces are laid at slight
   angles and straighten as they are placed — someone put them there. */
.worktable {
  display: grid; gap: clamp(1rem, 2.4vw, 2rem);
  grid-template-columns: repeat(6, 1fr);
  grid-auto-rows: minmax(0, auto);
}
.piece { margin: 0; position: relative; background: var(--paper-hi);
  padding: clamp(9px, 1.1vw, 14px) clamp(9px, 1.1vw, 14px) 0;
  border: 1px solid var(--line); box-shadow: var(--sh-mount);
  transition: transform var(--dur) var(--ease-mount), box-shadow var(--dur) var(--ease-out); }
.piece[data-in] { transform: translateY(26px) rotate(var(--rot, 0deg)); }
.piece[data-in].in { transform: rotate(var(--rest, 0deg)); }
.piece:hover { transform: translateY(-4px) rotate(0deg); box-shadow: var(--sh-lift); z-index: 2; }
.piece .mw, .piece .mini-site { width: 100%; }
/* the responsive-build piece on the viewing table — a small mounted screen.
   (Lives here, not with the canvas: the canvas grows its own product layer.) */
.mini-site {
  aspect-ratio: 16 / 10; background: var(--paper-hi);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--sh-mount); overflow: hidden;
  display: grid; grid-template-rows: auto 1fr;
}
.mini-bar { display: flex; gap: 5px; align-items: center;
  padding: 7px 9px; border-bottom: 1px solid var(--line); background: var(--paper); }
.mini-bar span { width: 6px; height: 6px; border-radius: 50%; background: var(--graphite-2); opacity: 0.6; }
.mini-body { padding: 9%; display: grid; gap: 8%; align-content: start; }
.mini-hero { height: 38%; min-height: 30px; background: var(--accent); opacity: 0.85; border-radius: 3px; }
.mini-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6%; }
.mini-row i { height: 22px; background: var(--paper-deep); border-radius: 3px; }
.piece figcaption {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-3); padding: 0.72em 0.1em 0.85em;
}
.piece figcaption b { font-family: var(--serif); font-weight: 600; font-size: 0.98rem; color: var(--ink); }
.piece figcaption span { font-family: var(--mono); font-size: 0.58rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--graphite-2); }
/* Four pieces, two clean rows, each laid large — a viewing table, not a wall
   of cards. Row one is a portrait beside a wide moving piece; row two is the
   build beside a print. Deliberately uneven within the row, exact across it. */
.p-tall  { grid-column: span 2; --rest: -0.7deg; }
.p-tall  .mw { aspect-ratio: 3 / 4; }
.p-wide  { grid-column: span 4; --rest: 0.4deg; }
.p-wide  .mw { aspect-ratio: 16 / 9; }
.p-site  { grid-column: span 3; --rest: -0.5deg; }
.p-print { grid-column: span 3; --rest: 1.1deg; }
.p-print .mw { aspect-ratio: 16 / 10; }
@media (max-width: 980px) {
  .worktable { grid-template-columns: repeat(4, 1fr); }
  .p-tall, .p-site, .p-print { grid-column: span 2; }
  .p-wide { grid-column: span 4; }
}
@media (max-width: 620px) {
  .worktable { grid-template-columns: 1fr; }
  .p-tall, .p-wide, .p-site, .p-print { grid-column: span 1; }
  /* on a phone a rotated print costs horizontal room it does not have */
  .piece[data-in].in { transform: none; }
}

/* ═══ 7 · PROCESS — STAMP / PIN / MARK / ADVANCE ═══════════════════════════
   Five steps, each leaving a mark on the production board (§28). The red is
   spent ONLY on the stamps — never as a background (§7). */
.board-steps {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--col-gap);
  position: relative;
}
/* the board's own tracking line, drawn through every stamp */
.board-steps::before {
  content: ""; position: absolute; left: 0; right: 0; top: 21px; height: 1px;
  background: var(--line); z-index: 0;
}
.pstep { position: relative; padding-top: var(--s-6); }
.pstep .stamp {
  position: absolute; top: 0; left: 0; z-index: 1;
  width: 42px; height: 42px; display: grid; place-items: center;
  font-family: var(--mono); font-size: 0.68rem; font-weight: 600;
  letter-spacing: 0.04em; color: var(--red);
  border: 1.5px solid var(--red); border-radius: 50%;
  background: var(--sec-bg);
  transform: rotate(-7deg) scale(1.5); opacity: 0;
}
/* a stamp does not fade in. It lands, hard, slightly crooked. */
.pstep[data-in].in .stamp { animation: stampDown 0.28s steps(3, end) var(--d, 0s) forwards; }
@keyframes stampDown {
  0%   { transform: rotate(-11deg) scale(1.55); opacity: 0; }
  60%  { transform: rotate(-6deg) scale(0.94); opacity: 1; }
  100% { transform: rotate(-4.5deg) scale(1); opacity: 1; }
}
.pstep b { display: block; font-family: var(--serif); font-weight: 600;
  font-size: var(--t-h3); color: var(--ink); }
.process-note { display: block; margin-top: var(--s-6); }
@media (max-width: 980px) {
  .board-steps { grid-template-columns: repeat(2, 1fr); row-gap: var(--s-5); }
  .board-steps::before { display: none; }
}
@media (max-width: 560px) { .board-steps { grid-template-columns: 1fr; } }

/* ═══ 8 · PRICING / ENGAGEMENT — LAYER / LIFT / UNFOLD ═════════════════════
   Commission the studio (§30). Large mounted sheets that LIFT, with supporting
   detail that UNFOLDS. No SaaS card borders. No invented prices, ever. */
.engage { align-items: stretch; }
.commission {
  display: flex; flex-direction: column; gap: var(--s-3);
  padding: var(--s-5) var(--s-4) var(--s-4);
  transition: transform var(--dur) var(--ease-mount), box-shadow var(--dur) var(--ease-out);
}
.commission:hover, .commission:focus-visible { transform: translateY(-6px); box-shadow: var(--sh-lift); }
/* the lead engagement sits on a second sheet — layered, not outlined */
.commission.feature::before {
  content: ""; position: absolute; inset: 8px -7px -8px 7px; z-index: -1;
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: var(--sh-mount);
}
.commission .c-k {
  font-family: var(--mono); font-size: var(--t-label);
  letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--accent-ink);
}
.commission h3 { font-size: var(--t-h3); }
.commission > p { font-size: var(--t-small); color: var(--ink-2); }
.c-list { list-style: none; margin: 0; padding: var(--s-3) 0 0; display: grid; gap: 0.5em;
  border-top: 1px solid var(--line); }
.c-list li { position: relative; padding-left: 1.15em; font-size: var(--t-small); color: var(--graphite); }
.c-list li::before { content: ""; position: absolute; left: 0; top: 0.62em;
  width: 6px; height: 1px; background: var(--accent); }
.c-price {
  margin-top: auto; padding-top: var(--s-3);
  font-family: var(--mono); font-size: var(--t-small);
  letter-spacing: 0.08em; color: var(--ink);
}
.c-go { display: inline-flex; align-items: center; gap: 0.5em;
  font-weight: 600; font-size: var(--t-small); color: var(--accent-ink); }
.commission:hover .arr { transform: translateX(3px); }
.c-go .arr { transition: transform var(--dur-fast) var(--ease-out); }
.engage-note { margin-top: var(--s-5); max-width: 62ch; }

/* ═══ 10 · CLOSER — the book closes ═══════════════════════════════════════*/
.closer { padding-block: clamp(6rem, 12vh, 10rem); text-align: center; }
.closer .container { display: grid; justify-items: center; gap: var(--s-4); }
.closer-line { font-size: var(--t-h1); max-width: 18ch; }
.closer .lede { max-width: 52ch; text-align: center; }
.closer-cta { margin-top: var(--s-3); }

/* ── FOOTER ────────────────────────────────────────────────────────────────*/
.site-footer { background: var(--paper-lo); border-top: 1px solid var(--line);
  padding-block: var(--s-7) var(--s-4); }
.footer-grid { display: grid; gap: var(--s-5);
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr)); }
.footer-brand .wordmark { display: block; margin-bottom: var(--s-3); }
.footer-brand .small { max-width: 32ch; }
.footer-col { display: grid; gap: 0.55em; align-content: start; }
.footer-col h4 { font-family: var(--mono); font-size: var(--t-label); font-weight: 500;
  letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--graphite);
  margin: 0 0 var(--s-2); }
.footer-col a { font-size: var(--t-small); color: var(--ink-2); width: fit-content; }
.footer-col a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.footer-base { display: flex; justify-content: space-between; flex-wrap: wrap; gap: var(--s-3);
  margin-top: var(--s-6); padding-top: var(--s-4); border-top: 1px solid var(--line); }
@media (max-width: 780px) { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 440px) { .footer-grid { grid-template-columns: 1fr; } }

/* ── SKIP LINK ─────────────────────────────────────────────────────────────*/
.skip-link {
  position: fixed; top: 8px; left: 8px; z-index: 200;
  padding: 0.7em 1.1em; background: var(--ink); color: var(--paper-hi);
  border-radius: var(--radius-sm); font-size: var(--t-small); font-weight: 600;
  transform: translateY(-160%); transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus-visible { transform: none; }
.tear-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ── REDUCED MOTION (§41) ──────────────────────────────────────────────────
   The story survives; the movement does not. The creative canvas has its own
   reduced-motion treatment next to its own rules — it paints the resolved
   piece and drops the pin entirely. */
@media (prefers-reduced-motion: reduce) {
  .mw img, .mw video { opacity: 1; transition: none; }
  .mw::after { animation: none; }
  .scrollcue i,
  .vis-brief i, .vis-direction em, .vis-wire::before, .vis-wire::after,
  .pstep .stamp, .contact-sheet .frame.sel::after {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  .device, .piece, .commission { transition: none !important; }
}



