/* ============================================================================
   Reel Lab landing — global rules. Decided once, here, before any layout.
   These exist because the hero broke four times in v1–v6 on the same class of bug.

   1. LAYERS: decoration is z-index 0–1 and pointer-events:none. Content is z-index 5+.
      No exceptions, no per-element overrides.
   2. FLOW: anything that must not overlap lives in normal flow (flex column,
      margin-top:auto). Never absolute positioning with pixel offsets tuned to a
      viewport height.
   3. TRANSFORMS: never mix a layout transform (rotate/centre) with an animated
      transform on the same node. Rotate an inner span, animate opacity on the outer.
   4. No viewport-height-dependent absolute offsets in a section that also has
      flowing content.
   5. Numbers come from js/data.js. Never type a price into markup.

   Palette is binding — frontend/styles.css :root is the machine truth.
   ============================================================================ */

:root {
  /* ground & surfaces */
  --ink: #0D0E11;
  --surface: #101115;
  --surface-2: #14161B;
  --raise: #1C1F26;
  --sunk: #0A0B0E;

  /* lines */
  --line: #1B1E24;
  --border: #22252C;
  --border-2: #2A2E36;
  --border-3: #3A3F49;

  /* text */
  --fg: #ECEEF2;
  --fg-soft: #C9CFD8;
  --fg-muted: #9AA3AF;
  --fg-faint: #7B8797;

  /* accent — means one thing: "click this" */
  --accent: #E3FF32;
  --accent-hover: #EEFF66;
  --accent-deep: #B8CC28;
  --on-accent: #141519;
  --accent-05: rgba(227, 255, 50, .05);
  --accent-12: rgba(227, 255, 50, .12);
  --accent-35: rgba(227, 255, 50, .35);
  --accent-60: rgba(227, 255, 50, .6);

  /* status — never used as accent */
  --ok: #3FCF8E;
  --warn: #F0B23A;
  --danger: #F05B54;
  --marked: #5B9DFF;

  --sans: 'Schibsted Grotesk', system-ui, -apple-system, sans-serif;
  --mono: 'Spline Sans Mono', ui-monospace, SFMono-Regular, monospace;

  --ease: cubic-bezier(.2, .8, .2, 1);
  --gutter: clamp(20px, 4vw, 72px);

  /* ── REVIEW MOTION, declared once ────────────────────────────────────────
     Keep / kill happens on three surfaces — #inbox, the viewer, the tour's
     inbox step — and it has to feel like one thing on all three.

     `--verdict-hold` is how long a judged frame stays on screen wearing its
     mark before the surface moves on. js/site.js READS it (see HOLD, next to
     verdictMark) rather than carrying its own copy, because the mark's own
     animation is timed off it as well and a duration typed twice drifts —
     when it drifts the frame advances mid-fade, which is exactly the "not
     fluid" this pass was raised about.

     `--frame-in` is the arriving frame. One number, one curve, so a take
     landing in the tour and a take landing in #inbox move identically. */
  --verdict-hold: 300ms;
  --frame-in: 240ms;
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ============================================================================
   THE SELECTION TREATMENT — one recipe, every surface (DEV-115)

   Theo: *"The selection must be more clear here."* It was not, because there
   were a dozen near-misses of the same idea scattered down this file: some
   selected things got an accent border, some a 12% bed, some a 5% bed, some a
   tinted label, one got a double ring. Nothing looked wrong on its own and
   nothing matched anything else, so "selected" had no consistent shape to
   learn.

   There is now ONE recipe and it is declared here, once:

       the edge you chose   → --sel-bd     (full accent — this is the live one)
       the bed under it     → --sel-bg     (one strength, not three)
       its ink              → --sel-ink    (full-strength, never a tint)

   Rows carry no border of their own, so the bed does the work there; framed
   things get both. That is geometry, not a second treatment.

   Adding a surface? Put its selector in the list below. Do NOT write
   `border-color: var(--accent)` next to a new component — that is how this
   started.

   TWO documented exceptions, both later in the file and both deliberate:
     · `.wchip--seg[data-on]` — the wall's VIEW and GROUP controls. Something is
       ALWAYS selected in each of them, and `--accent` on this site means "this
       is live", never "this is the current setting" (see the note at that rule).
     · `.pb__segb[data-on]`   — the app's own NORMAL/SECTIONS segmented toggle,
       copied from frontend/PromptBuilder.jsx. Same reasoning, and it is the
       app's language, not this page's.

   NOT selection, and deliberately untouched: `[data-lit]` / `[data-dim]`, the
   wall's hover-light. That is opacity + saturation and it must never become a
   hue shift — see the note at `.wtile`.
   ============================================================================ */

:root {
  --sel-bd: var(--accent);
  --sel-bg: var(--accent-12);
  --sel-ink: var(--fg);
}

/* ---------- shared atoms ---------- */

.eyebrow {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0;
}

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* graph-paper fill — used as panel background AND as shape fill (flim device) */
.graph {
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, .035) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, .035) 1px, transparent 1px);
  background-size: 22px 22px;
}

/* image slot — always the right aspect, whether or not a pixel exists yet */
.slot {
  position: relative;
  display: block;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 10px;
  background-color: var(--surface-2);
  overflow: hidden;
}
.slot img { display: block; width: 100%; height: 100%; object-fit: cover; }
.slot--empty::after {
  content: attr(data-label);
  position: absolute;
  left: 10px;
  bottom: 8px;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* ── THE VERDICT ON A FRAME — one declaration, every review surface ─────────
   `.vmark` is what keep · kill · mark-final LOOK like, and this is the only
   place that is written down. js/site.js `verdictMark()` is the only thing that
   builds it, and it goes inside the `.slot` — the one element #inbox, the
   viewer and the tour's inbox step have in common. Theo: *"unify it so that it
   looks the same all the way, all the time."*

   It replaced three treatments that were not one thing:
     · `.review .card[data-state]` + `.review__glyph` — right, but #inbox
       advanced past the judged take in the same tick, so nobody ever saw it
     · `.lb__win[data-verdict]` — the whole dialog to 35% opacity and a -0.4deg
       rotate, which greyed out the chip row you had just pressed
     · `.tinbox .card[data-out]` — no CSS at all, ever. Silence.

   The ring is a BORDER on this overlay, not `border-width` on the slot: growing
   the slot's own border from 1px to 3px reflows the picture inside it by 2px,
   and a 2px jump on the beat of a keypress is the twitch that read as "the
   animation is not fluid". Nothing here changes layout.

   Tone comes off FF.STATES (ok · danger · accent) through the `data-tone` the
   renderer sets. There is no fourth green in this file. */
.vmark {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  border: 3px solid transparent;
  border-radius: inherit;
  pointer-events: none;
  animation: vmark-in calc(var(--verdict-hold) * .45) var(--ease) both;
}
.vmark[data-tone='ok'] {
  border-color: var(--ok);
  background: color-mix(in srgb, var(--ok) 17%, transparent);
}
.vmark[data-tone='danger'] {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 19%, transparent);
}
.vmark[data-tone='accent'] {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* the glyph — ✓ for a keep or a final, ✕ for a kill. `<i>`, so the renderer
   never has to know a class name for it. */
.vmark i {
  font-style: normal;
  font-family: var(--mono);
  font-size: clamp(44px, 9vw, 118px);
  font-weight: 700;
  line-height: 1;
  color: currentColor;
  text-shadow: 0 4px 26px rgba(0, 0, 0, .6);
  animation: vglyph-in calc(var(--verdict-hold) * .6) var(--ease) both;
}
.vmark[data-tone='ok'] i     { color: var(--ok); }
.vmark[data-tone='danger'] i { color: var(--danger); }
.vmark[data-tone='accent'] i { color: var(--accent); }
@keyframes vmark-in  { from { opacity: 0; } }
@keyframes vglyph-in { from { opacity: 0; transform: scale(.82); } }

/* ============================================================================
   [data-pop] — "this frame opens the viewer", said once (DEV-117)

   `lb.pop()` in site.js is the ONLY way a frame becomes clickable, and it sets
   this attribute on every one of them — 232 across six surfaces. The cursor was
   nevertheless declared eight separate times, once per surface, and the split's
   six landed frames had been missed: they opened the viewer with a plain arrow
   cursor. It is declared HERE now, next to `.slot`, because the attribute — not
   the class — is what makes a frame clickable.

   Do not write `cursor: zoom-in` next to a new frame component. Call lb.pop().
   ============================================================================ */
[data-pop] { cursor: zoom-in; }

/* ============================================================================
   FOCUS — one treatment, page-wide.

   This existed only by accident: assets/island/island.css (a GENERATED bundle,
   loaded after this file) carries a global `:focus-visible` outline, so the
   landing page's keyboard focus was inherited from the app's build output and
   would have vanished the day the island was dropped. Same shape, declared here,
   so the page owns it.
   ============================================================================ */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A frame is a `<figure>` with the radius on the `.slot` inside it, so the ring
   would come out a sharp rectangle around a rounded picture without this. */
[data-pop]:focus-visible { border-radius: 10px; }

/* ---------- header ---------- */

.hdr {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 0 var(--gutter);
  height: 56px;
  background: rgba(13, 14, 17, .82);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.hdr__mark {
  font-weight: 800;
  font-size: 14px;
  /* The wordmark sets in caps. The tracking flips sign with it — -.02em is a
     value for lowercase display type and closes caps into a single shape. The
     markup still reads "Reel Lab" so the accessible name, the <title> and every
     line of prose on the page stay one spelling. */
  text-transform: uppercase;
  letter-spacing: .045em;
  white-space: nowrap;   /* "REEL LAB" is one word-shape; it never sets on two lines */
  color: var(--fg);
  text-decoration: none;
}
.hdr__nav { display: flex; gap: 28px; margin-left: 8px; }
.hdr__nav a {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-muted);
  text-decoration: none;
  transition: color .12s ease;
}
.hdr__nav a:hover { color: var(--fg); }
.hdr__right { margin-left: auto; display: flex; align-items: center; gap: 12px; }

/* the one persistent global affordance — same chip the app uses */
.kbd {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .1em;
  padding: 3px 6px;
  border: 1px solid var(--border-2);
  border-radius: 5px;
  color: var(--fg-muted);
  background: var(--sunk);
  white-space: nowrap;
}
.btn-k {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  padding: 0 10px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--fg-muted);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
  text-decoration: none;              /* the class is on an <a> too — The Craft, in the header */
  transition: border-color .12s ease, color .12s ease;
}
.btn-k:hover { border-color: var(--border-3); color: var(--fg-soft); }

/* Cross-link into The Craft, placed at the foot of the sections the guide goes
   deeper on. Quiet by default — this page sells, the guide teaches, and a reader
   who wants the second thing will be looking for exactly this. */
.spine__more { margin: 26px 0 0; padding: 0 var(--gutter); }
.spine__more a {
  display: inline-block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--fg-muted);
  text-decoration: none;
  border-bottom: 1px solid var(--border-2);
  padding-bottom: 3px;
  transition: color .14s var(--ease), border-color .14s var(--ease);
}
.spine__more a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* the single solid accent fill on this screen = the primary action */
.btn-buy {
  display: inline-flex;
  align-items: center;
  height: 30px;
  padding: 0 14px;
  border: 0;
  border-radius: 8px;
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background .12s ease;
}
.btn-buy:hover { background: var(--accent-hover); }
.btn-buy:active { background: var(--accent-deep); }
.btn-buy--lg { height: 44px; padding: 0 22px; font-size: 11px; border-radius: 10px; }

/* `.plan__notify` — the per-card email form and its 420px stacking rule — lived
   here. Both are gone with the three-fields-to-one change (see priceSection in
   js/site.js): there is one capture on this page now, `.notify` below the cards,
   and it already had all of this. Nothing was moved, it was deleted, because a
   second form component for the same job is how you end up with three of them. */


/* ---------- hero ----------
   THREE elements: the field of work, the two doors, the command bar.

   No headline. Theo: "we don't even need a headline — focus on the images or on
   the functions." The statement rides inside the buy door, where it is read by
   someone already looking at a button.

   The failure this rebuild fixes: it was not clear anything could be RUN. The
   tour is no longer a ghost button beside a filled one — it is a card the same
   size, and it plays a filmstrip of itself, so the thing you are about to start
   is visible before you press it.

   Rule 3 is load-bearing here: a tile's POSITION is left/top (layout), and the
   only animated property is `translate3d` (motion). They never share a node. */

.hero {
  position: relative;
  /* `100vh` on iOS Safari is the viewport with the toolbars HIDDEN, so the hero
     came out ~110px taller than the screen actually showing and the two doors
     sat below the fold on every iPhone until you scrolled. `svh` is the small
     viewport — the one you can see on arrival. `vh` stays as the fallback for
     anything that does not know the unit; both lines are required. */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(22px, 4vh, 48px);
  padding: clamp(96px, 14vh, 150px) var(--gutter) clamp(48px, 8vh, 96px);
  perspective: 1100px;
  overflow: hidden;
}

/* ---- the field ---- */

.field {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;                 /* rule 1: decoration never takes a click */
  transform-style: preserve-3d;
  transition: transform .5s var(--ease);
  transform: rotateX(calc(var(--my, 0) * -2.2deg)) rotateY(calc(var(--mx, 0) * 2.6deg));
}

/* One layer per job on the tour door, all three hanging the SAME twelve boxes, so
   a change of style is twelve images swapping inside a grid that does not move.

   A TRUE dissolve: the incoming layer fades up while the outgoing one is still
   fully opaque underneath, and only then drops out. Cross-fading both at once
   dipped to a dark screen in the middle of every change.

   `visibility` is what keeps it cheap. An off layer is not merely transparent —
   it is skipped by paint and compositing entirely, so at any moment the GPU is
   holding twelve tiles, not thirty-six. It flips only AFTER the fade has run,
   which is what the delay on the visibility transition is for. */
.field__l {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity .45s var(--ease) .5s, visibility 0s linear .95s;
}
.field__l[data-on='1'] {
  opacity: 1;
  visibility: visible;
  transition: opacity .8s var(--ease), visibility 0s;
  will-change: opacity;                 /* only ever on the one that is moving */
}

/* the 16:9 plane the authored percentages are measured against; JS sizes it to
   cover the hero (site.js sizeStages) */
.field__stage {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  transform-style: preserve-3d;
}

.field__t {
  position: absolute;                   /* layout: left/top/width/aspect-ratio only */
  transform: translate3d(
    calc(var(--mx, 0) * var(--d) * -14px),
    calc(var(--my, 0) * var(--d) * -10px),
    calc(var(--d) * 70px)
  );
  transition: transform .45s var(--ease), opacity .7s var(--ease);
  opacity: 0;
}
/* Global rule 3: the layout transform lives on the wrapper, the idle motion on an
   inner node. Mixing them is what broke the hero four times in v1–v6.
   Each tile gets its own period (--f) and offset (--o), so the plane breathes
   instead of sliding as one rigid sheet.

   Travel is deliberately small — parallax ≤14px, drift ≤6px. The layout's proof of
   no-overlap depends on it: the gutters between authored boxes are wider than the
   most two neighbours can move toward each other. Raise either number and check
   the columns in FF.HERO_FIELD again. */
.field__l[data-on='1'] .field__t .slot {
  animation: drift calc(var(--f, 8) * 1s) ease-in-out infinite;
  animation-delay: calc(var(--o, 0) * 1s);
}
@keyframes drift {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(0, -6px, 0) scale(1.01); }
}
.field__t[data-in='1'] { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .field__t .slot { animation: none; } }

/* The box is the frame. `!important` beats the inline aspect-ratio fillImages()
   writes from the image record — here the picture is cover-cropped into an
   authored box and has no say in the geometry. */
.field__t .slot {
  height: 100%;
  aspect-ratio: auto !important;
  border-color: rgba(255, 255, 255, .07);
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}

/* The doors sit over the centre and carry their own glass, so the scrim here is
   only wide enough to keep the type off the busiest frames. It was once .93 in
   the middle and the entire field went black — the work is the point of this
   screen, not the backdrop to it. */
.field::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(52% 44% at 50% 48%, rgba(13, 14, 17, .8) 0%, rgba(13, 14, 17, .34) 60%, rgba(13, 14, 17, 0) 100%);
  pointer-events: none;
}

/* ---- the two doors ---- */

/* Tour LEFT, buy RIGHT. The ratio is unchanged from when the sides were the other
   way round — the statement is set to break in three lines at this width and
   narrowing its column to give the examples more room costs a line. */
.hero__doors {
  position: relative;
  z-index: 10;
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  align-items: stretch;                 /* both cards full height … */
  gap: clamp(12px, 1.6vw, 22px);
  /* 1100 was the whole hero on any screen, so at 2560 the one thing the page
     opens with was 43% of it. The floor is still 1100 — nothing under ~1775px
     moves — and the ceiling stops before the statement starts setting on one
     line, which is the break the copy is written for. */
  width: min(clamp(1100px, 62vw, 1340px), 100%);
  margin: 0 auto;
}

.door {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: clamp(20px, 2.4vw, 34px);
  border: 1px solid var(--border-2);
  border-radius: 18px;
  background: rgba(16, 17, 21, .74);
  backdrop-filter: blur(22px) saturate(140%);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  box-shadow: 0 30px 90px rgba(0, 0, 0, .6);
  text-align: left;
  text-decoration: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: border-color .2s var(--ease), transform .25s var(--ease), background .2s var(--ease);
}
.door:hover { border-color: var(--border-3); transform: translateY(-3px); }
/* the bespoke `:focus-visible` that used to sit here (offset 3px, everything
   else on the page 2px) is gone — the one treatment at the top of this file
   covers a <button> and an <a> alike (DEV-117) */

.door__say {
  display: block;
  font-size: clamp(24px, 2.9vw, 46px);
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1.02;
  color: var(--fg-faint);
  text-wrap: balance;
}
.door__say b { color: var(--fg); font-weight: 800; }
/* The statement is sized against its CARD, not the screen, and the two are on
   different curves: `2.9vw` runs out at 1586px and the card keeps widening after
   that, so past ~1600 the type has to be re-anchored or it sets five lines in a
   625px card where every width the copy was written for sets four. Measured at
   1920: 53px gave five lines at a box-to-type ratio of 10.5; the ratio at 1440,
   which is the width this copy was cut on, is 12.0. 46-54 holds 11.8-12.1.
   The floor is exactly where the old ceiling stopped, so 1586 to 1600 is
   continuous and nothing below 1600 moves. */
@media (min-width: 1600px) {
  .door__say { font-size: clamp(46px, 2.45vw, 54px); }
}
/* the statement floats between the two auto margins rather than hugging the top
   of a card whose height is set by the taller one next to it */
.door--buy .door__say { margin-top: auto; }

/* … and both buttons on one line. This is the only thing in either card that
   is bottom-anchored, which is what makes the two CTAs sit level however much
   copy or however many examples the card above them holds. */
.door__foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin-top: auto;
  padding-top: 4px;
}

.door__meta {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

.door__go {
  align-self: center;
  white-space: nowrap;                  /* "Run the tour" never breaks across two lines */
  padding: 13px 22px;
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -.01em;
}
.door--buy:hover .door__go { background: var(--accent-hover); }

.door__go--ghost {
  background: transparent;
  border: 1px solid var(--border-3);
  color: var(--fg);
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
.door--tour:hover .door__go--ghost { border-color: var(--accent); color: var(--accent); }

/* the three examples. THESE are the label — one commercial, one cel-animated
   spot, one epic, on one grade. Hovering any of them repaints the whole field
   behind the card, so the row is a control on the screen and not a thumbnail
   rail. */
.door__strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 8px;
  align-items: stretch;
}

.door__ex {
  display: flex;
  flex-direction: column;
  gap: 8px;
  cursor: pointer;
}
.door__f {
  aspect-ratio: 3 / 4 !important;
  border-radius: 7px;
  opacity: .34;
  filter: grayscale(1);
  transform: scale(.95);
  transition: opacity .5s var(--ease), filter .5s var(--ease), transform .5s var(--ease);
}
.door__ex[data-on='1'] .door__f {
  opacity: 1;
  filter: none;
  transform: scale(1);
  border-color: var(--accent-35);
}
.door__ex:hover .door__f { opacity: .85; filter: none; }

.door__exn {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
  transition: color .3s var(--ease);
}
.door__ex[data-on='1'] .door__exn { color: var(--accent); }

.door__note {
  font-size: 13px;
  line-height: 1.35;
  color: var(--fg-muted);
  min-height: 1.35em;
}

/* ---- the command bar ---- */

/* In normal flow under the doors, not floated over them (global rule 2). It was
   absolutely centred when it was the hero's only object; now it is the third
   thing on the screen and it must not fight the two it sits beneath. */
.bar {
  position: relative;
  z-index: 10;
  width: min(680px, 100%);
  margin: 0 auto;
}

.bar__glass {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 62px;
  padding: 0 8px 0 22px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .14);
  background: rgba(255, 255, 255, .055);
  backdrop-filter: blur(22px) saturate(150%);
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .5), inset 0 1px 0 rgba(255, 255, 255, .1);
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.bar[data-live='1'] .bar__glass {
  border-color: var(--accent-35);
  background: rgba(255, 255, 255, .075);
}

/* ONE field recipe. `.tmake__write` is the tour's composer (DEV-108) and it is
   the same control — same mono, same accent caret, same placeholder — mounted by
   the same `mountMention`. It only opts out of the bar's single-line clipping. */
.bar__write,
.tmake__write {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.4;
  color: var(--fg);
  outline: none;
  white-space: nowrap;
  overflow: hidden;
  caret-color: var(--accent);
}
.bar__write:empty::before,
.tmake__write:empty::before {
  content: attr(data-ph);
  color: var(--fg-faint);
}

/* an @ resolves to a picture, exactly as MentionText renders it in the app */
.ment {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0 2px;
  padding: 2px 9px 2px 3px;
  border-radius: 999px;
  border: 1px solid var(--accent-35);
  background: var(--accent-12);
  color: var(--fg);
  font-size: 12px;
  vertical-align: baseline;
}
.ment img {
  width: 18px; height: 18px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
.ment i { font-style: normal; color: var(--accent); }

/* a / resolves to saved prompt text */
.snip {
  display: inline;
  padding: 2px 6px;
  margin: 0 2px;
  border-radius: 5px;
  background: rgba(91, 157, 255, .14);
  border: 1px solid rgba(91, 157, 255, .3);
  color: #BBD4FF;
  font-size: 12px;
}

.bar__go {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  flex: none;
  height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, .09);
  color: var(--fg-soft);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.bar__go:hover { background: var(--accent); color: var(--on-accent); }
.bar__go:hover .kbd { border-color: rgba(20, 21, 25, .35); color: var(--on-accent); background: transparent; }

/* ---- the @ / menu ---- */

/* one menu skin, two placements: the bar floats it, the tour card keeps it in
   flow (the tour body scrolls, and an absolute menu inside it gets clipped) */
.bar__menu,
.tmake__menu {
  position: absolute;
  left: 12px;
  right: 12px;
  top: calc(100% + 10px);
  max-height: 290px;
  overflow-y: auto;
  padding: 6px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, .14);
  background: rgba(16, 17, 21, .92);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
  box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
}
.bar__menu[hidden],
.tmake__menu[hidden] { display: none; }

.mrow {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--fg-soft);
  font-family: var(--mono);
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}
.mrow__img {
  width: 30px; height: 30px;
  flex: none;
  border-radius: 7px;
  object-fit: cover;
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.mrow__k { font-weight: 600; }
.mrow__n { margin-left: auto; font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--fg-faint); }

/* ---- the hint under the bar: three keys, no sentence ---- */

.bar__keys {
  display: flex;
  justify-content: center;
  gap: 22px;
  margin-top: 16px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  color: var(--fg-faint);
}
.bar__keys b { color: var(--fg-muted); font-weight: 500; }


/* ---------- window chrome ----------
   Not hero furniture any more — the shot card and the folder panel both use it. */

.chrome {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 12px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.chrome__dots { display: flex; gap: 5px; }
.chrome__dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--border-3); }
.chrome__title {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--fg-faint);
  /* It is a PATH, so it is exactly as long as the folder someone opened, and the
     deepest one on the page (`…/marque/renders/`) ran 33px past the window frame
     that draws it at 320px. A flex child will not shrink below its content width
     without `min-width: 0`; that plus the ellipsis is what turns "spills out of
     its own title bar" into "…/renders/". */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- typing caret ----------
   The hero no longer types at anyone. The prompt-split action lane still does. */

.ask__caret {
  display: inline-block;
  width: 7px;
  margin-left: 1px;
  border-bottom: 1.5px solid var(--accent-60);
  animation: blink 1.1s steps(1) infinite;
  vertical-align: baseline;
}
@keyframes blink { 50% { opacity: 0; } }


/* ---------- ⌘K palette ---------- */

.pal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  justify-content: center;
  align-items: flex-start;
  padding-top: 14svh;
  background: rgba(8, 9, 11, .72);
  backdrop-filter: blur(6px);
}
.pal[data-open='1'] { display: flex; }
.pal__box {
  width: min(620px, calc(100vw - 40px));
  border: 1px solid var(--border-2);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 30px 80px rgba(0, 0, 0, .55);
  overflow: hidden;
}
.pal__field {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 16px;
  height: 52px;
  border-bottom: 1px solid var(--line);
}
.pal__field input {
  flex: 1;
  border: 0;
  background: transparent;
  color: var(--fg);
  font-family: var(--mono);
  font-size: 13px;
  outline: none;
}
.pal__field input::placeholder { color: var(--fg-faint); }
.pal__list { max-height: 340px; overflow-y: auto; padding: 6px; }
.pal__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 11px 12px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--fg-soft);
  font-family: var(--sans);
  font-size: 13px;
  cursor: pointer;
}
.pal__answer {
  display: none;
  padding: 2px 12px 10px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--fg-muted);
  max-width: 62ch;
}
.pal__item[data-sel='1'] + .pal__answer { display: block; }
.pal__foot {
  display: flex;
  gap: 16px;
  padding: 10px 16px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* ============================================================================
   SECTION FURNITURE — shared by all 13 sections.
   Heights are min-heights, never fixed: content flows, the void is the remainder.
   ============================================================================ */

/* ══ THE SIZING SYSTEM (DEV-117) ══════════════════════════════════════════
   Theo: *"check the section size, add kinda a smart rule — max size per
   section, min size etc, so it feels all well designed."*

   DEV-116 set heights BY HAND and they drifted immediately: by the end of
   round 2 there were four different lane numbers (wall 500, shot list 490,
   file panes 510, tool + bench 620) and a fifth for the inbox stage, all
   invented independently, and the band ran 940–1263 (1.34×).

   There are now SIX tokens and no per-section height anywhere.

     --sec-pad   the air above and below. Equal, always. Every section head
                 starts the same distance below the section's top edge, which
                 is what makes thirteen different bodies read as one series.
     --sec-gap   head → body. One value, every section.
     --sec-rig   the RIGGING allowance: the fixed furniture a section carries
                 besides its lane — head, filter bar, nudge line, chip row,
                 filmstrip, foot. 300px is the largest rig on the page
                 (#inbox: head + nudge + metadata row + chips + strip).
     --sec-min   THE FLOOR. Nothing is shorter, so nothing reads as a runt.
     --sec-max   THE CEILING. Nothing is taller. Content past it scrolls
                 INSIDE its own lane; it never grows the page.
     --lane      DERIVED, not guessed: ceiling − padding − gap − rigging.
                 The tallest a section's scrolling body may be.

   THE RULE, in one line: a section is at least `--sec-min` and at most
   `--sec-max`, and the one child named `[data-lane]` absorbs the difference.

   `--sec-max ÷ --sec-min` is 104/90 while viewport-bound and 1144/990 while
   pixel-bound — the SAME 1.156 either way, so the band holds its shape at
   every window size instead of only at the one that got measured.

   WHEN A SECTION WANTS TO EXCEED THE CEILING: give its body `[data-lane]` and
   let it scroll. Do NOT give it a height of its own, and do NOT delete rows to
   make it fit — all 128 wall tiles, 10 shot rows, 36 takes, 16 tools and the
   whole folder tree are still rendered. If a section genuinely cannot lane,
   the rule is wrong: change the tokens, not that one section.

   THE ONE EXEMPTION is `.hero`, which carries no `.sec` class. It is a
   viewport statement (100vh) rather than a member of the band, and it is the
   reference the band is proportioned against. */
:root {
  --sec-pad: clamp(76px, 10vh, 128px);
  --sec-gap: clamp(34px, 4.6vh, 58px);
  --sec-rig: 300px;
  /* ── ONLY THE CEILING MOVED, AND HERE IS WHY ────────────────────────────
     The two numbers answer different questions and a pass that raised BOTH to
     1240/1433 — keeping the 1.156 ratio the comment above commits to — was
     wrong, visibly, and got reverted. Measured at 2000x1400:

         #layer   1240px tall, 791px of content   → 449px of manufactured void
         #money   1240px tall, 804px of content   → 436px
         #faq     1240px tall, 826px of content   → 414px

     A section's height is content clamped between the two. The CEILING can only
     ever take height away, so raising it is free: it lets the four sections that
     have MORE than fits — the wall, the sheet, the builder, the bench — use a
     tall screen instead of stopping a third of the way up it. The FLOOR ADDS
     height to a section that has nothing to fill it with, and four of the twelve
     have under 900px of content on any screen. 990 is as high as it can go
     before the short ones start growing holes; that is where it was, and that is
     where it stays.

     So the ratio no longer holds while pixel-bound (990 : 1433), and that is
     deliberate rather than an oversight. It still holds while viewport-bound
     (90vh : 104vh), which is every screen under 1378px tall — i.e. all of them
     except the big ones this ceiling exists for. */
  --sec-min: min(90vh, 990px);
  --sec-max: min(104vh, 1433px);
  --lane: calc(var(--sec-max) - 2 * var(--sec-pad) - var(--sec-gap) - var(--sec-rig));

  /* ── ONE WIDTH SCALE ──────────────────────────────────────────────────
     Before this there were six ad-hoc numbers (1325 gutter-bound, 1240,
     1040, 980, 820, 800) and the wide ones had NO cap at all, so at 2560 the
     wall ran 2416px beside a 800px FAQ, each hugging a different edge.

     --w-wide is the page's column. It is applied to `.sec` as PADDING, not
     as a max-width, so the section's border and background still reach the
     edges of the screen while everything inside lines up on one left edge.

     Each is now a clamp whose FLOOR is the number that used to be there, so
     nothing below ~1800px of viewport moves by a pixel — measured, not assumed.
     Above that the column grows with the screen instead of leaving 560px of
     dead padding either side at 2560. The ceilings are where the scale would
     stop being a column and start being a wall.                             */
  --w-wide: clamp(1440px, 80vw, 1680px);  /* the column: wall, bench, race, chain, builder, sheet */
  --w-mid:  clamp(1040px, 58vw, 1240px);  /* framed instruments: the inbox, the money panel       */
  --w-read: clamp(820px,  44vw,  920px);  /* reading measure: the FAQ, the plans                  */
  --w-head: clamp(900px,  52vw, 1100px);  /* the section head — display type reads wider than body */
}

.sec {
  position: relative;
  display: flex;
  flex-direction: column;
  padding-block: var(--sec-pad);
  padding-inline: max(var(--gutter), calc((100% - var(--w-wide)) / 2));
  border-top: 1px solid var(--line);
  min-height: var(--sec-min);
}
/* The ceiling applies ONLY where there is a lane to absorb it. This is a safety
   interlock, not a nicety: with a bare `max-height: var(--sec-max)` on `.sec`, a
   section that forgets its `[data-lane]` does not scroll — it CLIPS, silently.
   Measured, not guessed: on a build where the attributes were missing, `#work`
   rendered 1143px tall over 4524px of content and 3,400px of the wall was cut
   off with nothing on screen to say so.
   A section with no lane simply grows past the band instead — wrong, but
   visibly wrong, which is the failure mode you want. */
.sec:has(> [data-lane]) { max-height: var(--sec-max); }

/* Everything in a section holds its natural height … */
.sec > * { flex: 0 0 auto; }
/* … except the ONE child marked `data-lane` in index.html, which shrinks to
   whatever is left under the ceiling and scrolls the rest. Four of them hand
   the scroll to inner panes instead (below) because they are two-pane layouts
   whose panes scroll apart. */
.sec > [data-lane] {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
.build, .files, .race, .review { overflow: visible; }

/* ── A LANE THAT SCROLLS MUST SAY IT SCROLLS ───────────────────────────────
   macOS draws overlay scrollbars: zero width, invisible until you are already
   scrolling. `.wall` hid 3,500px behind 500 with no mark on the screen at all,
   and `.build__tool` went further and set `::-webkit-scrollbar { width: 0 }`.
   A load-bearing hidden scrollbar is a bug, not a clean look.

   Two marks, both automatic:
     · a soft FADE over the last 40px, switched off the moment the lane reaches
       its end (laneAffordance() in site.js clears the attribute's value). So
       "there is more below" and "that was all of it" look different, with
       nothing written down to explain it. On macOS this is the mark that does
       the work — verified: `.wall` masked at rest, unmasked at the bottom.
     · a real 8px TRACK in the page's own line colours. This only lands where
       `scrollbar-width` is unsupported (Safari): Chrome ignores
       `::-webkit-scrollbar` on any element that sets the standard property,
       and macOS draws overlay bars at zero width regardless. It is the
       fallback, not the primary — do not drop the fade in favour of it.  */
[data-scrolls]::-webkit-scrollbar { width: 8px; height: 8px; }
[data-scrolls]::-webkit-scrollbar-track { background: transparent; }
[data-scrolls]::-webkit-scrollbar-thumb {
  background: var(--border-2);
  border-radius: 99px;
  border: 2px solid transparent;
  background-clip: content-box;
}
[data-scrolls]::-webkit-scrollbar-thumb:hover { background: var(--border-3); background-clip: content-box; }
[data-scrolls='more'] {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent);
}
/* The one thing that could have argued for an exception here — `.pb__side` is
   `position: sticky` inside `.build__tool`, and a mask on a scroll container
   can detach sticky — was measured rather than assumed: the rail sits at the
   same offset masked and unmasked. No exception. */

/* ── THE BAND IS A DESKTOP INSTRUMENT · IT COMES OFF ON TOUCH ──────────────
   Measured on an iPhone 14 Pro (390x844) before this rule existed: EIGHT of the
   twelve sections were hiding content inside a nested scroller nobody could see.
   #work held 8,472px of wall in a 347px window. #shots hid 2,821px, #prompt
   2,102px, #layer 718px, #spine 562px — and #price, the one section whose whole
   job is to be read, hid 449px of itself, which is most of the plan table.

   The band is not wrong; it is a rule about filling a LANDSCAPE screen with a
   MOUSE on it, and a phone held upright has no such screen to fill. Two things
   happen below 1080px — and on any coarse-pointer screen up to 1400px, which is
   the clause that catches the iPads the width alone misses: an 11" Pro in
   landscape is 1180 and a 12.9" is 1366, both of them thumb-driven, both of them
   on the wrong side of a 1080px line. 1400 stops before a touchscreen desktop,
   which has the room the band was drawn for:

     · the section stops being height-bound. `min-height` was manufacturing dead
       air — a 145px head floating in an 878px section — and `max-height` was
       manufacturing the trap. Both go, and the page scrolls, which is the one
       gesture a phone has.
     · the lane stops scrolling. laneAffordance() in js/site.js reads the
       COMPUTED overflow, so `overflow: visible` clears its own `data-scrolls`
       fade on the next resize; nothing here needs a JS change.

   The four exceptions are the long instruments — the wall, the builder, the shot
   sheet and the tool bench. Released outright they are 8,472 / 2,582 / 3,282 /
   3,081px of list on a phone, which is the "scroll of nothing" the 560px block
   further down already argues against. They keep a pane, but one sized off the viewport so its top
   and bottom edge are both on screen at once — a box you can see the shape of
   is a box you know to scroll. `overscroll-behavior: contain` stays on them and
   comes off everything else, so the page never traps a swipe it does not own. */
@media (max-width: 1080px), (pointer: coarse) and (max-width: 1400px) {
  .sec { min-height: auto; }
  .sec:has(> [data-lane]) { max-height: none; }
  .sec > [data-lane] {
    max-height: none;
    overflow: visible;
    overscroll-behavior: auto;
  }
  #work > [data-lane],
  #prompt > [data-lane],
  #shots > [data-lane],
  #tools > [data-lane] {
    max-height: min(76vh, 720px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

.sec__head { max-width: var(--w-head); margin: 0 0 var(--sec-gap); }
.sec__head--center { margin-left: auto; margin-right: auto; text-align: center; }
.sec__head .eyebrow { margin-bottom: 18px; }

.h2 {
  margin: 0;
  /* 48 → 44 with the measure widened 760 → 900 (`--w-head`). Same scale on all
     thirteen; what changed is that the longest headline now sets four lines
     instead of five and the shortest still sets one, so the head block runs
     81–223px instead of 85–292px. No copy was touched to get there. */
  /* `2.6vw + 6px` is the old `3.2vw` curve re-cut so it passes through the same
     value at every width the page was tuned at — 32.6px at 1024, 43.4px at 1440
     against 32.8 and 44.0 before — and keeps climbing past it instead of
     flat-lining at 44 for the whole of a 27" display. */
  font-size: clamp(26px, calc(2.6vw + 6px), 56px);
  font-weight: 700;
  letter-spacing: -.032em;
  line-height: 1.08;
  color: var(--fg);
}
.h2 b { color: var(--accent); font-weight: 700; }
/* 20ch put the centred heads on four lines while the left-ranged ones ran two;
   26ch is the same measure in the same units, chosen so both come out the same
   number of lines. */
.h2--center { margin-inline: auto; max-width: 26ch; }

.card {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
}

/* ---------- 04 · money ----------
   DEV-103 rebuild: a project switcher, one ceiling panel per kind (image and
   video price very differently, so one slider was hiding the real decision),
   the manual-approval gate, and the per-project defaults. Every number is
   painted from FF.PROJECTS / FF.MODELS — nothing here is typed (site.js
   moneySection()). */

.money { max-width: var(--w-mid); }

.money__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 26px;
}
.mtab {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 9px 15px;
  border: 1px solid var(--border-2);
  border-radius: 10px;
  background: var(--surface);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.mtab:hover { border-color: var(--border-3); }
.mtab b { font-size: 13px; font-weight: 600; color: var(--fg-soft); }
.mtab i {
  font-family: var(--mono);
  font-style: normal;
  font-size: 9px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
/* the one thing that stays lit here — which project is live right now */
.mtab[data-on='1'] b { color: var(--accent); }

.money__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}
.money__kind {
  padding: 20px 22px 22px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
}
.money__kind[data-off='1'] { opacity: .5; }

.money__khead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}
.money__cap {
  font-size: clamp(22px, 2.6vw, 32px);
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--accent);
  line-height: 1;
}

.money__slider {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 3px;
  border-radius: 2px;
  background: var(--border-2);
  cursor: ew-resize;
  outline: none;
}
.money__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px; height: 18px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  cursor: ew-resize;
}
.money__slider::-moz-range-thumb {
  width: 18px; height: 18px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  cursor: ew-resize;
}
.money__slider:focus-visible { box-shadow: 0 0 0 3px var(--accent-12); }
.money__slider:disabled { opacity: .4; cursor: default; }
.money__slider:disabled::-webkit-slider-thumb { background: var(--fg-faint); }
.money__slider:disabled::-moz-range-thumb { background: var(--fg-faint); }

.money__kstat {
  margin: 14px 0 0;
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: .02em;
  color: var(--fg-faint);
}
.money__kstat b { color: var(--fg-soft); font-weight: 500; }
.money__kstat .warn { color: var(--warn); }

.money__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin-top: 22px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}

/* the manual-approval gate — a checkbox behind a small pill switch, styled
   rather than a native control since it is a decision, not a form field */
.money__gate { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.money__gate input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.money__gswitch {
  position: relative;
  width: 34px; height: 19px;
  flex: none;
  border: 1px solid var(--border-2);
  border-radius: 99px;
  background: var(--surface-2);
  transition: background .15s var(--ease), border-color .15s var(--ease);
}
.money__gswitch::before {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 13px; height: 13px;
  border-radius: 50%;
  background: var(--fg-faint);
  transition: transform .15s var(--ease), background .15s var(--ease);
}
.money__gate input:checked + .money__gswitch { border-color: var(--accent-35); background: var(--accent-05); }
.money__gate input:checked + .money__gswitch::before { background: var(--accent); transform: translateX(15px); }
.money__gate input:focus-visible + .money__gswitch { box-shadow: 0 0 0 3px var(--accent-12); }
.money__glabel { font-size: 12px; color: var(--fg-soft); max-width: 34ch; }

.money__defaults { display: flex; flex-wrap: wrap; gap: 8px; }
.mdef {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface-2);
}
.mdef i {
  font-style: normal;
  font-family: var(--mono);
  font-size: 8.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.mdef b { font-size: 11px; font-weight: 600; color: var(--fg-soft); }

.money__say {
  margin: 22px 0 0;
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.money__say b { color: var(--fg-soft); font-weight: 500; }
.money__say .warn { color: var(--warn); }

@media (max-width: 720px) {
  .money__grid { grid-template-columns: 1fr; }
  .money__row { flex-direction: column; align-items: flex-start; }
}

/* ---------- 06 · it lands sorted ----------
   DEV-104: two real panes — a folder tree, and a viewer for whatever was
   clicked in it — instead of a tree beside a static picture grid.

   DEV-118 (2026-07-29): this used to be the page's one full-bleed section —
   it dropped its right gutter so the view pane ran to the true viewport edge,
   and re-added the left one by hand. Theo: *"This section has like no limits
   at the right side. It's completely broken."* It read as an overflow bug
   rather than a choice, because it was the only section whose right edge was
   not the page column. The special case is gone: `.sec--files` inherits
   `.sec`'s own padding, so both edges land on the same column as every other
   section. Do not re-introduce a per-section gutter here. */
.sec--files { overflow: hidden; }

/* flex, not grid: the pane heights come from the section's remainder (see
   `[data-lane]`), and a flex column passes an indefinite height down to its
   children where a percentage inside a grid area does not resolve. */
.files {
  display: flex;
  align-items: stretch;
  gap: clamp(16px, 2.2vw, 30px);
  min-height: 0;
}
.files__tree { flex: 0 0 clamp(280px, 26%, 360px); }
.files__view { flex: 1 1 0; min-width: 0; }

/* 78vh → `min(50vh, 510px)` → the section's own remainder. DEV-114, Theo:
   *"way too large now because it's sucking up so much space… collapse it a
   little bit so that we have kind of like the same sizes on each section."*
   Both panes already scrolled internally; they were simply taller than every
   other section. DEV-117 removes the invented 510: the panes are now exactly
   as tall as the ceiling leaves them, like every other lane on the page. */
.files__tree, .files__view {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.tree { padding: 10px 0 16px; overflow-y: auto; }
.trow {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 5px 16px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg-muted);
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}
/* DEV-114: the tree really nests now — context/cast/vela.md is three levels,
   renders/archive/SC01_SH010_plate_v4.png is three. One step per level. */
.trow[data-depth='1'] { padding-left: 32px; }
.trow[data-depth='2'] { padding-left: 52px; }
.trow[data-depth='3'] { padding-left: 72px; }
.trow[data-kind='dir'] { color: var(--fg-soft); }
.trow[data-kind='dir'] .trow__ico { color: var(--accent-60); }
.trow__ico { font-size: 10px; color: var(--fg-faint); flex: none; }
.trow > span:nth-child(2) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trow__note {
  margin-left: auto;
  padding-left: 10px;
  flex: none;
  font-size: 9px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
  white-space: nowrap;
}
/* real states, not decorative badges — the same three words #inbox and
   #shots use (FF.DEMO takes), just as a trailing label instead of a chip. */
.trow[data-state='final']    .trow__note { color: var(--accent); }
.trow[data-state='approved'] .trow__note { color: var(--ok); }
.trow[data-state='killed']   .trow__note { color: var(--danger); }
.trow:hover            { background: var(--surface-2); color: var(--fg); }
.trow[data-hot='1']    { background: var(--accent-05); color: var(--fg); }

.files__body { flex: 1 1 auto; overflow-y: auto; padding: 22px 26px; }

.files__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
}
/* the renders/ grid reuses `.stake` (DEV-101's take chip) wholesale for the
   same Final / kept / killed badge language — grid cells stretch instead of
   the strip's fixed 96px lane. */
.files__grid .stake { width: auto; }
.files__grid .slot { aspect-ratio: 16 / 9; transition: border-color .12s ease, transform .2s var(--ease); }
.files__grid .slot[data-hot='1'] { border-color: var(--accent-35); transform: translateY(-2px); }

/* story.md and a context note — plain text in a sunk panel, the same
   language as `.build__out`/`.look__out` above (the prompt builder's own
   read-out), because "plain text you could open in any editor" is the whole
   argument of this section. */
.fview__text {
  margin: 0;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--sunk);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.75;
  color: var(--fg-soft);
  white-space: pre-wrap;
  max-width: 720px;
}
.fview__note { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
.fview__pin { flex: none; width: 150px; aspect-ratio: 1 / 1 !important; border-radius: 10px; cursor: zoom-in; }

/* ---- story.md and memory.md, read. DEV-114, Theo: *"not just like a very
        stupid one-liner story, but write like the story, the memory, as if this
        would be a real project."* So these are several pages of actual writing
        and they have to be READABLE — a `<pre>` of the markdown source is
        authentic and unreadable at that length. The frontmatter stays as source
        (that block is the argument that it is a plain file); the body renders. */
.fdoc { max-width: 660px; }
.fdoc .fview__text { margin-bottom: 22px; font-size: 11px; line-height: 1.6; }
.fdoc__h {
  margin: 26px 0 8px;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg);
}
.fdoc h3.fdoc__h { margin-top: 0; font-size: 15px; letter-spacing: -.01em; text-transform: none; }
.fdoc__p { margin: 0 0 12px; font-size: 13px; line-height: 1.68; color: var(--fg-muted); }

.fmem { list-style: none; margin: 0; padding: 0; }
.fmem__i {
  display: flex;
  gap: 14px;
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
}
.fmem__i:last-child { border-bottom: 0; }
.fmem__d { flex: none; width: 82px; font-size: 10px; color: var(--fg-faint); padding-top: 2px; }
.fmem__t { font-size: 12.5px; line-height: 1.62; color: var(--fg-muted); }
.fmem__t b { color: var(--fg); font-weight: 600; }
.fmem__t i { color: var(--fg-soft); }

/* ---------- 09 · price ---------- */

.plans {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  max-width: var(--w-read);
  margin: 0 auto;
}
.plan {
  display: flex;
  flex-direction: column;
  padding: 26px 24px 24px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
}
/* THE FEATURED CARD, using the recipe declared at the top of this file rather
   than a fourth near-miss of it. It was `border-color: var(--accent-35)` — one
   pixel of acid green at 35% alpha, all the way round, on a near-black ground.
   That is below the threshold where a hairline reads as a colour at all: it
   renders unevenly along an edge, and Theo read it as *"the green overlay is
   broken"* — which is the right reaction to a line you cannot tell from an
   artefact. --sel-bd is the full-strength accent the whole page uses to say
   "this is the live one", and it is spent on ONE edge so the card is marked
   without being outlined in highlighter. */
.plan[data-featured='1'] {
  border-color: var(--border-2);
  border-top: 2px solid var(--sel-bd);
  background: var(--surface-2);
}
.plan__name {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.plan__price {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 14px 0 4px;
}
.plan__num {
  font-size: 46px;
  font-weight: 700;
  letter-spacing: -.04em;
  line-height: 1;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}
.plan__unit {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.plan__lead { margin: 8px 0 20px; font-size: 14px; color: var(--fg-soft); }
.plan__list { margin: 0 0 24px; padding: 0; list-style: none; display: grid; gap: 10px; }
.plan__list li {
  display: flex;
  gap: 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--fg-muted);
}
.plan__list li::before { content: '·'; color: var(--accent-60); font-weight: 700; }
.plan .btn-buy { margin-top: auto; justify-content: center; height: 42px; border-radius: 10px; font-size: 11px; }
/* one solid accent fill per section — the second card is the quiet one */
.plan[data-featured='0'] .btn-buy {
  background: transparent;
  border: 1px solid var(--border-3);
  color: var(--fg-soft);
}
.plan[data-featured='0'] .btn-buy:hover { border-color: var(--accent-35); color: var(--fg); }

.plans__foot {
  max-width: var(--w-read);
  margin: 22px auto 0;
  text-align: center;
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* ---------- the notify field under the price ----------
   Shares its behaviour with the tour's field through FF.mountCapture — one
   implementation for every address field on the site. Six hand-rolled copies is how
   the previous version ended up with six different validation behaviours. */

.notify { max-width: 460px; margin: 30px auto 0; text-align: center; }
.notify__l {
  display: block;
  margin-bottom: 10px;
  font-size: 12.5px;
  color: var(--fg-muted);
}
.notify__row { display: flex; gap: 8px; }
.notify__in {
  flex: 1;
  min-width: 0;
  padding: 11px 14px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--sunk);
  font-family: var(--mono);
  font-size: 12px;
  color: var(--fg);
}
.notify__in::placeholder { color: var(--fg-faint); }
.notify__in:focus { outline: none; border-color: var(--accent-35); }
.notify__go {
  flex: none;
  padding: 11px 18px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 600;
  color: var(--fg);
  cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.notify__go:hover:not(:disabled) { border-color: var(--accent-35); background: var(--accent-05); }
.notify__go:disabled { opacity: .6; cursor: default; }
.notify__say {
  margin: 9px 0 0;
  min-height: 15px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .04em;
}
.notify__say[data-tone='ok'] { color: var(--ok); }
.notify__say[data-tone='warn'] { color: var(--warn); }
.notify[data-done='1'] .notify__row { opacity: .45; pointer-events: none; }

/* the checkout page's address field, and the key-copy button on the receipt */
.buyform { margin: 18px 0 0; }
.buyform__l { display: block; margin-bottom: 9px; font-size: 12.5px; color: var(--fg-muted); }
.buyform__in {
  width: 100%;
  padding: 13px 15px;
  margin-bottom: 12px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--sunk);
  font-family: var(--mono);
  font-size: 13px;
  color: var(--fg);
}
.buyform__in::placeholder { color: var(--fg-faint); }
.buyform__in:focus { outline: none; border-color: var(--accent-35); }
.buyform__say {
  margin: 10px 0 0;
  min-height: 15px;
  font-family: var(--mono);
  font-size: 10px;
  line-height: 1.6;
  letter-spacing: .04em;
}
.buyform__say[data-tone='ok'] { color: var(--ok); }
.buyform__say[data-tone='warn'] { color: var(--warn); }
.buyform[data-done='1'] .buyform__in { opacity: .45; pointer-events: none; }

.btn-copy {
  margin-top: 10px;
  padding: 8px 14px;
  border: 1px solid var(--border-2);
  border-radius: 6px;
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-soft);
  cursor: pointer;
}
.btn-copy:hover { border-color: var(--accent-35); color: var(--fg); }

/* ---------- 10 · faq + footer ---------- */

.faq { max-width: var(--w-read); border-top: 1px solid var(--line); }
.qa { border-bottom: 1px solid var(--line); }
.qa__q {
  display: flex;
  align-items: center;
  gap: 16px;
  width: 100%;
  padding: 20px 0;
  border: 0;
  background: none;
  text-align: left;
  color: var(--fg-soft);
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -.01em;
  cursor: pointer;
  transition: color .12s ease;
}
.qa__q:hover { color: var(--fg); }
.qa__q::after {
  content: '+';
  margin-left: auto;
  font-family: var(--mono);
  font-size: 15px;
  color: var(--fg-faint);
}
.qa[data-open='1'] .qa__q { color: var(--fg); }
.qa[data-open='1'] .qa__q::after { content: '–'; color: var(--accent); }
.qa__a {
  display: none;
  padding: 0 40px 22px 0;
  max-width: 68ch;
  font-size: 13.5px;
  line-height: 1.65;
  color: var(--fg-muted);
}
.qa[data-open='1'] .qa__a { display: block; }

.foot {
  padding: 30px var(--gutter) 40px;
  border-top: 1px solid var(--line);
  background: var(--sunk);
}
.foot__row {
  display: flex;
  align-items: center;
  gap: 28px;
  flex-wrap: wrap;
}
.foot__mark { font-weight: 800; font-size: 13px; text-transform: uppercase; letter-spacing: .045em; }
/* `flex-wrap` matters more than it looks: this row was the ONE element on the
   whole page that could not shrink, so at 375px it measured 428px and dragged the
   document's scrollWidth out to 448 — which then stretched the fixed header to
   448 too and made the header look like the bug. It was not; this was. */
.foot__nav { display: flex; gap: 22px; flex-wrap: wrap; row-gap: 10px; min-width: 0; }
.foot__nav a {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
  text-decoration: none;
}
.foot__nav a:hover { color: var(--fg-soft); }
.foot__note { margin-left: auto; font-size: 10px; letter-spacing: .1em; color: var(--fg-faint); }

/* ---------- 02 · the wall ----------
   Four productions interleaved so they touch. The adjacency IS the argument:
   the grade survives a change of genre, and you can only see that side by side.

   Hovering a face lights every frame it is pinned to and drops the rest. That is
   the pin, made visible — the page says nothing about it because it does not
   need to. `data-lit` / `data-dim` are set by site.js on anything tagged
   `data-ent`, so the cast chips, the wall tiles and the shot-row reference chips
   all answer the same hover without knowing about each other. */

/* ---- the filter bar ----
   DEV-121 · ONE LINE. It used to be six filter groups plus two segmented
   controls laid out flat — 43 buttons, 137px tall, above the thing the visitor
   actually came to look at. Theo: *"way too many buttons there at the top.
   Everything is so chaotic… It must be really, really simple and clean."*

   Now: the view switch, three faces, two menu buttons. Everything else is
   inside the filter menu, capped per group. See the long note in
   `js/site.js` `wallSection()` for what moved where and why the faces did not.

   It is a CONTROL SURFACE and it is styled like one — small, dense, neutral, the
   labels quieter than the chips. It must never compete with the wall underneath
   it. The only chip that is ever accent is a chip that is ON: `--accent` means
   "this is live" on every screen of this site, and a filter that is currently
   narrowing the grid is exactly that. */

.wfil {
  margin: 0 0 20px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}

/* The bar proper. It wraps only when the active-filter pills need a second
   line — the controls themselves always fit, because there are four of them. */
.wbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  min-width: 0;
}

.wfil__g {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 5px;
  min-width: 0;
}
.wfil__lab {
  font-family: var(--mono);
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-right: 3px;
}

/* ---- the view switch: two positions, one control ---- */
.wseg { display: flex; gap: 4px; }

/* ---- the faces that stayed out of the menu ---- */
.wfaces { display: flex; align-items: center; gap: 5px; min-width: 0; }
.wfaces__more {
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  font-size: 10px;
  color: var(--fg-faint);
  cursor: pointer;
  transition: border-color .16s var(--ease), color .16s var(--ease);
}
.wfaces__more:hover { border-color: var(--border-3); color: var(--fg-soft); }

/* ---- the two menus ------------------------------------------------------
   A menu button carries its own value ("Group  Project ▾"), so the bar says
   what it is set to without being opened. The panel is absolutely positioned
   and the bar is NOT a stacking/overflow trap — `.wfil` sits outside the
   wall's scrolling lane precisely so this can hang over the grid. */
.wmenu { position: relative; }
.wmenu__b {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  font: inherit;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-muted);
  cursor: pointer;
  transition: border-color .16s var(--ease), color .16s var(--ease), background .16s var(--ease);
}
.wmenu__b:hover { border-color: var(--border-3); color: var(--fg-soft); }
.wmenu[data-open='1'] .wmenu__b { border-color: var(--border-3); background: var(--raise); color: var(--fg); }
.wmenu__k { color: var(--fg-faint); letter-spacing: .1em; text-transform: uppercase; font-size: 8.5px; font-weight: 700; }
.wmenu__v:empty { display: none; }
.wmenu__v { color: var(--fg); }
.wmenu__car { font-style: normal; font-size: 8px; color: var(--fg-faint); }

/* a filter menu with something on says so on the button, not only in the pills */
.wmenu--filter .wmenu__b[data-on='1'] { border-color: var(--accent-35); color: var(--fg); }
.wmenu--filter .wmenu__b[data-on='1'] .wmenu__v {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent);
  color: #0b0b0c;
  font-weight: 700;
}

.wmenu__p {
  position: absolute;
  z-index: 30;
  top: calc(100% + 6px);
  left: 0;
  min-width: 200px;
  max-height: min(58vh, 460px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding: 8px;
  border: 1px solid var(--border-2);
  border-radius: 10px;
  background: var(--raise);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .55);
}
/* the filter panel is wide enough that its groups do not wrap after two chips,
   and right-anchored so it never leaves the section */
.wmenu--filter .wmenu__p { left: auto; right: 0; width: min(440px, 84vw); }
.wmenu--filter .wmenu__p .wfil__g { display: block; margin-bottom: 10px; }
.wmenu--filter .wmenu__p .wfil__g:last-child { margin-bottom: 0; }
.wmenu--filter .wmenu__p .wfil__lab { display: block; margin: 0 0 5px; }
.wmenu--filter .wmenu__p .wchip { margin: 0 4px 4px 0; }

/* a row in the group-by menu */
.wopt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 8px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  font: inherit;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--fg-muted);
  text-align: left;
  cursor: pointer;
}
.wopt:hover { background: var(--surface-2); color: var(--fg); }
.wopt__t { flex: 1 1 auto; }
.wopt__tick { font-style: normal; font-size: 9px; opacity: 0; color: var(--fg); }
.wopt[data-on='1'] { color: var(--fg); }
.wopt[data-on='1'] .wopt__tick { opacity: 1; }

/* "+N more" inside a capped group */
.wfil__more {
  display: inline-block;
  margin-top: 2px;
  padding: 3px 8px;
  border: 1px dashed var(--border-2);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-family: var(--mono);
  font-size: 9.5px;
  color: var(--fg-faint);
  cursor: pointer;
}
.wfil__more:hover { color: var(--fg-soft); border-color: var(--border-3); }

/* ---- what is currently on ---- */
.wbar__on { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; }
.wbar__on:empty { display: none; }
.wpill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px 3px 10px;
  border: 1px solid var(--accent-35);
  border-radius: 999px;
  background: var(--accent-05);
  font: inherit;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg);
  cursor: pointer;
}
.wpill:hover { border-color: var(--accent); }
.wpill__x { font-style: normal; color: var(--fg-faint); }
.wpill:hover .wpill__x { color: var(--fg); }

/* Narrow: a panel anchored to its BUTTON runs off the screen, because the
   button is only ~90px wide and the panel is 315. Measured at 375: the filter
   panel opened at L−118. So below 760 the anchor becomes the BAR — the panel
   drops under the whole control row and spans the section's column, which is
   the only box on screen guaranteed to be on screen. */
@media (max-width: 760px) {
  .wfil { position: relative; }
  .wmenu { position: static; }
  .wmenu__p,
  .wmenu--filter .wmenu__p {
    left: 0;
    right: 0;
    width: auto;
    min-width: 0;
  }
}

.wchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px 3px 3px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--fg-muted);
  font: inherit;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .02em;
  text-align: left;
  cursor: pointer;
  transition: border-color .16s var(--ease), background .16s var(--ease), color .16s var(--ease);
}
/* a chip with no picture does not need the picture's padding */
.wchip:not(:has(.wchip__f)) { padding-left: 10px; }
.wchip:hover { border-color: var(--border-3); color: var(--fg-soft); }
.wchip__f {
  flex: 0 0 20px;
  width: 20px;
  aspect-ratio: 1 / 1 !important;
  border-radius: 5px;
}
.wchip__f--round { border-radius: 50%; }
.wchip__t { white-space: nowrap; }

/* the cast chips carry the hover-light that used to be the whole section: hover
   a face and every frame it is pinned to lifts. Hovering is NOT filtering, so
   this state is a border only — it must not look like a chip that is on. */
.wfil__g--cast .wchip[data-lit='1'] { border-color: var(--border-3); }

/* The segmented arrange control: same chip, squared off, so it reads as one
   control with four positions rather than four independent toggles.

   Its selected position is NEUTRAL, not accent. Something is always selected
   here — one of the four is on from the moment the page loads — and `--accent`
   on this site means "this is live", never "this is the current setting". An
   always-lit accent control is exactly the decoration DEV-95 took back out. */
.wchip--seg { border-radius: 6px; padding: 3px 9px; }
.wchip--seg[data-on='1'] {
  border-color: var(--border-3);
  background: var(--raise);
  color: var(--fg);
}

.wfil__clear {
  padding: 3px 10px;
  border: 1px dashed var(--border-2);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-faint);
  cursor: pointer;
}
.wfil__clear:hover { color: var(--fg-soft); border-color: var(--border-3); }

/* DEV-116: 5618-7656px tall, three times the next-largest section and nine
   times the smallest. It scrolls in its own lane now. Every one of the 128
   frames is still rendered and still filterable — the filter bar sits OUTSIDE
   the lane, so it stays put while the grid moves under it, which is what makes
   a filter legible in the first place.

   DEV-109 shortened the lane again with a `--wall-lane` of its own; DEV-117
   deleted it. The wall is `[data-lane]` now — it takes whatever the ceiling
   leaves after the head, the filter bar and the count line, which is the same
   arithmetic every other section obeys. The filter bar stays OUTSIDE the lane,
   so it holds still while the grid moves under it. */
.wall {
  padding-right: 4px;
}

/* ---- the groups. Ported from frontend/Library.jsx:124-188 `section()`: NAME,
        COUNT, then the media. A library is never one field there and it is not
        one here. The header is a button because it folds — a visitor should be
        able to see the SHAPE of 128 frames without scrolling through them. ---- */
.wgrp { margin: 0 0 14px; }
.wgrp:last-child { margin-bottom: 0; }

.wgrp__h {
  display: flex;
  align-items: baseline;
  gap: 9px;
  width: 100%;
  margin: 0 0 8px;
  padding: 5px 8px 6px 4px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.wgrp__h:hover { border-bottom-color: var(--border-3); }
.wgrp__caret { font-size: 9px; color: var(--fg-faint); flex: none; }
.wgrp__f {
  flex: 0 0 18px;
  width: 18px;
  aspect-ratio: 1 / 1 !important;
  border-radius: 50%;
  align-self: center;
}
.wgrp__name {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg);
}
.wgrp__sub {
  font-style: normal;
  font-size: 10.5px;
  color: var(--fg-faint);
}
.wgrp__n {
  margin-left: auto;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--fg-muted);
}
.wgrp__n[data-tone='ok']     { color: var(--ok); }
.wgrp__n[data-tone='danger'] { color: var(--danger); }
.wgrp__n[data-tone='accent'] { color: var(--accent); }
.wgrp[data-open='0'] .wgrp__grid,
.wgrp[data-open='0'] .wgrp__rows { display: none; }

.wgrp__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(140px, 13vw, 200px), 1fr));
  gap: 8px;
}

/* ---- LIST view. frontend/Library.jsx:231 `S.view === 'list' ? <AssetList …>`.
        Dense, scannable, one line a frame: the mode that proves the work is
        organised rather than merely pretty. ---- */
.wgrp__rows { display: flex; flex-direction: column; }
.wrow {
  display: grid;
  grid-template-columns: 62px minmax(0, 1.6fr) 92px minmax(0, 1.5fr) 56px 74px;
  align-items: center;
  gap: 12px;
  padding: 5px 8px;
  border-bottom: 1px solid var(--line);
  cursor: zoom-in;
  transition: background .12s var(--ease);
}
.wrow:hover { background: var(--surface-2); }
.wrow__t { aspect-ratio: 16 / 9 !important; border-radius: 4px; }
.wrow__name {
  font-size: 10.5px;
  color: var(--fg-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wrow__kind {
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.wrow__tags { display: flex; gap: 5px; flex-wrap: wrap; overflow: hidden; }
.wrow__tag {
  font-family: var(--mono);
  font-size: 9.5px;
  color: var(--fg-muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 7px;
  white-space: nowrap;
}
.wrow__scene { font-size: 10px; color: var(--fg-faint); }
.wrow__state {
  justify-self: end;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.wrow__state[data-tone='ok']     { color: var(--ok); }
.wrow__state[data-tone='danger'] { color: var(--danger); }
.wrow__state[data-tone='accent'] { color: var(--accent); }

@media (max-width: 900px) {
  .wrow { grid-template-columns: 52px minmax(0, 1fr) 70px; }
  .wrow__tags, .wrow__scene { display: none; }
}

/* the empty answer, so the section keeps its shape */
.wall__none {
  margin: 0;
  padding: 46px 0;
  font-size: 11px;
  letter-spacing: .08em;
  text-align: center;
  color: var(--fg-faint);
}

.wtile {
  margin: 0;
  cursor: zoom-in;
  transition: transform .3s var(--ease), opacity .3s var(--ease), filter .3s var(--ease);
}
.wtile .slot { border-radius: 8px; }
.wtile:hover .slot { border-color: var(--border-3); }

/* the highlight itself. Opacity + saturation, never a hue shift — a colour
   filter would say "Reel Lab is a filter", which is the one thing it is not. */
[data-ent][data-dim] { opacity: .18; filter: saturate(.15); }
[data-ent][data-lit='1'] { opacity: 1; }
.wtile[data-lit='1'] .slot { border-color: var(--accent-35); }

.wall__foot {
  margin: 20px 0 0;
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* ---------- 03 · the prompt builder ----------
   The app's own axis grammar, running here. White is what changes per shot;
   grey is the block that was locked once and is appended verbatim forever.

   `.build` used to be declared HERE as well as at the section's own block
   further down, and being earlier in the file it simply lost — a silent second
   source of truth for the section's column ratio, and the same shape of bug as
   the `.srow` collision AUDIT.md §6 records. Removed; the live declaration is
   the one under "two panes". */

.look { display: flex; flex-direction: column; gap: 16px; }

.look__saved { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.look__savedrow { display: flex; gap: 6px; flex-wrap: wrap; }

.look__axes { display: flex; flex-direction: column; gap: 10px; }
.look__ax { display: grid; grid-template-columns: 92px 1fr; gap: 12px; align-items: start; }
.look__axname {
  padding-top: 6px;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.look__opts { display: flex; gap: 5px; flex-wrap: wrap; }

/* ─────────────────────────── THE REVIEW-ACTION CHIP ───────────────────────
   `.act` — a faithful port of MediaAction (frontend/RenderMedia.jsx:87-103).

   **This block did not exist.** The markup did: three surfaces (the inbox row,
   the flip lightbox, the tour footer) all built `<button class="act">` with the
   right `data-tone` and `data-fill` attributes and a comment citing MediaAction —
   and then nothing styled them, so they rendered as bare browser buttons. Grey
   rectangles, all the same weight, keep and kill indistinguishable.

   The app's rule, verbatim from RenderMedia.jsx: *glass pill, capitalized label,
   hotkey in parens, tone as border+text tint, fill reserved for the one primary
   state action.* Keep is green, Kill is red, Mark final is accent, everything else
   is neutral — so the two keys you actually press are the two you can see.

   Do not restyle this per-surface. That is precisely how the app grew two visual
   languages for one control, which is what MediaAction exists to have fixed. */
.act {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 14px;
  border: 1px solid var(--border-2);
  border-radius: 999px;
  /* glass, not solid: the chip sits over media and must read on any frame */
  background: rgba(13, 14, 17, .6);
  backdrop-filter: blur(6px);
  color: var(--fg-soft);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease), color .15s var(--ease);
}
/* the hotkey rides inside the label, quieter — never a second control */
.act small {
  font-size: 9.5px;
  font-weight: 400;
  opacity: .65;
}
.act:hover { border-color: var(--border-3); color: var(--fg); }

/* tone = border + text tint. color-mix at 45% is the app's own figure. */
.act[data-tone='ok'] {
  border-color: color-mix(in srgb, var(--ok) 45%, transparent);
  color: var(--ok);
}
.act[data-tone='danger'] {
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  color: var(--danger);
}
.act[data-tone='accent'] {
  border-color: var(--accent-35);
  color: var(--accent);
}
.act[data-tone='ok']:hover     { background: color-mix(in srgb, var(--ok) 12%, transparent); }
.act[data-tone='danger']:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }
.act[data-tone='accent']:hover { background: var(--accent-12); }

/* fill — the ONE primary state action per surface. Solid tone bed, dark text. */
.act[data-fill='1'] {
  border-color: transparent;
  background: var(--fg-soft);
  color: var(--ink);
}
.act[data-fill='1'][data-tone='ok']     { background: var(--ok); color: var(--ink); }
.act[data-fill='1'][data-tone='accent'] { background: var(--accent); color: var(--on-accent); }
.act[data-fill='1']:hover { filter: brightness(1.08); }
.act[data-fill='1'] small { opacity: .55; }

.act:disabled { opacity: .4; cursor: default; }

/* THE chip. One declaration — the app has two byte-identical copies of this and
   that is exactly how the interface drifted into three visual languages.

   DEV-117: the type size was 10.5px here and 10px on `.wchip` (the wall's
   filters) and `.refchip` (a shot's pinned references) — three pills, three
   sizes, no reason. One size now; the rest of each declaration is a real
   difference, not drift, and the reasons are written at each. */
.chip {
  padding: 6px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--fg-muted);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .02em;
  cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease), color .15s var(--ease);
}
.chip:hover { border-color: var(--border-3); color: var(--fg-soft); }

/* the picture IS the definition — "Chiaroscuro" is a word you either know or
   you don't, and the still says it in one glance.
   Same geometry as `.wchip__f` on the wall: one picture-pill, two surfaces. */
.chip--img {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px 3px 3px;
}
.chip--img img {
  width: 26px;
  height: 26px;
  border-radius: 999px;
  object-fit: cover;
  filter: saturate(.55) brightness(.85);
  transition: filter .15s var(--ease);
}
.chip--img:hover img,
.chip--img[data-on='1'] img { filter: none; }

.build__out, .look__out {
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--sunk);
}
.build__line, .look__line {
  margin: 0;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.75;
}
.build__line em, .look__line em { font-style: normal; color: var(--fg); }
.build__line s, .look__line s { text-decoration: none; color: var(--fg-faint); }

.build__meta, .look__meta {
  margin: 14px 0 0;
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.build__meta b, .look__meta b { color: var(--accent); font-weight: 600; }

.build__frame { position: sticky; top: 90px; }
.build__frame .slot { cursor: zoom-in; }

/* ---------- 04 · the shot rows ----------
   A real piece of the app, not a picture of one. Editing a beat re-resolves the
   reference chips, because the references belong to the SHOT and not to the
   sentence — which is the thing that has to be felt rather than read. */

/* Capped, and left-aligned to the heading above it. Full-bleed rows on a 1440
   screen left the beat text stranded a third of the way across with a metre of
   empty row after it. */
/* 1000 → 1240 (DEV-112): six takes at ~158px plus the right column need the
   room, and below this the strip wraps to a second line and the row grows. This
   cap only started working in the same ticket — see the note in mountRows(). */
/* The 1240 cap is gone with DEV-117: `--w-wide` on `.sec` is the column now,
   and it is never narrower than 1240 above 1280px of viewport. */
/* DEV-116: ten shot rows ran the section to 1803px. Same ten rows, in the
   lane. DEV-112 shortens the lane again — the rows are TALLER now (every take
   is on screen instead of hidden behind a hover) and the section still had to
   come down, so the trade is: more of each row, less of the list at once. All
   ten rows are still rendered and still reachable. DEV-117: the invented
   `min(48vh, 490px)` is gone; the list takes the section's remainder. */
.sheet__list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-right: 4px;
}
/* …unless the caller already owns the scrolling. The tour stacks one of these
   per scene inside `.tour__body`, and a lane each hid a third of every scene
   behind a nested scrollbar (DEV-107). `mountRows(host, rows, {lane:false})`. */
.sheet__list--flow { max-height: none; overflow: visible; }

/* 168px, not 96px. This is the app's most recognisable surface and it was rendering
   smaller than the FAQ, with half of every row empty — Theo asked specifically to
   see how the storyboard breaks down, and a postage stamp does not show that. */
.srow {
  display: grid;
  grid-template-columns: 168px 1fr auto;
  gap: 18px;
  align-items: center;
  padding: 12px 18px 12px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  opacity: 0;
  transform: translateY(6px);
  animation: srow-in .4s var(--ease) forwards;
  animation-delay: calc(var(--i) * 60ms);
  transition: border-color .18s var(--ease);
}
.srow:hover { border-color: var(--border-2); }
/* a row with a take strip has no lead thumbnail — the strip is the frames */
.srow--rich { grid-template-columns: 1fr auto; align-items: start; }
@keyframes srow-in { to { opacity: 1; transform: none; } }

.srow__t { aspect-ratio: 16 / 9 !important; border-radius: 7px; cursor: zoom-in; }
.srow__t--none { display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: 7px; background: var(--surface-2); }

.srow__mid { min-width: 0; }
.srow__id { display: flex; align-items: baseline; gap: 9px; margin: 0 0 5px; }
.srow__id b { font-family: var(--mono); font-size: 11px; font-weight: 600; color: var(--accent); }
.srow__id i {
  font-style: normal;
  font-size: 9px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

.srow__beat {
  margin: 0 0 8px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--fg);
  outline: none;
  border-radius: 4px;
}
.srow__beat:focus { box-shadow: 0 0 0 2px var(--accent-35); }

.srow__refs { display: flex; gap: 5px; flex-wrap: wrap; margin: 0; }

.refchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px 3px 3px;
  border: 1px solid var(--border-2);
  border-radius: 999px;
  background: var(--surface-2);
  transition: border-color .15s var(--ease), opacity .3s var(--ease), filter .3s var(--ease);
}
.refchip img { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; }
.refchip b { font-family: var(--mono); font-size: 10px; font-weight: 600; color: var(--fg-soft); }
.refchip i {
  font-style: normal;
  font-size: 8.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.refchip--none {
  padding: 3px 10px;
  border-style: dashed;
  color: var(--fg-faint);
  font-family: var(--mono);
  font-size: 9.5px;
}

.srow__right { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.srow__takes { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-muted); }
.srow__cost { font-size: 12px; font-weight: 600; color: var(--fg); }

/* ---------- DEV-112 · the takes strip, ALWAYS ON SCREEN ----------
   DEV-101 hid this behind a hover. Theo: *"the whole point of this one here is
   really the variation… we should see basically the underlying images, because
   if we open it, it's like too small."* A reveal that vanishes when the pointer
   moves is not seeing the variations, so the strip is simply part of the row —
   and bigger, because judging a take at 96px is not judging it.

   The row still fits: `.sheet__list` scrolls in `--lane`, so a taller row costs
   scroll inside the section, never page height. */
.srow__tally { font-size: 9.5px; letter-spacing: .04em; color: var(--fg-faint); text-align: right; }

.srow__strip {
  display: flex;
  grid-column: 1 / -1;
  grid-row: 2;
  gap: 8px;
  flex-wrap: wrap;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

.stake {
  position: relative;
  flex: 0 0 auto;
  width: 96px;
  padding: 0;
  border: 1.5px solid var(--border);
  border-radius: 7px;
  background: var(--sunk);
  cursor: zoom-in;
  overflow: hidden;
  transition: border-color .15s var(--ease), transform .15s var(--ease);
}
/* the strip's own frames are the point of the section, so they are the size you
   would actually judge one at */
.srow__strip .stake { width: clamp(120px, 11vw, 158px); }
.stake:hover { border-color: var(--border-3); transform: translateY(-2px); }
.stake[data-state='final']    { border-color: var(--accent-35); }
.stake[data-state='killed']   { opacity: .6; }
.stake__img { display: block; width: 100%; aspect-ratio: 16 / 9 !important; border: none; border-radius: 0; }
/* the state, in FF.STATES' own words — "Final" · "Kept ✓" · "Killed ✕" — the
   same four the wall's chips, the folder tree and the inbox use. It used to be
   a bare glyph here and the word "Approved" one section up. */
.stake__badge {
  position: absolute;
  top: 3px;
  left: 3px;
  padding: 0 5px;
  border-radius: 3px;
  font-family: var(--sans);
  font-size: 8px;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.7;
}
.stake[data-state='final'] .stake__badge    { background: var(--accent); color: var(--on-accent); }
.stake[data-state='approved'] .stake__badge { background: var(--ok); color: var(--ink); }
.stake[data-state='killed'] .stake__badge   { background: var(--danger); color: var(--ink); }
/* a context note opened as a folder: the file name under its pinned photograph */
.stake__name {
  display: block;
  padding: 4px 6px 5px;
  font-size: 9.5px;
  color: var(--fg-muted);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sheet__foot {
  margin: 18px 0 0;
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* ---------- the flip lightbox ----------
   Click a frame, press F, and the picture turns over to show the prompt that
   made it — the same string gen-images.mjs sent, split into its labelled parts
   by js/prompt.js. Not a description of a prompt. The prompt. */

.lb {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: none;
  place-items: center;
  padding: clamp(16px, 4vh, 56px) clamp(16px, 4vw, 64px);
  background: rgba(6, 7, 9, .9);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.lb[data-open='1'] { display: grid; }

/* header · frame · take stack · action bar. Only the frame flexes; the other
   three are auto so a six-take strip never squeezes the picture out. */
.lb__win {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto;
  gap: 12px;
  width: min(1080px, 100%);
  max-height: 100%;
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}
/* `.lb__win[data-verdict]` used to live here — the whole dialog to 35% opacity
   and a -0.4deg rotate on a kill. It was the only verdict treatment on the site
   that dimmed its own controls, and at a glance it read as the page breaking.
   The verdict is `.vmark` on the frame now, exactly as in #inbox and the tour;
   see "THE VERDICT ON A FRAME" near the top of this file. Do not re-add a
   window-level one. */

.lb__flip {
  position: relative;
  min-height: 0;
  transform-style: preserve-3d;
  transition: transform .55s var(--ease);
}
.lb__flip[data-back='1'] { transform: rotateY(180deg); }

.lb__face {
  margin: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.lb__face--front { display: grid; place-items: center; min-height: 0; }
/* 240px → 360px: the window now carries a header and a take strip as well as
   the action bar, and a frame sized for the old furniture overlapped both. */
.lb__face--front .slot {
  width: auto;
  max-width: 100%;
  max-height: calc(100svh - 360px);
  border-radius: 12px;
  border-color: var(--border-2);
}
.lb__face--front .slot img {
  width: auto;
  max-width: 100%;
  max-height: calc(100svh - 360px);
  object-fit: contain;
  /* the arriving take, on the same `--frame-in` #inbox and the tour use. The
     <img> is a fresh element on every draw (fillImages swaps it), so this fires
     once per take with no attribute toggling and no forced reflow. Walking a
     take stack used to be an instant, jarring cut. */
  animation: frame-in var(--frame-in) var(--ease) both;
}

.lb__face--back {
  position: absolute;
  inset: 0;
  transform: rotateY(180deg);
  overflow-y: auto;
  padding: 22px 24px;
  border: 1px solid var(--border-2);
  border-radius: 12px;
  background: var(--surface);
}

.lb__sections { display: flex; flex-direction: column; gap: 14px; }

.lbref { display: flex; align-items: center; gap: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.lbref__lab {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
}
.lbref__strip { display: flex; gap: 6px; }
.lbref__t { width: 58px; border-radius: 6px; }

/* ---- the shared prompt view (js/prompt.js FF.renderPrompt) ----
   One row shape for the lightbox, the tour and the split — `.srow2`, the same
   row the Prompt Builder draws under SECTIONS. The tone rules below are what
   the lightbox's old `.lbsec` markup carried; they move onto the shared row so
   the action still reads as the one line that was written for this frame.
   `.lbsec` itself is gone — there was no second copy left to style. */
.pmodes { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 4px; }
.pmode {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: transparent;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
  cursor: pointer;
  text-align: left;
  transition: color .16s var(--ease), border-color .16s var(--ease);
}
.pmode i {
  font-style: normal;
  font-size: 8.5px;
  letter-spacing: .04em;
  text-transform: none;
  color: var(--fg-faint);
}
.pmode:hover { border-color: var(--border-3); color: var(--fg); }
.pmode[data-on='1'] i { color: var(--fg-muted); }

.psec .srow2:last-child { border-bottom: none; }
.psec .srow2__l i {
  display: block;
  margin-top: 3px;
  font-style: normal;
  font-size: 8.5px;
  letter-spacing: .04em;
  text-transform: none;
  color: var(--fg-faint);
}
/* the one line written for this frame */
.psec .srow2[data-tone='own'] .srow2__v { color: var(--fg); }
.psec .srow2[data-tone='own'] .srow2__l { color: var(--accent); }
/* the saved look, and the parts it is assembled from */
.psec .srow2[data-tone='block'] .srow2__l { color: var(--fg-soft); }
.psec .srow2[data-in-block='1'] { padding-left: 14px; border-left: 2px solid var(--border-2); }
.psec .srow2[data-tone='ref'] .srow2__l { color: var(--marked); }
.psec .srow2[data-tone='guard'] { opacity: .5; }

.lb__bar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.lb__name { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-faint); }
.lb__acts { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }

/* ---- the viewer's header: WHICH SHOT, and the flip control ----
   DEV-115. The viewer opens on a shot and the picture is one of its takes, so
   the shot has to be named above it. The flip toggle lives here rather than in
   the action row below because it is a VIEW control, not a review verdict —
   keeping it out of that row is what leaves `fill` free for Keep, the one
   primary state action MediaAction reserves it for. */
.lb__top {
  display: flex;
  align-items: baseline;
  gap: 14px;
  min-width: 0;
}
.lb__shot {
  flex: none;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
}
.lb__beat {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  color: var(--fg-muted);
}
.lb__top .act { margin-left: auto; flex: none; }

/* ---- the take stack ----
   Every other take of this shot, as the SAME `.stake` chip #shots and #files
   draw. Horizontal, scrolling inside its own lane so a six-take shot never
   pushes the frame off the screen. */
.lb__takes {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: none;
}
/* centred under the frame, but WITHOUT `justify-content: center` — that clips
   the first chip once the strip overflows and there is no way to scroll back
   to it. Auto-margin spacers centre a short strip and collapse on a long one. */
.lb__takes::before, .lb__takes::after { content: ''; margin: auto; }
.lb__takes::-webkit-scrollbar { height: 0; }
.lb__takes[hidden] { display: none; }
.lb__takes .stake { width: 104px; cursor: pointer; }

/* ---------- 03 · the Prompt Builder ----------
   Copied from frontend/PromptBuilder.jsx: 280px settings sidebar (presets, then
   axes), main canvas (subject & environment, then the preview panel with its
   accent top border). The picker modal is the app's image grid.

   Sizes, radii and the accent usage are the app's, not re-chosen here — the
   point of copying a component is that it still looks like the product. */

/* ---- 03 · two panes: the tool, and what the tool does ----
   The builder was the tallest block on the page and the emptiest: a 280px sidebar,
   a canvas, and two thirds of the viewport doing nothing. It is now capped in width
   and height and paired with the result, so the section is about half its old height
   and has no dead column. The TOOL is not reduced — all 11 axes, all 247 tokens and
   both preview modes are still the app's own component; only the frame around it is. */
/* DEV-110: the PICTURE column comes first, because the picture is the input.
   Left is what you click, right is what it writes; you read the section the way
   the mechanism runs. Round 1 had the tool on the left and the frame on the
   right, which said the opposite. */
/* flex 5:7, not grid 5fr:7fr — same proportion, but a flex column passes the
   section's remainder down to both panes, where a percentage height inside a
   grid area does not resolve (DEV-117). */
.build {
  display: flex;
  gap: 20px;
  align-items: stretch;
  min-height: 0;
}

/* The tool scrolls inside its own lane rather than stretching the page. It is a
   2700px-tall component — eleven axes, a preset rail and two preview modes — and
   letting that set the section height is what made this the tallest block on the
   page. `overflow-y: auto`, never `hidden`: clipping it would silently amputate
   the last axes, which is a worse bug than the one being fixed. */
.build__tool {
  flex: 7 1 0;
  min-width: 0;
  min-height: 0;
  /* DEV-110, Theo: *"the prompt builder must be also a little bit smaller."*
     DEV-117: no number of its own any more — the pane is as tall as the
     section's ceiling leaves it. Still all 11 axes and all 247 tokens. */
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--line);
  border-radius: 12px;
}
/* `::-webkit-scrollbar { width: 0 }` used to live here and it was the bug rule 4
   of DEV-117 names: a lane that hides 380px behind 620 with no visible bar is a
   lane nobody scrolls. It gets the page's one scrollbar treatment now. */
/* the tool's own sticky sidebar sticks to this lane, not to the viewport */
.build__tool .pb__side { top: 0; }

/* The pick column. No longer sticky: it is the LEAD column now, roughly the
   same height as the tool beside it, and a sticky lead column detaching from
   its own section header was the one thing that made this pair read as two
   unrelated widgets. */
.build__out {
  flex: 5 1 0;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* ONE height in both modes. The stage swaps between a 16:9 comparison frame and
   a square look mosaic, and a stage that changed height as you clicked would
   make the whole section breathe under the reader — the bug §7.2 of AUDIT.md
   records for the inbox. So the height is fixed and it is the BOX that changes
   shape: square and centred for a look, full width for a frame. */
.craft__stage {
  height: min(24vh, 244px);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  background: var(--sunk);
}
.craft__stage img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* a named look's preview is a 320px SQUARE mosaic of four frames — cropping it
   to 16:9 would eat half the evidence, so the box goes square instead */
.craft__stage[data-fit='contain'] {
  width: min(24vh, 244px);
  align-self: center;
}
.craft__stage[data-fit='contain'] img { object-fit: contain; }

/* ---- the axis tabs. STYLE first, OPTICS last — that order is the ticket. ---- */
.pick__tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.pick__g {
  font-size: 8px;
  letter-spacing: .16em;
  color: var(--fg-faint);
  padding-right: 2px;
}
.pick__g + .pick__g,
.pick__t + .pick__g { margin-left: 6px; }
.pick__t {
  padding: 4px 9px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
  cursor: pointer;
  transition: color .16s var(--ease), border-color .16s var(--ease);
}
.pick__t:hover { color: var(--fg-soft); }

.craft__cap { margin: 0; display: flex; flex-direction: column; gap: 3px; }
.craft__cap b {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fg);
}
.craft__cap span { font-size: 11.5px; color: var(--fg-muted); }

/* A grid, not a scroller. These frames are the section's input — one hidden off
   the right edge is one nobody clicks. */
/* Two rows' worth, pinned. Tabs hold between 2 and 8 frames and the cells are
   not the same shape on every tab, so without a floor the section changed
   height every time someone clicked a tab. */
.craft__strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  align-content: start;
  gap: 6px;
  min-height: 182px;
}
/* the Looks tab holds the app's own preset previews, which are SQUARE 2×2
   mosaics rather than 16:9 frames — narrower cells so seven of them cost the
   same height as seven comparison frames */
.craft__strip[data-tab='looks'] { grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); }
.craft__t--look img { aspect-ratio: 1 / 1; }
.craft__t {
  width: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--sunk);
  cursor: pointer;
  opacity: .55;
  transition: opacity .16s var(--ease), border-color .16s var(--ease);
}
.craft__t img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.craft__t i {
  font-style: normal;
  padding: 0 5px 5px;
  font-family: var(--mono);
  font-size: 8.5px;
  letter-spacing: .06em;
  color: var(--fg-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.craft__t:hover { opacity: .85; }
.craft__t[data-on='1'] { opacity: 1; }
.craft__t[data-on='1'] i { color: var(--fg); }

.craft__disc { margin-top: 2px; }
.craft__disc + [data-open='1'] { margin-top: 8px; }
.craft__note { margin: 0; font-size: 9px; line-height: 1.6; letter-spacing: .06em; color: var(--fg-faint); }

/* ---- THE HANDOFF ----
   DEV-110 ends on a locked style and DEV-111 consumes exactly it. This bar is
   the seam between the two sections, so it has to look like a wire and not like
   a caption: accent rule on the left, the block's real length, and a link that
   goes to the section that spends it. */
.lockbar {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin: 16px 0 0;
  padding: 11px 16px;
  border: 1px solid var(--accent-35);
  border-left: 2px solid var(--accent);
  border-radius: 10px;
  background: var(--accent-05);
}
.lockbar__k { font-size: 9px; letter-spacing: .16em; color: var(--accent); }
.lockbar__n { font-size: 13px; font-weight: 600; letter-spacing: -.01em; color: var(--fg); }
.lockbar__m { font-size: 9.5px; letter-spacing: .06em; color: var(--fg-faint); }
.lockbar__go {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-soft);
  text-decoration: none;
  border-bottom: 1px solid var(--line);
}
.lockbar__go:hover { color: var(--accent); border-bottom-color: var(--accent-35); }

@media (max-width: 1000px) {
  .build { flex-direction: column; }
  .build__out, .build__tool { flex: none; overflow: visible; }
  .lockbar__go { margin-left: 0; }
}

.pb {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 24px;
  align-items: start;
  position: relative;
}

.pb__side {
  position: sticky;
  top: 74px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  padding: 14px 10px;
  max-height: calc(100svh - 100px);
  overflow-y: auto;
}
.pb__h { display: flex; align-items: baseline; gap: 8px; padding: 0 8px 8px; }
.pb__h--top { padding: 16px 8px 4px; border-top: 1px solid var(--line); margin-top: 10px; }
.pb__n { margin-left: auto; font-size: 9px; color: var(--fg-faint); }
.pb__n[data-on='1'] { color: var(--accent); }

.pb__presets { display: flex; flex-direction: column; gap: 2px; }
.prow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
}
.prow:hover { background: var(--surface-2); }
.prow img {
  width: 34px; height: 34px;
  border-radius: 4px;
  object-fit: cover;
  flex: none;
  background: var(--surface-2);
}
.prow__n {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--fg-soft);
}
.prow[data-on='1'] .prow__n { color: var(--fg); font-weight: 600; }

/* The axis rail folds — see the note in builder.js. `display:none` and not a
   height transition: the rail is eleven rows tall and animating it open pushes
   the whole side column around for 300ms, which is more motion than the thing
   is worth. */
.pb__axes { display: flex; flex-direction: column; gap: 1px; }

/* ── THE DISCLOSURE, declared once ─────────────────────────────────────────
   #prompt now folds three things — the builder's axis rail, and the two option
   racks in the craft pane beside it — and three near-identical caret buttons is
   how this file ended up with six of everything else. One header treatment, one
   caret, one `[data-open]` contract on the body it controls.

   `display: none` and not a height transition: these bodies are eleven rows and
   a thumbnail grid, and animating them open shoves the column around for 300ms,
   which is more motion than the thing is worth. */
.disc {
  display: flex;
  align-items: center;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
}
.disc:hover .eyebrow, .disc:hover .disc__t { color: var(--fg-soft); }
.disc__t {
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.disc__n { margin-left: 7px; font-family: var(--mono); font-size: 9px; color: var(--fg-faint); }
.disc__k {
  margin-left: auto;
  padding-left: 8px;
  font-family: var(--mono);
  font-size: 9px;
  color: var(--fg-faint);
  transition: transform .18s var(--ease);
  display: inline-block;
}
.disc[aria-expanded='true'] .disc__k { transform: rotate(90deg); }
[data-open='0'] { display: none; }

/* the builder's axis rail is one of the three — same button, its own spacing */
.pb__fold { width: 100%; }
.arow {
  display: block;
  width: 100%;
  padding: 9px 8px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
}
.arow:hover { background: var(--surface-2); }
.arow__top { display: flex; align-items: baseline; gap: 8px; }
.arow__name { font-family: var(--mono); font-size: 10.5px; color: var(--fg-soft); }
.arow__max { font-size: 8.5px; color: var(--fg-faint); }
.arow__picks { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 6px; }

.apill {
  padding: 3px 9px;
  border-radius: 99px;
  border: 1px solid var(--accent-35);
  background: var(--accent-12);
  color: var(--accent);
  font-family: var(--mono);
  font-size: 10px;
}
.apill--empty {
  border: 1px dashed var(--border-2);
  background: transparent;
  color: var(--fg-faint);
  padding: 3px 10px;
}

.pb__main { display: flex; flex-direction: column; gap: 18px; min-width: 0; }

.pb__panel {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;
}
/* the preview panel is the one thing on the page the app itself accents */
.pb__panel--out { border: 1px solid var(--accent-35); border-top: 2px solid var(--accent-60); }

.pb__panelh {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 11px 18px;
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}
.eyebrow--on { color: var(--accent); }
.pb__note, .pb__count { margin-left: auto; font-size: 9.5px; color: var(--fg-faint); }
.pb__preset { font-size: 9.5px; color: var(--accent); }

.pb__se { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 14px 18px 18px; }
.pb__labrow { display: flex; align-items: baseline; gap: 8px; margin-bottom: 7px; }
.pb__lab {
  display: block;
  margin-bottom: 7px;
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.pb__lab--on { color: var(--accent); }
.pb__labrow .pb__lab { margin-bottom: 0; }
.pb__browse {
  margin-left: auto;
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .1em;
  color: var(--accent);
}
.pb__ta {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  background: var(--ink);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--fg);
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.6;
  padding: 11px 13px;
}
.pb__ta:focus { outline: none; border-color: var(--accent-35); }

.pb__seg { display: inline-flex; gap: 2px; border: 1px solid var(--border); border-radius: 99px; padding: 2px; }
.pb__segb {
  padding: 4px 14px;
  border: 0;
  border-radius: 99px;
  background: transparent;
  color: var(--fg-muted);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .1em;
  cursor: pointer;
}
.pb__segb[data-on='1'] { background: var(--fg); color: var(--on-accent); }

/* Was a read-only <textarea>; now a <p>, so the clause the last token wrote can be
   marked inside it. `height` becomes a max with scroll — a paragraph grows with its
   content where a textarea did not, and an eleven-axis look would otherwise push the
   preview past the lane it is capped to. */
.pb__final {
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  max-height: 168px;
  overflow-y: auto;
  background: transparent;
  border: none;
  color: var(--fg);
  font-size: 11.5px;
  line-height: 1.8;
  padding: 16px 18px;
  display: block;
}
.pb__final:focus { outline: none; }
/* the clause the token you just pressed wrote — the same device craft.js uses */
.pb__final mark {
  background: var(--accent-05);
  box-shadow: inset 0 0 0 1px var(--accent-35);
  color: var(--fg);
  border-radius: 3px;
  padding: 1px 2px;
}

/* SECTIONS — the prompt, broken into its parts. 120px label, fragment beside it,
   dashed rule between. The lightbox reuses this row for a rendered frame. */
.pb__sections { padding: 4px 16px 12px; }
.srow2 {
  display: flex;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px dashed var(--line);
  align-items: flex-start;
}
.srow2__l {
  flex: none;
  width: 120px;
  padding-top: 2px;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.srow2[data-empty='1'] .srow2__l { color: var(--fg-faint); }
.srow2__v {
  flex: 1;
  background: transparent;
  border: none;
  resize: none;
  line-height: 1.6;
  padding: 0;
  font-size: 11.5px;
  color: var(--fg);
}
.srow2__v:focus { outline: none; }

.pb__bar {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  flex-wrap: wrap;
}
.pb__clause { font-size: 9.5px; color: var(--fg-faint); }
.pb__btn {
  margin-left: auto;
  padding: 6px 16px;
  border: 1px solid var(--border-2);
  border-radius: 7px;
  background: transparent;
  color: var(--fg-soft);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 700;
  cursor: pointer;
}
.pb__btn--fill { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }

/* ---- the image-grid picker modal ---- */

.pk {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 34px;
  background: rgba(5, 6, 8, .82);
}
.pk[hidden] { display: none; }
.pk__box {
  width: 100%;
  max-width: 1120px;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: 12px;
  overflow: hidden;
}
.pk__h { display: flex; align-items: baseline; gap: 12px; padding: 16px 20px 12px; }
.pk__t {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fg);
}
.pk__s { font-size: 11.5px; color: var(--fg-faint); font-style: italic; }
.pk__done, .pk__c + .pk__done {
  margin-left: auto;
  padding: 5px 14px;
  border: 1px solid var(--border-2);
  border-radius: 5px;
  background: transparent;
  color: var(--fg-soft);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 700;
  cursor: pointer;
}
.pk__search {
  margin: 0 20px 12px;
  background: var(--ink);
  border: 1px solid var(--accent-60);
  border-radius: 7px;
  color: var(--fg);
  font-family: var(--sans);
  font-size: 13px;
  padding: 11px 14px;
}
.pk__search:focus { outline: none; }
.pk__grid {
  overflow-y: auto;
  padding: 4px 20px 18px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(196px, 1fr));
  gap: 12px;
  align-content: start;
}
.pcard {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  overflow: hidden;
  cursor: pointer;
  padding: 0;
  text-align: left;
  font: inherit;
  color: inherit;
}
.pcard:hover { border-color: var(--border-3); }
.pcard img, .pcard__none {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  background: var(--surface-2);
}
.pcard__none { display: grid; place-items: center; color: var(--border-3); font-size: 20px; }
.pcard__tick {
  position: absolute;
  top: 8px; right: 8px;
  width: 20px; height: 20px;
  border-radius: 99px;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 12px;
  font-weight: 700;
}
.pcard__b { display: block; padding: 9px 11px 11px; }
.pcard__n { display: block; font-size: 12.5px; font-weight: 700; color: var(--fg); }
.pcard[data-on='1'] .pcard__n { color: var(--accent); }
.pcard__d {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 10.5px;
  color: var(--fg-muted);
  line-height: 1.5;
  margin-top: 3px;
}
.pk__empty { padding: 30px 0; color: var(--fg-muted); font-size: 11px; letter-spacing: .1em; }
.pk__f { display: flex; align-items: center; gap: 10px; padding: 11px 20px; border-top: 1px solid var(--line); }
.pk__c { font-size: 10px; letter-spacing: .1em; color: var(--fg-faint); }

/* ---------- 03b · the split ----------
   What the AI writes, and what it never touches. Two lanes, deliberately unequal:
   the left one is short and white, the right one is long and grey, and that shape
   IS the argument before a word of it is read. */

/* THE RACE (DEV-111). One brief at the top, two lanes under it, both TYPING at
   the same rate. The left lane running out of work while the right one is still
   streaming IS the argument, so the layout has to make that legible at a glance:
   same width, same row shape, same counter row, and a clock that visibly stops on
   one side and not the other. */

/* stretch, not start: the two counter rows have to sit on the same line or the
   two clocks cannot be read against each other, which is the entire point */
.race {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* row 1 is the brief and holds; row 2 is the two lanes and gives, so the
     section obeys the ceiling by scrolling the ROWS inside each lane rather
     than by growing (DEV-117). The counters and the clocks — the whole point
     of the section — stay pinned under the rows either way. */
  grid-template-rows: auto minmax(0, 1fr);
  gap: 12px;
  align-items: stretch;
  min-height: 0;
}

/* ---- the brief: one prompt, both lanes, spanning both columns ---- */
.race__brief {
  grid-column: 1 / -1;
  padding: 13px 18px 15px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--sunk);
}
.race__briefh { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.race__briefk { font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--fg-faint); }
.race__acts { margin-left: auto; display: flex; gap: 6px; }
.race__go, .race__skip {
  padding: 6px 13px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .12em;
  cursor: pointer;
}
/* the ONE primary: --accent means "this is live", and pressing it is what makes
   the section live. Skip is the quiet twin beside it. */
.race__go { border: 1px solid var(--accent); background: var(--accent); color: var(--on-accent); font-weight: 700; }
.race__go:hover { filter: brightness(1.08); }
.race__skip { border: 1px solid var(--border); background: transparent; color: var(--fg-faint); }
.race__skip:hover { color: var(--fg-soft); border-color: var(--border-3); }
.race__briefq {
  margin: 9px 0 0;
  font-size: 15px;
  line-height: 1.5;
  letter-spacing: -.01em;
  color: var(--fg);
  max-width: 62ch;
}

.race__l {
  display: flex;
  flex-direction: column;
  min-height: 300px;
  padding: 16px 18px 16px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  transition: border-color .3s var(--ease);
}
.race__l[data-side='ff'] { border-color: var(--accent-35); background: var(--accent-05); }
.race__l[data-side='ff'][data-done='1'] { border-color: var(--accent); }

.race__h { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.race__h b {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.race__l[data-side='ff'] .race__h b { color: var(--accent); }
.race__h i { font-style: normal; font-size: 10.5px; color: var(--fg-faint); }

/* the lock — shown ONCE, before anything moves. That is the difference. */
.race__lock { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px dashed var(--line); }
.race__lockt { margin: 0 0 6px; font-size: 11px; line-height: 1.6; }
.race__lockk {
  display: block;
  margin-bottom: 4px;
  font-size: 8.5px;
  letter-spacing: .14em;
  color: var(--accent);
}
/* two lines and no more: it is EVIDENCE that a block exists, not something to
   read — reading it is what the right-hand lane is for */
.race__lockt s {
  text-decoration: none;
  color: var(--fg-faint);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.race__lockm { margin: 0; font-size: 9px; letter-spacing: .06em; color: var(--fg-faint); }

/* ---- the rows. A finished row collapses to one line and keeps its frame; the
   row being written stays open, which is what a streaming log looks like and
   what stops the right lane pushing its own counters off the screen. ---- */
.race__rows {
  display: flex;
  flex-direction: column;
  gap: 7px;
  /* was `min-height: 232px`, which reserved the space the rows would need so the
     two lanes kept the same shape before either had typed anything. `flex: 1 1
     auto` does that better — the rows fill whatever the lane leaves between the
     style block and the counters, and give it back when the ceiling asks. */
  min-height: 0;
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  margin-bottom: 4px;
}
.race__r {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) 62px;
  gap: 9px;
  align-items: start;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--sunk);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .22s var(--ease), transform .22s var(--ease);
}
.race__r[data-in='1'] { opacity: 1; transform: none; }
.race__id { font-size: 9px; letter-spacing: .08em; color: var(--fg-faint); padding-top: 2px; }

/* the prompt line — white is what a model just typed, grey was appended */
.pinline { margin: 0; font-size: 11px; line-height: 1.6; }
.pinline em { font-style: normal; color: var(--fg); }
.pinline s { text-decoration: none; color: var(--fg-faint); }

/* the caret: it only exists while a row is being written, which is the whole
   difference between "typing" and "a progress bar" */
.race__r:not([data-done]) .race__p .pinline::after {
  content: '';
  display: inline-block;
  width: 6px;
  height: 11px;
  margin-left: 1px;
  vertical-align: -1px;
  background: var(--accent);
  animation: racecaret .9s steps(1) infinite;
}
@keyframes racecaret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

.race__r[data-done] .pinline {
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.race__im { display: block; }
.race__imf { display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: 4px; overflow: hidden; }

/* ---- FOUR counters, live, both lanes. The clock is the big one: a stopped
   clock beside a running one is the strongest single image in the section, so
   nothing else on the row is allowed to compete with it. Cost is last and
   smallest, on purpose — the claim is method and speed, not dollars. ---- */
.race__n {
  margin: auto 0 0;
  padding-top: 12px;
  border-top: 1px dashed var(--line);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, auto));
  gap: 4px 14px;
  align-items: baseline;
}
.race__c { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.race__c b {
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--fg-soft);
  font-variant-numeric: tabular-nums;
}
.race__c i {
  font-style: normal;
  font-family: var(--mono);
  font-size: 8px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.race__c--clock b { font-size: 22px; color: var(--fg); }
.race__c--clock[data-run='1'] b { color: var(--accent); }
/* THE image of the section: one clock stopped, one not. Three states, because a
   frozen resting frame that says "running" while nothing moves is a small lie —
   at rest the unfinished lane says "still going", and only a live playback says
   "running". */
.race__c--clock i::after { content: ' · stopped'; }
.race__c--clock[data-run='1'] i::after { content: ' · still going'; color: var(--accent); }
.race[data-live='1'] .race__c--clock[data-run='1'] i::after { content: ' · running'; }
.race__c--cost b { font-size: 10px; color: var(--fg-faint); }
.race__c--cost i { font-size: 7.5px; }

.race__done { margin: 8px 0 0; font-size: 10px; letter-spacing: .08em; color: var(--fg-faint); min-height: 14px; }
.race__l[data-side='ff'][data-done='1'] .race__done { color: var(--accent); }

/* Two columns, because this is a footnote and it has to stay one: at a single
   92ch measure the same words were a 194px slab of grey under a 669px section,
   which is a paragraph asking to be read instead of an appendix. */
.race__f {
  margin: 14px 0 0;
  font-size: 9.5px;
  line-height: 1.7;
  letter-spacing: .04em;
  color: var(--fg-faint);
  columns: 2;
  column-gap: 34px;
}

@media (max-width: 900px) {
  .race { grid-template-columns: 1fr; grid-template-rows: none; }
  .race__l { min-height: 0; }
  .race__rows { min-height: 0; }
  .race__acts { margin-left: 0; }
  .race__f { columns: 1; }
}

/* ---------- 04b · the bench ----------
   Four tools showing a real artefact, twelve named. This replaced sixteen identical
   text cards whose `reads → writes` rows were full of file paths — authentic to the
   app's own Tools screen, and exactly the vocabulary BREAKDOWN.md §2 bans on a page
   read by someone who has never opened a terminal. A product surface and a sales page
   are not the same document. */

/* All sixteen render. Nine carry a real artefact, seven are text — a tool with
   nothing honest to put in the frame gets no frame. Three across so the artefacts
   stay big enough to read; the text-only cards fill the gaps. */
/* DEV-116: 1890px. All sixteen tools are still rendered — Theo: *"render all
   so I can check everything instead of just 30%"* — they scroll in the lane
   rather than setting the height of the page. */
/* `.bench` is the section's `[data-lane]` and is otherwise transparent: it
   exists so the grid inside it can be told how tall it may be. */
.bench { display: flex; flex-direction: column; min-height: 0; }
.bench > .feats { flex: 0 1 auto; }
/* ============================================================================
   THE BENCH · SIXTEEN FLIP CARDS

   What was here: a three-column grid of `.feat` cards whose height came from
   whatever each one had to show. Nine tools own a real artefact and rendered a
   132px picture grid; seven have nothing honest to show and rendered two lines
   of text. `align-items: start` meant a row was as tall as its tallest member,
   so a 251px Generate card sat beside an 81px Camera Moves card in a 251px row
   and the section came out as a field of holes with borders floating in it.

   Now every card is the SAME box: one artefact — a thumbnail if the tool has
   one, its glyph on a tinted field if it does not — plus the name and the
   hotkey. The one-liner and the reads/writes move to the BACK, which is one
   click away. Nothing was cut: all sixteen render, and every word that was on
   the front is still on the card.

   Two motions, and they must not fight:
     · TILT tracks the pointer and lives on the wrapper as CSS variables.
     · FLIP is 180deg of rotateY and lives on `.bcard__flip` inside it.
   They compose because the flip adds its 180deg to the tilt's own rotateY
   rather than replacing the transform. Put both on one node and the card
   snaps back to square the moment it turns.
   ============================================================================ */

.feats {
  display: grid;
  /* 4-up. The card is a fixed shape now, so the column count is the only thing
     that decides how many rows the bench costs. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* The row height is declared HERE and not derived from the card. An
     `aspect-ratio` on the card looked right and measured right — every card
     reported 358px — but the grid sized its rows from the card's CONTENT, and
     the content is two absolutely-positioned faces contributing nothing. Rows
     came out 158px apart under 358px cards, i.e. every row overlapping the next
     by 200px. A track sized by the track is the only version of this that
     cannot drift. */
  grid-auto-rows: clamp(206px, 19vw, 296px);
  gap: 12px;
  align-items: stretch;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding-right: 4px;
  /* the tilt reads as depth only if the vanishing point is shared by the grid */
  perspective: 1100px;
}

/* ---- the card ---- */

.bcard {
  /* it is a <button>: the flip is an action, so it takes Enter, Space and a
     focus ring for free rather than a div wearing a role */
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  height: 100%;
  transform-style: preserve-3d;

  /* neutral rest state — every one of these is rewritten by the rig on hover */
  --pointer-x: 50%;
  --pointer-y: 50%;
  --pointer-from-center: 0;
  --background-x: 50%;
  --background-y: 50%;
  --rotate-x: 0deg;
  --rotate-y: 0deg;
}

.bcard__flip {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transform: rotateX(var(--rotate-y)) rotateY(var(--rotate-x));
  /* the slow curve is the RETURN. While the pointer is on the card the rig
     drives it every frame and this transition is switched off, or the smoothing
     in the engine and the smoothing here fight and the card lags the cursor. */
  transition: transform .55s var(--ease);
}
.bcard.is-live .bcard__flip { transition: none; }
/* The flip adds to the tilt instead of replacing it — see the header note. */
.bcard[data-flip='1'] .bcard__flip {
  transform: rotateX(var(--rotate-y)) rotateY(calc(var(--rotate-x) + 180deg));
  transition: transform .5s var(--ease);
}

.bcard__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: border-color .18s var(--ease);
}
.bcard[data-flag='1'] .bcard__face { background: var(--surface-2); }

.bcard__face--back {
  transform: rotateY(180deg);
  padding: 14px 15px;
  gap: 7px;
  background: var(--surface-2);
}

/* ---- the front ---- */

.bcard__art {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  margin: 9px 9px 0;
  border-radius: 8px;
  overflow: hidden;
  display: grid;
  place-items: center;
  /* AT REST THE RACK IS GREY. The first cut of this drew all sixteen marks in
     full --accent on an accent wash, and sixteen glowing panels is precisely
     what the palette note at the top of this file forbids: "accent — means one
     thing: click this". Sixteen of them means it says nothing. The mark is a
     drafting line until you point at it. */
  color: var(--fg-faint);
  background: var(--sunk);
  transition: color .22s var(--ease);
}
/* …and the card you ARE pointing at lights. The wash is anchored to
   --background-x/y, so the tilt rig drags the light across the field rather
   than sliding the mark under a fixed one. Driven by `:hover` and
   `:focus-visible` rather than by the rig, because the rig is pointer-only: a
   keyboard user tabbing the rack and a phone with no hover at all still get the
   state, they just get it without the tilt. */
.bcard__art::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .25s var(--ease);
  background: radial-gradient(125% 135% at var(--background-x) var(--background-y),
    var(--accent-12) 0%, var(--accent-05) 42%, transparent 76%);
}
.bcard:hover .bcard__art,
.bcard:focus-visible .bcard__art { color: var(--accent); }
.bcard:hover .bcard__art::after,
.bcard:focus-visible .bcard__art::after { opacity: 1; }
/* A faint measuring grid under the mark. Without it the field is a flat panel
   and the mark looks pasted onto it; with it the card reads as a drawing on
   graph paper, which is the register the whole tool bench is written in. It is
   decoration, so it takes no clicks — global rule 1. */
.bcard__art::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, .045) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, .045) 1px, transparent 1px);
  background-size: 16px 16px;
  -webkit-mask-image: radial-gradient(72% 72% at 50% 50%, #000 0%, transparent 100%);
          mask-image: radial-gradient(72% 72% at 50% 50%, #000 0%, transparent 100%);
}
/* the mark sits above both the grid and the wash */
.bcard__mark { z-index: 1; }

/* ---- the marks ----
   Sixteen schematics, drawn rather than photographed. This replaced a pass that
   put a real render in each card: the frames were 2000px files cropped into a
   345x148 letterbox, so a portrait turnaround became a strip of somebody's coat
   and a batch of eight became one face. A photograph in a 148px box is not a
   thumbnail of the tool, it is a crop of an unrelated picture — and the standing
   rule in site.js says a tool that has nothing honest to show gets no frame.
   A drawn mark shows the SHAPE of the job instead, at any size, and costs no
   bytes over the wire.

   `currentColor` throughout, so the accent is set once on the field above and
   every stroke follows it. `non-scaling-stroke` keeps the 1.5px line at 1.5px
   whatever the card scales to — a mark drawn at 64 units and rendered at 96px
   would otherwise carry a 2.2px stroke and read heavier than the type beside it. */
.bcard__mark {
  position: relative;
  display: block;
  width: auto;
  height: 62%;
  max-height: 104px;
  overflow: visible;
}
.bcard__mark * { vector-effect: non-scaling-stroke; }

.bcard__h {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  padding: 11px 13px 13px;
}
.bcard__h b {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bcard__h .kbd { margin-left: auto; flex: none; }

/* ---- the back ---- */

.bcard__eyebrow {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 9px;
  color: var(--accent-deep);
}
.bcard__eyebrow span:first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bcard__eyebrow .kbd { margin-left: auto; flex: none; }

.bcard__head {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.2;
  color: var(--fg);
}
.bcard__say {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--fg-muted);
  overflow: hidden;
}
.bcard__io {
  display: flex;
  gap: 7px;
  margin: 0;
  font-family: var(--mono);
  font-size: 9px;
  line-height: 1.45;
  color: var(--fg-faint);
}
.bcard__io i {
  flex: none;
  font-style: normal;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent-deep);
}
.bcard__io span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.bcard__back-foot { margin-top: auto; }

/* ---- the light ----
   There is no white here, and that is the decision rather than an omission.
   The ProfileCard this began from runs a white-and-rainbow sweep in colour-dodge
   over the whole card. Ported at any strength it reads as a sheet of glass laid
   over the work — Theo: *"the shiny effects are not so good… take out the white
   overlay, just focus on the green"* — and on a page whose palette is one accent
   on neutral darks, a white specular is a second light source nothing else has.

   So the only thing that happens on hover is ACCENT, and it happens in three
   places that agree with each other: the mark turns from drafting grey to
   accent, the wash behind it lights (`.bcard__art::after`, above), and the edge
   picks up the same accent. All three are on the same .22s curve so they read
   as one state and not as three effects. The pointer still moves the wash —
   that is what the tilt rig's --background-x/y is for — it just moves a green
   light instead of a white one. */
.bcard:hover .bcard__face,
.bcard:focus-visible .bcard__face {
  border-color: var(--accent-35);
}

/* the hint that the card has a back. It is the only thing on the front that
   says so, so it is not decoration and does not go under rule 1. */
.bcard__turn {
  position: absolute;
  z-index: 5;
  right: 10px;
  bottom: 46px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--border-2);
  background: rgba(13, 14, 17, .66);
  backdrop-filter: blur(8px);
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-faint);
  opacity: 0;
  transform: translateY(3px);
  transition: opacity .18s var(--ease), transform .18s var(--ease);
}
.bcard:hover .bcard__turn,
.bcard:focus-visible .bcard__turn { opacity: 1; transform: none; }
.bcard[data-flip='1'] .bcard__turn { opacity: 0; }

/* Under prefers-reduced-motion the rig never starts (site.js reads the same
   query), so the card is flat and the sheen never lights. The flip stays —
   it is a state change, not an animation, and it is the only way to the copy. */
@media (prefers-reduced-motion: reduce) {
  .bcard__flip { transition: none; }
  .bcard__art, .bcard__art::after { transition: none; }
}

@media (max-width: 1400px) {
  .feats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
  .feats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 460px) {
  .feats { grid-template-columns: 1fr; grid-auto-rows: 216px; }
}

/* ---------- 04b · inbox ---------- */

.sec--review { background: var(--sunk); }
/* DEV-102: the arrival invitation, which flips in place to the live "N sorted
   in Xs" readout on the first real verdict. One line, never two states on
   screen at once — see paintNudge() in site.js. */
.review__nudge {
  margin: -6px 0 4px;
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--fg-faint);
  text-align: center;
}
.review__nudge b { color: var(--fg-soft); font-weight: 500; }
.review__nudge .kbd { margin: 0 1px; }
/* 620px, not 820px. The queue images are 4:5, so at 820 the card rendered 1025px tall
   and pushed the keep/kill row and the queue strip clean off a 1100px viewport — the
   two keys the section exists to demonstrate were below the fold. The card is also
   height-capped, so a 16:9 frame arriving in the queue cannot stretch the section. */
/* DEV-113, Theo: *"the proportion of [the inbox] here is very bad… it must be
   a little bit bigger here, the whole section."* 620 → 740, and the frame gets
   more of the screen with it. The cap below is what keeps this honest: the
   section was capped at 620/56vh in the first place because at 820 the card
   rendered 1025px tall and pushed the chips and the queue strip off an 1100px
   viewport. The width goes up; the frame stays bounded by viewport height, so
   that regression cannot come back. */
/* DEV-113 · 740 → 1040. *"It must feel more like Lightroom, so where we see the
   results, where it's baked in."* One big stage, a metadata head above it, one
   chip row, and the whole queue as a filmstrip underneath — the four parts
   frontend/ReviewInbox.jsx has, in its order. The frame is still capped by
   VIEWPORT height rather than by the column's width, which is the cap that
   stops "make it bigger" from pushing the chips and the strip off an 1100px
   screen the way it did once before (AUDIT.md §6). */
/* `width: 100%` is load-bearing, not tidiness. `margin: 0 auto` on a flex item
   turns OFF the parent's stretch, so `.review` is sized by its own content and
   then clamped by `max-width` — and since DEV-119 made `.review__strip` a
   `nowrap` row of 36 chips, that content is ~4200px wide. Without an explicit
   100% the section sat at a flat 1040 on a 390px phone and pushed the whole
   PAGE into horizontal scroll. Measured, not theorised: `documentElement
   .scrollWidth` was 1060 against a 390 viewport. */
.review { width: 100%; max-width: var(--w-mid); margin: 0 auto; display: flex; flex-direction: column; gap: 14px; align-items: center; min-height: 0; }

/* ---- the head: label over value, mono, tabular. ReviewInbox.jsx:61-88.
        Position is the timeline; Kept / Killed / Reviewed is the result
        accumulating in front of you, which is the thing that used to vanish the
        moment a card animated away. ---- */
.review__head {
  display: flex;
  align-items: center;
  gap: 22px;
  width: 100%;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--ink);
  overflow: hidden;
}
.rcell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rcell__l {
  font-size: 8.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.rcell__v {
  font-size: 11px;
  color: var(--fg-soft);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.rcell__v[data-tone='ok']     { color: var(--ok); }
.rcell__v[data-tone='danger'] { color: var(--danger); }
.rcell__sp { margin-left: auto; }

/* The queue mixes 16:9 plates and 4:5 fashion frames, so the card was ~90px
   taller on some takes than others and the SECTION resized under the reader.
   The stage is one height whatever is in it. */
/* DEV-117 · the stage is the inbox's lane. It cannot scroll — a review stage
   that scrolls is not a review stage — so it takes `--lane` instead, capped at
   the 560px DEV-113 set, and gives way (with the take scaling on its own aspect)
   if the section would otherwise pass the ceiling. That is why `--sec-rig` is
   300px: this section's furniture — head, nudge, metadata row, chips, filmstrip
   — is the largest rig on the page, and the lane is what is left after it. */
.review__stage {
  width: 100%;
  height: min(var(--lane), 560px);
  flex: 0 1 auto;
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  align-content: stretch;
  justify-items: center;
}
.review .card {
  position: relative;
  justify-self: center;
  align-self: center;
  max-width: 100%;
  max-height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  border: 1.5px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
  transition: transform .2s var(--ease), opacity .2s var(--ease), border-color .2s var(--ease);
}
/* The verdict is `.vmark` INSIDE the slot (see "THE VERDICT ON A FRAME" near
   the top of this file), not a tint on this card. The card used to carry
   `[data-state]` border-colour rules plus a `::after` wash plus its own
   `.review__glyph`; keeping any of them would double the ring the shared mark
   already draws, in a place the viewer and the tour cannot reach. */
/* The stage is sized by HEIGHT and the frame's own aspect ratio decides the
   width — the queue mixes 16:9 plates, 4:5 fashion frames and 1:1 product
   shots, and a fixed-width card would show the wide ones small and crop the
   tall ones to ribbons. Height is capped against the VIEWPORT so "make it
   bigger" can never push the chips and the filmstrip off the screen the way it
   did once before (AUDIT.md §6). A 16:9 take lands at ~995 × 560. */
.review .card .slot {
  border: 0;
  border-radius: 0;
  /* `min(52vh, 560px)` → the lane. Same 560 on a 1100px screen; on a short one
     it now tracks the same number every other section's body tracks. */
  height: min(var(--lane), 560px);
  flex: 0 1 auto;
  min-height: 0;
  width: auto;
  max-width: 100%;
}
.review .card .slot img { width: 100%; height: 100%; object-fit: cover; }
/* The motion is on the ARRIVING take, never on the outgoing one: an exit
   animation has to be run to completion, and this section moves at the speed of
   the keyboard — see decide() in site.js, which holds the JUDGED frame for
   `--verdict-hold` and then swaps, rather than animating anything away.

   `--frame-in`, so this is the same arrival as the viewer's and the tour's. It
   was a bespoke .16s + translateY(10px): a 10px vertical jump in 160ms on every
   keypress, which is a twitch rather than a transition. Fading and settling a
   whisker of scale reads as one continuous thing. */
/* `.card[data-in]` and not `.review .card[data-in]`: `data-in='1'` is set in
   exactly two places — #inbox's paintStage and the tour's inbox card — and both
   of them are a take arriving on a review stage. The tour's used to get no
   arrival animation at all, which is half of why its keep/kill felt dead. */
.card[data-in='1'] { animation: frame-in var(--frame-in) var(--ease) both; }
@keyframes frame-in { from { opacity: 0; transform: scale(.988); } }

.review__verdict {
  position: absolute;
  top: 10px;
  left: 12px;
  padding: 2px 9px;
  border-radius: 6px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--fg-muted);
}
.review__verdict[data-tone='ok']     { background: var(--ok); }
.review__verdict[data-tone='danger'] { background: var(--danger); }
.review__verdict[data-tone='accent'] { background: var(--accent); color: var(--on-accent); }
.review__stars {
  position: absolute;
  top: 10px;
  right: 12px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .1em;
  color: var(--accent);
}

/* ── DEV-119 · THE VERDICT IS THE FRAME ───────────────────────────────────
   A 1.5px tinted border and a small word in the corner is not something you
   read at a glance, and reading it at a glance is the whole section. Theo: *"we
   see basically like framing, like green for keep and x for kill, so the user
   can experience that quick."*

   THE RULE IS UNCHANGED; IT LIVES SOMEWHERE ELSE NOW. The thick edge, the wash
   and the big glyph are `.vmark` near the top of this file, because the viewer
   and the tour's inbox needed the identical mark and could not reach a rule
   scoped to `.review`. Three copies of a verdict is how the three surfaces
   drifted into three different languages, two of which rendered nothing.

   What stays here is the one part that IS this section's: a killed take loses
   its light, the way it does on the wall and in the folder tree. */
.review .card[data-state='killed'] .slot img { filter: saturate(.35) brightness(.72); }

/* the same mark on the filmstrip, small — scanning 36 chips has to answer
   "what did I keep?" without reading 36 captions. `.stake` is already
   `position: relative` (line ~2349) — do not re-declare it here. */
.stake__glyph {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--mono);
  font-size: 26px;
  line-height: 1;
  font-weight: 700;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .7);
  pointer-events: none;
}
.stake__glyph[data-tone='ok']     { color: var(--ok); }
.stake__glyph[data-tone='danger'] { color: var(--danger); }
.stake__glyph[data-tone='accent'] { color: var(--accent); }

/* ── DEV-119 · THE SCRUB BAR ───────────────────────────────────────────────
   A real <input type=range>, so drag, click-to-position, arrow keys and a
   focus ring all arrive for free. It moves the stage and judges nothing. */
.review__scrub {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 0 2px;
}
.rscrub__e {
  font-size: 8.5px;
  letter-spacing: .14em;
  color: var(--fg-faint);
  flex: none;
}
.rscrub__i {
  flex: 1 1 auto;
  min-width: 0;
  height: 18px;
  margin: 0;
  background: transparent;
  cursor: pointer;
  accent-color: var(--accent);
}
.rscrub__i:disabled { opacity: .4; cursor: default; }
.rscrub__n {
  flex: none;
  font-size: 10px;
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums;
  min-width: 118px;
  text-align: right;
}
.rscrub__p {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font: inherit;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-muted);
  cursor: pointer;
  transition: border-color .16s var(--ease), color .16s var(--ease), background .16s var(--ease);
}
.rscrub__p i { font-style: normal; color: var(--fg-faint); }
.rscrub__p:hover { border-color: var(--border-3); color: var(--fg-soft); }
.rscrub__p[data-on='1'] { border-color: var(--border-3); background: var(--raise); color: var(--fg); }
.rscrub__p:disabled { opacity: .4; cursor: default; }

/* Narrow: FIRST / LAST / the readout / the Prompt button are ~295px of fixed
   width plus gaps, which on a 375 screen leaves the range about 40px — a
   control you cannot actually aim at. The end caps are decoration (the strip
   underneath already says where you are), so they go first, and the readout
   drops to its own line rather than eating the track. */
@media (max-width: 620px) {
  .review__scrub { flex-wrap: wrap; row-gap: 6px; }
  .rscrub__e { display: none; }
  .rscrub__i { flex: 1 1 100%; order: -1; }
  .rscrub__n { min-width: 0; text-align: left; flex: 1 1 auto; }
}

/* ── DEV-119 · THE PROMPT, OVER THE FRAME ──────────────────────────────────
   An overlay, not a drawer. `.review` is the tallest rig on the page and the
   stage is whatever is left after it — a panel that pushed the section taller
   would break the one sizing rule the page obeys (AUDIT.md §10). This costs
   zero layout height. */
.rprompt {
  position: absolute;
  inset: auto 0 0 0;
  max-height: 72%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: color-mix(in srgb, var(--ink) 92%, transparent);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--border-2);
  animation: rprompt-in .16s var(--ease);
}
@keyframes rprompt-in { from { opacity: 0; transform: translateY(12px); } }
@media (prefers-reduced-motion: reduce) { .rprompt { animation: none; } }
.rprompt__h {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  flex: none;
}
.rprompt__h b { font-size: 10px; font-weight: 500; color: var(--fg-faint); }
.rprompt__x {
  margin-left: auto;
  padding: 2px 7px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: transparent;
  font: inherit;
  font-size: 11px;
  color: var(--fg-faint);
  cursor: pointer;
}
.rprompt__x:hover { color: var(--fg); border-color: var(--border-3); }
.rprompt__b {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding: 10px 12px 12px;
}

.review__acts { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }

/* ---- the filmstrip. The whole queue as pictures, each carrying the verdict it
        was given. Ten decisions are ten visible decisions — before this the
        queue was ten 4px ticks and a decision left almost no trace.

        DEV-119: it is 36 chips now, not six, so it is a real FILMSTRIP — one
        row that scrolls sideways. Wrapping put four rows under the stage and
        spent the section's whole height budget on thumbnails. It stays
        centred while it still fits and only starts at the left edge once it
        does not, which is what `justify-content: safe center` buys: plain
        `center` on an overflowing flex row makes the FIRST items unreachable,
        because they overflow past the scroll origin. ---- */
.review__strip {
  display: flex;
  gap: 6px;
  width: 100%;
  /* it must be allowed to be narrower than its 36 chips — that is what makes it
     scroll instead of widening its parent */
  min-width: 0;
  flex-wrap: nowrap;
  justify-content: safe center;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  padding-bottom: 4px;
  /* NOT `scroll-behavior: smooth`. This section's claim is that it keeps up
     with a keyboard, and a smooth scroll cannot: hold K down and the strip
     animates towards take 4 while the stage is already on take 12. Instant is
     the correct behaviour here, not the lazy one. */
}
.review__strip .stake { flex: none; }
.stake--strip { width: clamp(84px, 8vw, 112px); }

/* ---- what it was all for. ReviewInbox.jsx:120-142. ---- */
.rdone { display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center; }
.rdone__tick { font-family: var(--mono); font-size: 40px; color: var(--accent); line-height: 1; }
.rdone__h { font-size: 16px; font-weight: 700; color: var(--fg); }
.rdone__n { font-size: 12px; color: var(--fg-muted); font-variant-numeric: tabular-nums; }

/* The caption bar under a reviewed frame. #inbox moved its metadata up into
   `.review__head`; the TOUR's inbox step still draws this one (site.js
   ~4470), so it stays. */
.review__meta {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-faint);
}
.review__meta .stars { margin-left: auto; color: var(--accent); letter-spacing: .1em; }

/* the old 4px queue ticks. Still used by the TOUR's inbox step (.tinbox__q). */
.qslot {
  width: 26px; height: 4px;
  border-radius: 2px;
  background: var(--border);
  border: 1px solid transparent;
}
.qslot[data-done='keep'] { background: var(--ok); }
.qslot[data-done='kill'] { background: var(--danger); }

@media (max-width: 900px) {
  .review__head { gap: 14px; flex-wrap: wrap; }
  .rcell__sp { display: none; }
}

/* ---------- motion ----------
   The page's one motion vocabulary. Everything below the hero used to simply
   appear, which made the top of the page feel like a different product from the
   rest of it.

   Rule 3 still holds: a node either carries the reveal transform or the parallax
   transform, never both, and never a layout transform as well. */

[data-rise] {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition:
    opacity .7s var(--ease) calc(var(--r, 0) * 1s),
    transform .7s var(--ease) calc(var(--r, 0) * 1s);
}
[data-rise][data-seen='1'] { opacity: 1; transform: none; }

/* parallax rides on --py, written by one shared rAF. `will-change` only on the
   handful of nodes that actually move. */
[data-par] {
  transform: translate3d(0, var(--py, 0), 0);
  will-change: transform;
}

/* The wall tile's lift lives on the inner slot rather than the figure. It used to
   be that way because the figure carried a parallax transform and two transforms
   never share a node; the parallax is gone (DEV-98 — the wall re-orders and hides
   its own tiles now, and drift fights that), but the split stays: the figure is
   what the grid's `order` moves, and a transform on it would smear that. */
.wtile .slot { transition: transform .3s var(--ease), border-color .2s var(--ease); }
.wtile:hover .slot { transform: translateY(-5px) scale(1.015); }
.wtile[data-lit='1'] .slot { transform: translateY(-4px); }

@media (prefers-reduced-motion: reduce) {
  [data-rise] { opacity: 1; transform: none; }
  [data-par] { transform: none; }
}

/* the lightbox turn, slower and with a little depth so it reads as a card
   flipping rather than a texture swapping */
.lb__flip { transition: transform .68s cubic-bezier(.62, .02, .3, 1); }

/* section heads settle a beat before their contents */
.sec__head { --r: 0; }

/* ---------- responsive ---------- */

@media (max-width: 1080px) {
  .hdr__nav { display: none; }

  .shot__grid { grid-template-columns: 1fr; }
  /* `.files` used to be listed on that line and it did nothing at all: `.files`
     is a FLEX row, not a grid, so `grid-template-columns` was inert on it. The
     tree kept its `flex: 0 0 clamp(280px, 26%, 360px)` basis and the viewer next
     to it got whatever was left — 94px on a 390px phone, inside which the 16:9
     file grid computed to `15px 15px` and every take chip rendered 15x9. Stack
     it for real. */
  .files { flex-direction: column; }
  .files__tree { flex: 0 0 auto; }
  .files__view { flex: 1 1 auto; }
  .shot__takes { width: calc(100% + 90px); }
  .files__tree, .files__view { max-height: none; }
  .bench { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cases__rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .vary__body { grid-template-columns: 1fr; }
  .vary__held { position: static; display: grid; grid-template-columns: 96px minmax(0,1fr); gap: 12px 14px; align-items: start; }
  .vary__held .eyebrow { grid-column: 1 / -1; }
  .vary__grid { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }

}

@media (max-width: 900px) {
  .hero__doors { grid-template-columns: 1fr; }
  /* stacked, the price card leads again — side by side the tour is the bigger
     target, but nobody should scroll a filmstrip to reach the button */
  .hero__doors .door--buy { order: -1; }
  .pb { grid-template-columns: 1fr; }
  .pb__side { position: static; max-height: none; }
  .pb__se { grid-template-columns: 1fr; }
  /* stacked, the card is as wide as the screen — three 3:4 tiles across it would
     be 320px tall each and push the button off the fold. Cap the row instead of
     re-cropping the frames. */
  .door--tour .door__strip { grid-auto-columns: minmax(0, 1fr); max-width: 400px; }
  .build { grid-template-columns: 1fr; }
  .build__frame { order: -1; }
}

@media (max-width: 720px) {
  .files__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .plans { grid-template-columns: 1fr; }
  .wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* On a phone the bar stacks group by group, and each group becomes ONE
     swipeable line rather than wrapping — thirty-seven chips wrapped at 360px is
     eleven rows of filter above the thing being filtered. The scroll is inside
     the group, so the page itself never scrolls sideways. */
  /* `width: 100%` alone does not constrain a flex child whose PARENT is sized by
     its own content — the row happily grew to 905px and took the whole page with
     it. `min-width: 0` on both is what actually lets the overflow container do
     its job; without it a nowrap flex row is un-shrinkable by definition. */
  .wfil { gap: 8px 14px; min-width: 0; }
  .wfil__g {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .wfil__g::-webkit-scrollbar { display: none; }
  .wfil__lab { position: sticky; left: 0; background: var(--bg); z-index: 1; }
  .wchip { flex: 0 0 auto; }
  .wfil__g--arr { margin-left: 0; }
  /* the base grid is `168px 1fr auto` (three columns). This used to declare two,
     so the third child wrapped under the thumbnail into a 74px gutter and the
     take count came out one character per line. Declare all three. */
  .srow { grid-template-columns: 84px minmax(0, 1fr); row-gap: 10px; }
  .srow__mid { grid-column: 2; }
  .srow__right { grid-column: 1 / -1; flex-direction: row; gap: 12px; align-items: baseline; }
  .tour__win { grid-template-columns: 1fr; }
  .lb__win { grid-template-rows: 1fr auto; }
  .foot__note { margin-left: 0; width: 100%; }
}

/* ============================================================================
   PHONE — measured at 375px, not guessed.
   Everything below fixes something that was actually broken at that width.
   ============================================================================ */
@media (max-width: 560px) {
  /* The tour's rail. It was `display:none` below 720, which throws away the one
     thing the tour exists to prove — that every choice is KEPT. On a phone it
     becomes a scrolling strip along the top instead of vanishing. */
  .tour__kept {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    padding: 10px 14px;
    border-right: 0;
    border-bottom: 1px solid var(--line);
    scrollbar-width: none;
  }
  .tour__kept::-webkit-scrollbar { display: none; }
  .tour__kept .eyebrow { display: none; }
  .tour__kept .tkept { flex: none; width: auto; gap: 7px; }
  .tour__kept .tkept__f { width: 26px; height: 26px; }
  .tkept__none { font-size: 10px; }

  /* Review chips wrap to two rows rather than shrinking below a tap target. */
  .review__acts { gap: 5px; }
  .act { padding: 7px 12px; font-size: 10.5px; }

  /* The split's two lanes stack; the long style block gets its own scroll rather
     than making the section 900px of grey text. */
  .lane__txt--set { max-height: 150px; overflow-y: auto; }

  /* Section rhythm — 3739px of tool bench on a phone is a scroll of nothing. */
  .feats { gap: 10px; }
  .bcard__h { padding: 9px 11px 11px; }
  .bcard__head { font-size: 13px; }

  /* The hero field is decoration; on a phone it competes with the doors. */
  .field { opacity: .5; }

  /* The header. Three controls plus the wordmark do not fit 375px — "The Craft"
     and "Buy · $49" were both wrapping to two lines and the bar grew to 88px.
     ⌘K is not a gesture anyone has on a phone, so that one goes; the other two
     stop wrapping. */
  .hdr { padding-left: 16px; padding-right: 16px; gap: 8px; }
  /* `[data-pal-open]`, NOT `.btn-k` — the Craft link carries the same class, and
     hiding the class hid the only route to a 47-chapter guide. */
  .hdr [data-pal-open] { display: none; }
  .hdr__mark { font-size: 15px; }
  .hdr .btn-k, .btn-buy {
    white-space: nowrap;
    padding: 8px 11px;
    font-size: 10.5px;
  }
}

/* ============================================================================
   TAP TARGETS — gated on the POINTER, not on the width (DEV-119)

   Every number below was measured on the live page at 390x844, not estimated.
   The page mimics a dense pro app, and a lot of that density is right: a chip
   row is meant to look like a chip row. What is not right is a control you
   cannot reliably hit with a thumb, and there were four of those under 20px:

       .money__slider   3px     the ceiling slider — the money section's ONLY control
       .pb__browse     13px
       .lockbar__go    15px
       .rscrub__i      18px     the take scrubber

   `(pointer: coarse)` and not `(max-width: …)` because the thing that changed is
   the input device, not the screen: an iPad in landscape is 1024px wide and is
   still driven by a thumb, and a 380px browser window on a desktop is still
   driven by a mouse and should keep the dense layout it was drawn for.

   NOTHING HERE MOVES LAYOUT. Every rule either grows a box that had slack, or
   pays for its new padding with an equal negative margin. That is deliberate:
   the sections are laid out to the pixel and a tap-target pass that reflowed
   them would be a redesign wearing an accessibility hat.

   AND NOTHING HERE SETS `display`. Every one of these is already a centred flex
   box, so `min-height` alone does the job — and the one version of this block
   that DID set `display: inline-flex` un-hid the ⌘K button on phones, because it
   matched `.hdr .btn-k` at the same specificity as the `.hdr [data-pal-open] {
   display: none }` four hundred lines above it and simply came later in the
   file. A tap-target rule must never be able to put a control back on screen.
   ============================================================================ */
@media (pointer: coarse) {

  /* The two range inputs. A slider's hit area is its BOX, and the box was the
     3px track. Growing `height` would move the track off the row's centre line,
     so the height stays and a transparent border does the work: `content-box`
     keeps the 3px on the content edge, `background-clip: content-box` keeps the
     paint there too, and the negative margin gives back exactly what the border
     took. Result: 3px of visible track, a 39px box to hit it with. */
  .money__slider {
    box-sizing: content-box;
    border: 18px solid transparent;
    background-clip: content-box;
    width: calc(100% - 36px);
    margin: -18px;
  }
  /* The scrubber is already transparent, so it only needs to be taller. */
  .rscrub__i { height: 44px; }

  /* Two text-links that behave as buttons. Padded to 40px, margin paid back.
     `margin-block` and `margin-right` rather than the shorthand — both of these
     are pushed into place by `margin-left: auto` and the shorthand would drop it. */
  .pb__browse  { padding: 13px 6px; margin-block: -13px; margin-right: -6px; }
  .lockbar__go { padding-block: 13px; margin-block: -13px; }

  /* The primary path — anything that starts, buys, keeps or kills. These are the
     ones a miss actually costs something, so they take the full 44. */
  .btn-buy, .hdr .btn-k, .door__go, .act, .race__go, .race__skip, .gate__go, .pb__btn {
    min-height: 44px;
  }

  /* Secondary chrome — filters, tabs, group headers, segment toggles. 36px, not
     44: these come in rows of five to thirty-seven and 44 apiece turns a filter
     bar into a screen of its own. Still 1.5x what they were. */
  .wchip, .pick__t, .pb__segb, .wmenu__b, .wfaces__more, .wgrp__h, .rscrub__p {
    min-height: 36px;
  }
}

/* ============================================================================
   CHECKOUT · RECEIPT · DOWNLOAD — same system, no scroll-story.
   These pages are quiet on purpose: order summary, one decision, one button.
   ============================================================================ */

.page {
  max-width: 720px;
  margin: 0 auto;
  padding: 112px var(--gutter) 96px;
}
.page--wide { max-width: 880px; }

.page__back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 34px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
  text-decoration: none;
}
.page__back:hover { color: var(--fg-soft); }

.page__h1 {
  margin: 0 0 12px;
  font-size: clamp(28px, 3.6vw, 44px);
  font-weight: 700;
  letter-spacing: -.035em;
  line-height: 1.06;
}
.page__sub { margin: 0 0 40px; font-size: 14px; color: var(--fg-muted); max-width: 56ch; }

.panel {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  padding: 22px;
}
.panel + .panel { margin-top: 14px; }
.panel__t {
  margin: 0 0 16px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* order summary rows */
.line {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
  color: var(--fg-muted);
}
.line:last-child { border-bottom: 0; }
.line b { color: var(--fg-soft); font-weight: 500; }
.line .amt {
  margin-left: auto;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}
.line--total { padding-top: 16px; border-top: 1px solid var(--border-2); border-bottom: 0; }
.line--total b { color: var(--fg); font-size: 15px; }
.line--total .amt { font-size: 22px; font-weight: 600; letter-spacing: -.02em; }

/* plan switch on the checkout page */
.pick { display: grid; gap: 10px; margin-bottom: 20px; }
.pick__opt {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  cursor: pointer;
  transition: border-color .12s ease, background .12s ease;
}
.pick__opt:hover { border-color: var(--border-3); }
.pick__dot {
  width: 15px; height: 15px; min-width: 15px;
  border: 1px solid var(--border-3);
  border-radius: 50%;
}
.pick__opt[data-on='1'] .pick__dot { border-color: var(--accent); box-shadow: inset 0 0 0 4px var(--accent); }
.pick__name { font-size: 14px; font-weight: 600; color: var(--fg); }
.pick__lead { display: block; margin-top: 2px; font-size: 12px; color: var(--fg-faint); font-weight: 400; }
.pick__amt {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 600;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

/* form field on the checkout pages. NOT `.field` — that is the hero's image
   plane, and a second definition down here silently shadowed it. */
.fld { display: block; margin-bottom: 16px; }
.fld__l {
  display: block;
  margin-bottom: 7px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.fld input {
  width: 100%;
  height: 42px;
  padding: 0 14px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--sunk);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 14px;
  outline: none;
  transition: border-color .12s ease;
}
.fld input:focus { border-color: var(--accent-35); }

.btn-wide {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 48px;
  border-radius: 10px;
  font-size: 11px;
}
/* `:disabled` as well as the data attribute — buy.js sets the real property so
   the button is unreachable by keyboard too, not merely greyed. A dashed border
   is deliberate: it reads as "not wired yet" rather than "you did something wrong". */
.btn-wide[data-disabled='1'],
.btn-wide:disabled {
  background: var(--surface-2);
  border: 1px dashed var(--border-3);
  color: var(--fg-faint);
  cursor: not-allowed;
  pointer-events: none;
}

.trust {
  display: grid;
  gap: 8px;
  margin-top: 18px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .08em;
  color: var(--fg-faint);
  line-height: 1.6;
}

.note {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px dashed var(--border-2);
  border-radius: 9px;
  background: var(--sunk);
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 1.7;
  letter-spacing: .04em;
  color: var(--fg-faint);
}
.note b { color: var(--warn); font-weight: 500; }

/* licence key block */
.keyblock {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--border-2);
  border-radius: 9px;
  background: var(--sunk);
  /* `word-break` was declared one rule down, on `.keyblock code` — and buy.js
     builds this element the OTHER way, as `<p class="keyblock mono">SHA-256 …</p>`
     with the hash as a bare text node and no <code> anywhere. So the 64-character
     checksum on download.html had nothing telling it to break: it ran 515px wide
     inside a 307px panel and dragged the LAYOUT VIEWPORT out with it to 559px on a
     393px phone, which is why the fixed header measured 559 and the whole page
     scrolled sideways. The rule belongs on the block, not on a child it does not
     always have. */
  min-width: 0;
  word-break: break-all;
  overflow-wrap: anywhere;
}
.keyblock code {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .08em;
  color: var(--fg);
  word-break: break-all;
}

.dl { display: grid; gap: 10px; }
.dlrow {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 15px 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  text-decoration: none;
  transition: border-color .12s ease;
}
.dlrow:hover { border-color: var(--border-3); }
.dlrow[data-you='1'] { border-color: var(--accent-35); background: var(--accent-05); }
.dlrow__n { font-size: 14px; font-weight: 600; color: var(--fg); }
.dlrow__s { display: block; margin-top: 2px; font-family: var(--mono); font-size: 10px; color: var(--fg-faint); letter-spacing: .06em; }
.dlrow__go {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.dlrow[data-off='1'] { opacity: .5; pointer-events: none; }

.steps { margin: 0; padding: 0; list-style: none; counter-reset: s; display: grid; gap: 12px; }
.steps li {
  display: flex;
  gap: 14px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--fg-muted);
}
.steps li::before {
  counter-increment: s;
  content: counter(s);
  flex: none;
  width: 20px; height: 20px;
  border: 1px solid var(--border-2);
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 9px;
  color: var(--fg-faint);
}

@media print {
  .hdr, .foot, .page__back, .no-print { display: none !important; }
  body { background: #fff; color: #000; }
  .panel, .keyblock { border-color: #ccc; background: #fff; }
  .page__h1, .line b, .line .amt, .keyblock code { color: #000; }
}

/* ---------- 01 · the layer ----------
   One instruction, four stops, left to right in the order the work and the money
   actually move. It plays on its own and lights the stops in sequence; the first
   click takes over.

   Replaced four static cards of body copy. That block was explaining the product's
   whole category — "we sit on top of the agent CLI you already pay for" — and was
   the least visual thing on the page. */

.said {
  margin: 0 0 26px;
  text-align: center;
}
.said__q {
  display: inline-block;
  padding: 11px 20px;
  border: 1px solid var(--border-2);
  border-radius: 999px;
  background: var(--surface);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .01em;
  color: var(--fg-soft);
}
/* the caret that says this was typed by a person */
.said__q::before { content: '> '; color: var(--accent); }

.flow {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
  align-items: stretch;
}

.stop {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 18px 18px 20px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  cursor: pointer;
  /* Unlit is DIM, not hidden. Someone who never waits for the loop still sees the
     whole chain and can read it as a diagram. */
  opacity: .34;
  transition: opacity .22s var(--ease), border-color .22s var(--ease),
              background .22s var(--ease), transform .22s var(--ease);
}
.stop[data-on='1'] { opacity: 1; border-color: var(--border-2); }
.stop[data-us='1'][data-on='1'] { border-color: var(--accent-35); background: var(--accent-05); }

/* the connector. Fills as the run reaches it — the travel made visible without
   animating a token across a grid, which never survives a reflow. */
.stop + .stop::before {
  content: '';
  position: absolute;
  left: -14px;
  top: 46px;
  width: 14px;
  height: 1px;
  background: var(--border);
  pointer-events: none;
}
.stop[data-on='1'] + .stop::before,
.stop + .stop[data-on='1']::before { background: var(--accent-35); }

.stop__head { min-height: 62px; }
.stop__kind {
  display: block;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.stop[data-us='1'] .stop__kind { color: var(--accent); }
.stop__name {
  display: block;
  margin-top: 10px;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -.025em;
  color: var(--fg);
}
.stop__sub {
  display: block;
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: .05em;
  color: var(--fg-faint);
}

/* ---- the screen each stop shows while it is lit ----
   Fixed height so the four cards stay level whatever is inside them. A grid that
   reflows as each stop lights would make the whole section jump. */
.stop__screen {
  display: flex;
  flex-direction: column;
  gap: 5px;
  justify-content: center;
  min-height: 104px;
  margin: 14px 0 0;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--sunk);
  overflow: hidden;
}

/* children stagger in behind their stop lighting up */
.stop__screen > * {
  margin: 0;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 220ms var(--ease), transform 220ms var(--ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.stop[data-on='1'] .stop__screen > * { opacity: 1; transform: none; }

.slog { display: flex; gap: 9px; font-family: var(--mono); font-size: 10px; line-height: 1.5; }
.slog__k { flex: none; width: 40px; color: var(--accent); }
.slog__v { color: var(--fg-muted); }

/* `.frow`, not `.srow` — the storyboard already owns `.srow` at :1295 and this block,
   coming later in the file, silently overrode it and flattened every shot row on the
   page into a single mono line. Prefix anything new here. */
.frow { display: flex; align-items: baseline; gap: 8px; font-family: var(--mono); font-size: 10px; }
.frow b { color: var(--fg); font-weight: 600; }
.frow__size { color: var(--fg-faint); }
.frow__ref { margin-left: auto; color: var(--accent); }

.sbill__t { display: flex; align-items: baseline; gap: 8px; font-size: 12px; }
.sbill__t b { color: var(--fg); }
.sbill__each { margin-left: auto; font-family: var(--mono); font-size: 10px; color: var(--fg-faint); }
.sbill__dots { display: flex; gap: 4px; }
.sbill__dots i {
  width: 14px; height: 14px;
  border-radius: 3px;
  background: var(--accent);
}
.sbill__sum { font-size: 9px; letter-spacing: .06em; color: var(--fg-faint); }

.sfile { display: flex; align-items: center; gap: 9px; }
.sfile__t { flex: none; width: 34px; border-radius: 4px; overflow: hidden; }
.sfile__t img { display: block; width: 100%; height: 100%; object-fit: cover; }
.sfile__n { font-family: var(--mono); font-size: 9.5px; color: var(--fg-muted); word-break: break-all; }

.stop__line {
  margin: 14px 0 0;
  font-size: 12px;
  line-height: 1.55;
  color: var(--fg-muted);
}

@media (max-width: 980px) {
  .flow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stop + .stop::before { display: none; }
  .stop { opacity: 1; }        /* no run to follow on a small screen — show it all */
}
@media (max-width: 620px) {
  .flow { grid-template-columns: 1fr; }
}

/* ============================================================================
   THE TOUR — the second door.
   Big, dark, keyboard-first. Every step is at most three things: a grid of
   pictures, a title, and the keys. Nothing to read, everything to press.
   ============================================================================ */

.tour {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: none;
  padding: clamp(12px, 3vh, 40px) clamp(12px, 3vw, 48px);
  background: rgba(6, 7, 9, .82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.tour[data-open='1'] { display: flex; }

.tour__win {
  display: grid;
  grid-template-columns: 232px 1fr;
  width: 100%;
  max-width: 1240px;
  margin: auto;
  max-height: 100%;
  border: 1px solid var(--border-2);
  border-radius: 16px;
  background: var(--surface);
  box-shadow: 0 40px 120px rgba(0, 0, 0, .7);
  overflow: hidden;
  animation: tourIn .26s var(--ease);
}
@keyframes tourIn { from { transform: translateY(14px) scale(.99); opacity: 0; } }

/* ---- the kept rail ----
   Theo: "we save also the information that the user is selecting." It is on
   screen the whole way through rather than implied by a highlight somewhere. */

/* The rail does not exist until there is something in it. It used to sit open and
   empty on the first screen, which says the opposite of "we keep what you choose".
   It now opens ON the first pick, carrying that pick — the width animates, so the
   main pane widens into the space rather than jumping. */
.tour__kept {
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 18px 16px;
  border-right: 1px solid var(--line);
  background: var(--sunk);
  overflow-y: auto;
  overflow-x: hidden;
}
.tour__kept[data-on='0'] {
  width: 0;
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
  border-right-width: 0;
  opacity: 0;
}
.tour__kept .eyebrow { margin-bottom: 6px; }

/* a row that is genuinely new announces itself; rows already there do not move */
.tkept[data-new='1'] { animation: tkeptIn .34s var(--ease) both; }
@keyframes tkeptIn {
  from { opacity: 0; transform: translateX(-10px); }
  to   { opacity: 1; transform: none; }
}

.tkept__none { margin: 0; font-size: 11.5px; line-height: 1.55; color: var(--fg-faint); }

.tkept {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  animation: tkept-in .32s var(--ease);
}
@keyframes tkept-in { from { opacity: 0; transform: translateX(-6px); } }

.tkept__f {
  flex: 0 0 34px;
  width: 34px;
  aspect-ratio: 1 / 1 !important;
  border-radius: 6px;
}
.tkept__f--none { display: block; background: var(--surface-2); border: 1px solid var(--border); }
.tkept__t { display: flex; flex-direction: column; min-width: 0; }
.tkept__t i {
  font-style: normal;
  font-family: var(--mono);
  font-size: 8.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.tkept__t b {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tour__main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-height: 100%;
}

/* ---- the step stage ---- */

.tstage { display: flex; flex-direction: column; gap: 18px; }

/* the generating beat. A grid that simply appears claims nothing; one that
   resolves with the model name and the running cost on screen shows the thing
   the product is actually for. Every figure comes from FF.MODELS. */
.tgen {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 40px 0;
  max-width: 460px;
}
.tgen__h { margin: 0; font-size: 15px; font-weight: 600; color: var(--fg-soft); }
.tgen__bar { height: 3px; border-radius: 2px; background: var(--border); overflow: hidden; }
.tgen__bar i {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
  transition: width .3s var(--ease);
}
.tgen__m { margin: 0; font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-faint); }

/* the real tool, opened inline on `O` without leaving the step */
.ttool {
  padding: 18px;
  border: 1px solid var(--accent-35);
  border-radius: 12px;
  background: var(--sunk);
  animation: tkept-in .3s var(--ease);
}
.ttool__h { display: flex; align-items: baseline; gap: 10px; margin: 0 0 14px; }
.ttool__h b {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
}
.ttool__h i { font-style: normal; font-size: 10px; color: var(--fg-faint); }

/* ---- bar ---- */

.tour__bar {
  display: flex;
  align-items: center;
  gap: 16px;
  flex: none;
  padding: 0 16px;
  height: 50px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.tour__dots { display: flex; gap: 6px; }
.tour__dots i {
  width: 22px; height: 3px;
  border-radius: 2px;
  background: var(--border-2);
  transition: background .2s var(--ease);
}
.tour__dots i[data-on='1']   { background: var(--accent); }
.tour__dots i[data-past='1'] { background: var(--accent-35); }

.tour__crumb {
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.tour__x {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  padding: 0 12px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: transparent;
  color: var(--fg-muted);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
}
.tour__x:hover { border-color: var(--border-3); color: var(--fg); }

/* ---- body ---- */

.tour__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: clamp(20px, 3.5vh, 40px) clamp(18px, 3vw, 44px);
}

/* DEV-117 · what this whole thing IS, above what the step asks. Only the first
   step carries one — it is the only screen someone can land on without having
   read a word of the page behind it. It is deliberately the accent: "nothing is
   generated while you watch" is the sentence a cold visitor most needs, and the
   one they were previously left to work out for themselves. */
.tour__eyebrow {
  margin: 0 0 10px;
  color: var(--accent);
}

.tour__h {
  margin: 0 0 6px;
  font-size: clamp(21px, 2.5vw, 34px);
  font-weight: 700;
  letter-spacing: -.03em;
  color: var(--fg);
}
.tour__sub {
  margin: 0 0 clamp(18px, 3vh, 30px);
  font-size: 13.5px;
  color: var(--fg-muted);
  max-width: 62ch;
}

/* the picture grid — the step IS the pictures */
.tgrid { display: grid; gap: 12px; }
.tgrid[data-cols='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.tgrid[data-cols='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tgrid[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.tgrid[data-cols='5'] { grid-template-columns: repeat(5, minmax(0, 1fr)); }

/* one option. Arrives with a stagger so a step feels generated, not painted. */
.tcard {
  position: relative;
  display: block;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .34s var(--ease), transform .34s var(--ease),
              border-color .14s ease, box-shadow .14s ease;
}
.tcard[data-in='1'] { opacity: 1; transform: none; }
.tcard:hover { border-color: var(--border-3); }
/* elevation only — the border/bed come from THE selection treatment at the
   top of this file. The extra 1px accent ring was a second border and made
   this the one selected thing on the site that looked different. */
.tcard[data-sel='1'] { box-shadow: 0 16px 40px rgba(0, 0, 0, .5); }
.tcard .slot { border: 0; border-radius: 0; }

.tcard__k {
  position: absolute;
  left: 8px; top: 8px;
  z-index: 2;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: rgba(10, 11, 14, .78);
  backdrop-filter: blur(6px);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  color: var(--fg-soft);
}
.tcard[data-sel='1'] .tcard__k { background: var(--accent); color: var(--on-accent); }

.tcard__lab {
  display: block;
  padding: 10px 12px 12px;
  border-top: 1px solid var(--line);
}
.tcard__n { display: block; font-size: 13px; font-weight: 600; color: var(--fg); letter-spacing: -.01em; }
.tcard__s { display: block; margin-top: 2px; font-family: var(--mono); font-size: 10px; color: var(--fg-faint); }

/* ---- the shot list step ---- */

/* ══════════════════ THE TOUR'S REBUILT STEPS (W4) ══════════════════
   cast · look · shots · compose · inbox. The rule running through all of them:
   nothing on screen is a picture of the product — every one mounts the component
   the page already has, and the motion exists to make a mechanism legible rather
   than to decorate a slide. */

/* ---- cast: one photograph, as large as the pane allows ---- */
.tcast { display: flex; flex-direction: column; gap: 12px; opacity: 0; transform: translateY(8px);
         transition: opacity .34s var(--ease), transform .34s var(--ease); }
.tcast[data-in='1'] { opacity: 1; transform: none; }
.tcast__sheet { border-radius: 12px; overflow: hidden; border: 1px solid var(--border); }
.tcast__sheet img { width: 100%; display: block; }
.tcast__cap { margin: 0; display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.tcast__cap b { font-family: var(--mono); font-size: 12px; letter-spacing: .1em; color: var(--accent); }
.tcast__cap span { font-size: 11.5px; color: var(--fg-muted); }
.tcast__strip { display: flex; gap: 7px; }
.tcast__v { flex: 1; min-width: 0; border-radius: 7px; overflow: hidden; opacity: .5; }
.tcast__v img { width: 100%; display: block; }

/* ---- look: four named looks, one click (DEV-106) ----
   The reel that used to live here is gone — it jumped, it settled by itself, and
   while it was running it swallowed the first ⏎. The cards are `.tcard`, so the
   selected look wears the ONE selected recipe at the end of this file. */
.tlook { display: flex; flex-direction: column; gap: 12px; }
.tlook__cap { margin: 0; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.tlook__cap b { font-family: var(--mono); font-size: 12px; letter-spacing: .08em; color: var(--accent); }
.tlook__cap span { font-size: 11px; color: var(--fg-faint); }
.tlook__blk {
  margin: 0; font-size: 10px; line-height: 1.7; color: var(--fg-faint);
  max-height: 74px; overflow-y: auto;
}

/* ---- shots: scene headers over the real rows ---- */
.tscene { margin: 14px 0 8px; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.tscene b { font-size: 10px; letter-spacing: .14em; color: var(--fg); }
.tscene i { font-style: normal; font-size: 11px; color: var(--fg-muted); }
.tscene span { font-size: 9px; letter-spacing: .06em; color: var(--fg-faint); margin-left: auto; }
.tgenbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
.tgenbar__s { font-size: 9.5px; letter-spacing: .05em; color: var(--fg-faint); }

/* ---- the three phases of DEV-107, and they must never overlap ----
   held   → not written yet: the node is not on screen at all.
   writing→ written, not rendered: a dashed row with its frames absent.
   gen    → rendered: solid accent border, the frames fade up.  */
.tscene[data-hold='1'] { display: none; }
.srow[data-hold='1'] { display: none; }
.srow[data-writing='1'] { border-style: dashed; }
.srow[data-writing='1'] .srow__t,
.srow[data-writing='1'] .srow__strip,
.srow[data-writing='1'] .srow__tally,
.srow[data-writing='1'] .srow__takes { opacity: 0; }
/* a row that has "generated" — the frame settles in, the row stops being pending */
.srow[data-gen='1'] { border-color: var(--accent-35); }
.srow[data-gen='1'] .srow__t,
.srow[data-gen='1'] .srow__strip { animation: genIn .4s var(--ease) both; }
@keyframes genIn { from { opacity: .25; filter: saturate(.4); } to { opacity: 1; filter: none; } }

/* ---- compose: the substeps the visitor drives (DEV-108) ----
   ONE instruction on screen at a time, and the composer arrives EMPTY. The old
   `.tmake__line / __chips / __keys / __k` block is gone with the screen that
   showed a finished shot next to a rail of keys nobody could press. */
.tmake { display: flex; flex-direction: column; gap: 14px; }
.tmake__now { margin: 0; display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.tmake__now i {
  font-style: normal; font-family: var(--mono);
  font-size: 10px; letter-spacing: .14em; color: var(--accent);
}
.tmake__now b { font-size: 18px; font-weight: 600; letter-spacing: -.01em; color: var(--fg); }
.tmake__now span { font-size: 12px; color: var(--fg-muted); }
.tmake__go { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
/* `display:flex` beats the UA's [hidden] rule, so the + New shot button stayed on
   screen after it had been pressed. Say it here or `.hidden = true` does nothing. */
.tmake__go[hidden], .tmake__card[hidden], .tmake__cost[hidden] { display: none; }
.tmake__card {
  position: relative;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--sunk); padding: 14px 16px;
}
.tmake__h { margin: 0 0 10px; display: flex; gap: 10px; align-items: baseline; }
.tmake__h b { font-size: 9.5px; letter-spacing: .14em; color: var(--fg-faint); }
.tmake__h i { font-style: normal; font-size: 9.5px; color: var(--fg-faint); margin-left: auto; }
.tmake__write {
  display: block;
  white-space: normal;
  overflow: visible;
  font-size: 14px;
  line-height: 1.75;
  min-height: 46px;
}
/* the site's own focused-field idiom (`.notify__in`, `.buyform__in`, `.pb__ta`,
   `.tmail__in`): no ring on the box, the CARD's border says it is live. The
   bundled island.css sets a global 2px accent `:focus-visible` outline, which
   around a 46px block reads as a lime rectangle rather than as a text cursor. */
.tmake__write:focus, .tmake__write:focus-visible { outline: none; }
.tmake__card:focus-within { border-color: var(--accent-35); }
/* in flow, not floated: the tour body scrolls and an absolute menu is clipped */
.tmake__menu { position: static; left: auto; right: auto; top: auto; margin-top: 12px; max-height: 236px; }
.tmake__cost { margin: 0; font-size: 11px; line-height: 1.6; color: var(--fg-muted); }
.tmake__cost b { color: var(--fg); letter-spacing: .04em; }
.tmake__out { display: flex; flex-direction: column; gap: 8px; }

/* ---- inbox: the same queue, inside the tour overlay ---- */
.tinbox { display: flex; flex-direction: column; gap: 12px; }
.tinbox__stage { min-height: 220px; }
.tinbox__acts { display: flex; gap: 6px; flex-wrap: wrap; }
.tinbox__q { display: flex; gap: 5px; }
.tinbox__q .qslot { flex: 1; }
.tinbox__done { margin: 0; font-size: 13px; line-height: 1.65; color: var(--fg-muted); }
.tinbox__done b { color: var(--accent); }

.tshots { display: grid; gap: 10px; }
.tshot {
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  gap: 14px;
  align-items: center;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--surface-2);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .3s var(--ease), transform .3s var(--ease);
}
.tshot[data-in='1'] { opacity: 1; transform: none; }
.tshot .slot { border-radius: 8px; }
.tshot__id {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .12em;
  color: var(--accent);
}
.tshot__p { margin: 5px 0 0; font-family: var(--mono); font-size: 11px; line-height: 1.6; color: var(--fg-muted); }
.tshot__p em { font-style: normal; color: var(--fg); }
.tshot__p s { text-decoration: none; color: var(--fg-faint); }

/* ---- the done step ---- */

.tdone { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 22px; align-items: center; }
.tdone__tree {
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--sunk);
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 2;
  color: var(--fg-muted);
}
.tdone__tree b { color: var(--accent); font-weight: 500; }
.tdone__say { font-size: 15px; line-height: 1.6; color: var(--fg-soft); }
.tdone__say b { color: var(--fg); }

/* ---- the one ask on the whole site ----
   It sits at the end of the tour rather than in the hero because this is the only
   moment a visitor has actually built something. The ask is "keep what you just
   made", and the setup they configured really is attached to the record. Nothing is
   gated behind it — the buy button sits alongside and works regardless. */
.tmail { grid-column: 1 / -1; margin-top: 4px; }
.tmail__h {
  margin: 0 0 9px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.tmail__row { display: flex; gap: 8px; max-width: 460px; }
.tmail__in {
  flex: 1;
  min-width: 0;
  padding: 11px 14px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--sunk);
  font-family: var(--mono);
  font-size: 12px;
  color: var(--fg);
}
.tmail__in::placeholder { color: var(--fg-faint); }
.tmail__in:focus { outline: none; border-color: var(--accent-35); }
.tmail__go {
  flex: none;
  padding: 11px 18px;
  border: 1px solid var(--border-2);
  border-radius: 8px;
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 600;
  color: var(--fg);
  cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.tmail__go:hover:not(:disabled) { border-color: var(--accent-35); background: var(--accent-05); }
.tmail__go:disabled { opacity: .6; cursor: default; }

.tmail__say { margin: 8px 0 0; min-height: 15px; font-family: var(--mono); font-size: 10px; letter-spacing: .04em; }
.tmail__say[data-tone='ok'] { color: var(--ok); }
.tmail__say[data-tone='warn'] { color: var(--warn); }

/* answered — the field steps back rather than vanishing, so the answer has
   somewhere to sit and the layout does not jump */
.tmail[data-done='1'] .tmail__row { opacity: .45; pointer-events: none; }

/* ---- foot ---- */

.tour__foot {
  display: flex;
  align-items: center;
  gap: 16px;
  flex: none;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}
.tour__note {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .06em;
  color: var(--fg-faint);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tour__acts { margin-left: auto; display: flex; gap: 8px; flex: none; }

.btn-ghost--lg { height: 44px; padding: 0 20px; gap: 10px; }

@media (max-width: 860px) {
  .tgrid[data-cols='4'], .tgrid[data-cols='5'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tgrid[data-cols='3'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tdone { grid-template-columns: 1fr; }
  .tshot { grid-template-columns: 92px minmax(0, 1fr); }
  .tour__note { display: none; }
}

/* ---------- 01 · the spine ----------
   The section that mounts the app's real Story tab (see the comment on #spine in
   index.html). Almost nothing belongs here: the island's own container rules
   ship inside the bundle at landingpage/island/island.css, so the box travels
   with the thing that needs it. What is left is only the section furniture the
   static page owns. */

.sec--spine { background: var(--sunk); }

.spine__foot {
  max-width: 92ch;
  margin: 16px 0 0;
  font-size: 10.5px;
  line-height: 1.6;
  letter-spacing: .02em;
  color: var(--fg-faint);
}

/* Before the bundle mounts — and forever, if it 404s — the section is an empty
   box rather than a collapsed one, so the page never reflows under the reader. */
[data-island]:not([data-mounted]) { min-height: 520px; }

/* ============================================================================
   FOCUS — the tie-breaker (DEV-117)

   The treatment is declared once, at the TOP of this file with the other shared
   atoms. This repeats it at a specificity that beats a bare `outline: none`,
   because six controls answered a Tab key with nothing visible at all: three
   buttons carrying `outline: none` (`.wfil__clear`, `.pk__done`, `.tour__x`)
   and three text fields that swapped the ring for a border tint on a dark
   surface (`.pb__ta`, `.pk__search`, `.notify__in`, and `.buyform__in` /
   `.tmail__in` on the checkout pages).

   The border tints stay. They are a SECOND cue on the field you are typing in,
   not the only one.

   ONE exclusion, immediately below: `.tmake__write`, the tour's note field,
   which is a contenteditable inside a card that lights on `:focus-within`.
   ============================================================================ */
:is(a[href], button, input, select, textarea, [tabindex], [contenteditable='true']):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.tmake__write:focus-visible { outline: none; }

/* ============================================================================
   THE READING GATE — js/gate.js

   Layer 100, above all three existing overlays (palette 60, tour 80, viewer 90),
   because it is the only one that is not dismissible and must never be found
   underneath something.

   No `filter: blur()` on the page behind it. The scrim is a backdrop-filter on the
   overlay itself, which is what .tour and .lb already do — blurring the page's own
   subtree would give every one of the 128 wall frames a new compositing layer for
   one modal.
   ============================================================================ */

/* Freezing the page is NOT `html { overflow: hidden }`. Hiding the viewport's own
   overflow makes it unscrollable in both directions at once — including
   programmatically — and the browser clamps the scroll position to 0 on the spot. A
   reader four sections down gets yanked to the hero the instant the panel appears,
   and there is no scrollTo that can put them back while it is up.

   So: take the body out of flow at a negative offset equal to where they were, and
   put it back on release. js/gate.js owns the offset — it is the only number here
   the stylesheet cannot know. */
html[data-gated='1'] body {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
  overflow: hidden;
}

.gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  place-items: center;
  padding: clamp(16px, 5vh, 56px) clamp(16px, 5vw, 56px);
  background: rgba(6, 7, 9, .86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.gate[data-open='1'] { display: grid; }

.gate__win {
  width: min(460px, 100%);
  max-height: 100%;
  overflow-y: auto;
  padding: clamp(24px, 4vw, 36px);
  border: 1px solid var(--border-2);
  border-radius: 16px;
  background: var(--surface);
  box-shadow: 0 40px 120px rgba(0, 0, 0, .7);
  animation: gateIn .3s var(--ease);
}

@keyframes gateIn {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.gate__eyebrow { color: var(--accent); margin: 0 0 14px; }

.gate__title {
  margin: 0 0 12px;
  font-size: clamp(19px, 2.4vw, 23px);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -.01em;
  color: var(--fg);
}

.gate__lede {
  margin: 0 0 22px;
  font-size: 13px;
  line-height: 1.65;
  color: var(--fg-muted);
}

.gate__form { display: flex; gap: 8px; }

.gate__in {
  flex: 1;
  min-width: 0;
  height: 40px;
  padding: 0 14px;
  border: 1px solid var(--border-2);
  border-radius: 10px;
  background: var(--sunk);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 13px;
}
.gate__in::placeholder { color: var(--fg-faint); }
.gate__in:focus { border-color: var(--accent-35); }

/* The code field is the one place on this page where a monospace, wide-tracked
   field is not decoration: six digits read off a phone get miscounted otherwise. */
.gate__in--code {
  font-family: var(--mono);
  font-size: 18px;
  letter-spacing: .42em;
  text-align: center;
  padding-right: 0;                 /* the tracking adds a phantom column on the right */
}

.gate__go { flex: none; height: 40px; }

.gate__say {
  margin: 14px 0 0;
  min-height: 17px;                 /* reserve it — a message must not shift the footer */
  font-size: 12px;
  line-height: 1.4;
  color: var(--fg-muted);
}
.gate__say[data-tone='ok'] { color: var(--ok); }
.gate__say[data-tone='warn'] { color: var(--warn); }

.gate__foot {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}
/* `hidden` is a UA rule (`[hidden] { display: none }`) and a class that sets
   `display` outranks it — so an author `display: flex` silently defeats
   `node.hidden = true`. Every element in this panel that JS hides AND that carries a
   display of its own needs the attribute spelled out. That is this one. */
.gate__foot[hidden] { display: none; }

.gate__alt, .gate__bail {
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg-faint);
  font-family: var(--sans);
  font-size: 12px;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border-3);
}
.gate__alt:hover:not(:disabled), .gate__bail:hover { color: var(--fg-soft); }
.gate__alt:disabled { cursor: default; text-decoration: none; color: var(--fg-faint); opacity: .7; }

/* The way out sits right, apart from the resend, and only ever appears when the
   visitor has hit a dead end — see js/gate.js. It is deliberately quiet, not hidden:
   a link nobody can find is the same as no link at all. */
.gate__bail { margin-left: auto; }
.gate__bail[hidden] { display: none; }

@media (max-width: 480px) {
  .gate__form { flex-direction: column; }
  .gate__go { width: 100%; }
}

/* ============================================================================
   THE SELECTION TREATMENT — the rule itself.

   Declared LAST on purpose. Every one of these components also has a `:hover`
   or a state rule of the same specificity further up this file, and the copies
   this replaces each sat immediately after their own — so the only way one
   declaration can stand in for a dozen is for it to come after all of them.
   The tokens and the reasoning are at the top of the file, with :root.

   Two selectors carry an exclusion rather than an exception rule:
     · `:not(.wchip--seg)` — the wall's view/group controls keep their neutral
       selected state (something is always selected there; --accent means live).
     · `:not([data-done])` — a queue slot that has been judged keeps its verdict
       colour; "current" must not paint over "killed".
   ============================================================================ */

.mrow[data-sel='1'],
.pal__item[data-sel='1'],
.trow[data-active='1'],
.chip[data-on='1'],
.wchip:not(.wchip--seg)[data-on='1'],
.mtab[data-on='1'],
.prow[data-on='1'],
.pick__opt[data-on='1'],
.pmode[data-on='1'],
.pcard[data-on='1'],
.tcard[data-sel='1'],
.stake[data-sel='1'],
.qslot[data-now='1']:not([data-done]),
.craft__t[data-on='1'],
.pick__t[data-on='1'] {
  border-color: var(--sel-bd);
  background: var(--sel-bg);
  color: var(--sel-ink);
}

/* a take chip's border is WHICH ONE YOU ARE LOOKING AT; its state is the badge.
   Selection therefore outranks state, and a killed take you have opened comes
   back to full strength — you cannot review what is dimmed to 60%. */
.stake[data-state][data-sel='1'] { border-color: var(--sel-bd); background: var(--sel-bg); }
.stake[data-state='killed'][data-sel='1'] { opacity: 1; }
