/* ReelView — one stylesheet, no framework. Light app shell, dark video surfaces.
   Two scenes drove that split: uploading happens on a phone in Australian sun (forces light,
   high contrast); reviewing sixteen clips happens at a desk at night (forces dark around video,
   which is why every NLE is dark). Theme is per-surface, not global. */

:root {
  color-scheme: light;

  --bg:        oklch(0.985 0.003 70);
  --surface:   oklch(1 0 0);
  --surface-2: oklch(0.965 0.004 70);
  --line:      oklch(0.90 0.005 70);
  /* 0.65, not the 0.82 this started at: --line-firm is the entire visible boundary of every
     quiet button, input and icon button, and at 0.82 it sat at 1.7:1 — under WCAG 1.4.11's 3:1
     for perceiving a control at all. --line stays soft: a row divider is decoration, not a
     control, and 1.4.11 does not reach it. */
  --line-firm: oklch(0.65 0.006 70);

  --ink:       oklch(0.24 0.008 70);
  --ink-2:     oklch(0.47 0.008 70);
  /* 0.54 clears 4.5:1 even on --surface-2 (hovered rows, the icon button). At the 0.62 it was,
     "Queued" and every hint missed AA at 0.875rem — too small for the large-text exemption. */
  --ink-3:     oklch(0.54 0.007 70);

  /* Ochre, not purple. Purple-on-white is the AI-tool reflex and this product's credibility
     rests on restraint about what the AI did. Actions and selection only, never decoration. */
  --accent:    oklch(0.52 0.095 68);
  --accent-in: oklch(0.46 0.10 68);
  --on-accent: oklch(0.99 0.002 70);

  --ok:        oklch(0.48 0.09 150);
  --warn:      oklch(0.52 0.11 62);
  /* Destructive only — removing someone, and nothing else. Same lightness as --warn so it reads
     as part of this palette and still clears 4.5:1 on white. */
  --bad:       oklch(0.52 0.19 27);
  --bad-bg:    oklch(0.96 0.02 27);

  --radius: 8px;
  --pad: 1rem;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

* { box-sizing: border-box; }

html {
  /* Reserve the scrollbar gutter always. Without it a short page has no scrollbar and a long
     one does, so the viewport differs by its width and every centred element — wordmark, nav,
     footer — shifts a few pixels as you navigate. Cheaper and steadier than any JS. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 400 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* One measure for the FRAME, whatever the content does. The masthead, the workflow bar and the
   footer all use --frame, so the wordmark sits in the same place on every page — with two
   measures they moved 140px as you navigated between a narrow page and a wide one, which reads
   as the whole site shifting rather than the content changing. */
:root { --frame: 1180px; }

/* One width for the workflow pills and the masthead CTA. They sit one above the other in the
   same header, so a pill measured by its longest label and a button measured by its own text
   read as two unrelated controls stacked by accident. */
:root { --pill: 8.5rem; }

.wrap { max-width: 900px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }

/* The job screens are a working surface, not a reading one: a scene row carries a grip, a
   thumbnail, a name, a shot description and three controls, and at 900px those crowd while the
   prose limits elsewhere (66ch) keep the rest of the site readable. Wider only where it earns
   it — the legal pages and the landing copy stay narrow. */
.wrap.wide { max-width: var(--frame); }

/* --- masthead ------------------------------------------------------------ */

/* Sticky: the plan and review screens scroll for sixteen scenes, and the balance and the way
   back should never be a scroll away. Translucent + blur so footage and photo grids read as
   passing under it rather than colliding with it. */
.top {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in oklch, var(--surface) 88%, transparent);
  -webkit-backdrop-filter: saturate(1.6) blur(10px);
  backdrop-filter: saturate(1.6) blur(10px);
  border-bottom: 1px solid var(--line);
}
/* Same measure as .steps-bar and .wrap, so the wordmark sits above the page's left edge and
   the nav above its right, instead of both being flung to the window edges. */
/* Grid, not space-between: the middle cell has to sit on the CENTRE of the frame, and with
   flex it would sit wherever the wordmark and the nav left it — drifting as the balance goes
   from "9 credits" to "12 credits". Equal 1fr shoulders pin it. With no CTA (a stranger) the
   middle column collapses to nothing and the result is what space-between drew before. */
.topline {
  max-width: var(--frame); margin: 0 auto;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem;
  padding: 0.85rem 1.25rem;
}
/* Columns named explicitly, not left to auto-placement. When there is no CTA — a stranger, a
   legal page — the element is absent rather than empty, and auto-placement drops the nav into
   the middle column and centres it. Pinning it to column 3 keeps the right edge the right edge. */
.topline .mark { grid-column: 1; justify-self: start; }
/* Louder text, same box. .btn carries a 44px tap target, which next to a 32px Log out made the
   masthead look like it had swallowed something — so the height is pinned to match the controls
   beside it and the emphasis comes from the type instead. The tap target comes back on mobile
   below, where the button has its own row and thumbs are the input. */
/* `a.cta`, not `.cta`: .btn.small is the same specificity and sits further down the file, so it
   quietly won the font-size and the button came out at 14px regular. */
.topline a.cta {
  grid-column: 2;
  min-height: 0; height: 2rem; width: var(--pill); padding: 0 0.85rem;
  /* margin: 0 kills the inherited bottom margin. It is invisible but it is part of the grid
     item's box, so align-items centred button+margin and hung the button ~5px above the Log out
     beside it — while adding its whole height to the masthead. */
  margin: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.9375rem; font-weight: 600;
}
.topline nav   { grid-column: 3; justify-self: end; }

/* --- workflow steps ------------------------------------------------------
   Where the job is, and what is behind it. Lives in the masthead because a job is one URL
   whose shape changes with its phase — without this the agent can see what the current screen
   wants but not what is left or what a click will cost.

   Three states, each carrying a non-colour signal as well: done has a tick and is a link,
   current is filled and marked aria-current, ahead is dimmed and is not a link at all. */
/* The bar spans the full window (it lives in the masthead), but its CONTENTS have to line up
   with the page: same max-width and auto margins as .wrap, or "centred" means centred on the
   viewport while the content sits in a 1180px column, and the escape pins to the window edge
   rather than the content edge. That mismatch is what made it look off.
   Grid `1fr auto 1fr`: the steps occupy the middle column, so they stay put as the escape
   label changes width — with flex the whole run would shift when Cancel became Leave. */
.steps-bar {
  border-top: 1px solid var(--line);
  max-width: var(--frame); margin: 0 auto; padding: 0.4rem 1.25rem 0.55rem;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.5rem;
}
.steps-bar ol {
  grid-column: 2;
  display: flex; align-items: center; justify-content: center; gap: 0.3rem;
  list-style: none; margin: 0; padding: 0;
  overflow-x: auto; scrollbar-width: none;
}
.steps-bar ol::-webkit-scrollbar { display: none; }
/* Far left of the content column, directly under the wordmark — the bar shares .topline's frame
   and padding, so column 1 starts exactly where the mark does. Named explicitly rather than left
   to auto-placement: with no Cancel rendered, the steps would otherwise slide into its column.
   The compound selector is deliberate — `.top a` sets colour at 0-1-1 and would beat a lone
   `.step-back`, which is how the masthead buttons ended up with unreadable text once already. */
.step-back { grid-column: 1; justify-self: start; }
/* The same metrics as Cancel in the opposite shoulder, so the two ends of the bar are a matched
   pair rather than two different-sized controls. NOT `.btn.small`: that is (0,2,0) and sits
   later in this file, so it would win the tie on font-size and padding and quietly resize this
   — exactly how the masthead CTA came out 44px tall. `a.step-back.btn` is (0,2,1) and does not
   depend on source order.
   nowrap because at 400px the 1fr shoulder squeezes the label to its longest word, and
   "‹ Reels" over two lines makes the bar taller than the steps beside it. */
a.step-back.btn {
  min-height: 0; padding: 0.3rem 0.7rem; font-size: 0.8125rem; min-width: 69px;
  white-space: nowrap;
}
/* Far right of the content column, and always in column 3 whether or not it is rendered. */
.step-esc { grid-column: 3; justify-self: end; margin: 0; }
/* Same metrics AND the same width as Log out directly above it: they sit in the same corner of
   two stacked bars, and "Cancel" being seven pixels narrower than "Log out" read as a wobble in
   the right edge rather than as two different buttons. min-width, not width, so a longer label
   still fits. */
.step-esc button { min-height: 0; padding: 0.3rem 0.7rem; font-size: 0.8125rem; min-width: 69px; }
a.step-esc.quiet-link {
  font-size: 0.8125rem; color: var(--ink-3); text-decoration: none;
}
a.step-esc.quiet-link:hover { color: var(--accent-in); text-decoration: underline; }
/* Every step the same width, so the run does not reflow as labels change and the bar reads as
   five equal stages rather than five differently-important ones. 8.5rem fits the longest
   ("Review" plus its tick) with room to spare. */
/* The li is the flex ROW holding [connector][box], so the ::before below lands beside the box
   rather than inside it. Getting this wrong once put the chevron in the pill: with the box
   styling applied to the li itself, its ::before became a flex item within the pill's own row. */
/* flex: none, or the equal width above is a suggestion the browser overrules. The li is a flex
   item in a container that runs out of room on a phone, so the default flex-shrink:1 shrank every
   pill to its own longest word — 53/37/52/51/47px where the rule says one width — and the run
   read as five differently-important stages, which is the exact thing it exists not to do. */
.step {
  display: flex; align-items: center; flex: none;
  font-size: 0.8125rem; white-space: nowrap;
}
/* var(--radius), matching button/.btn — the pill radius made these look like a different family
   of control from everything else on the page. */
/* `.step.done` as well as `.step.done a`: Photos is done AND unlinked once rendering starts
   (replan refuses after that), so it matched neither the anchor rule nor .now/.todo and rendered
   as bare 40px text while every other step was a 136px box. */
.step a, .step.now, .step.todo, .step.done {
  display: flex; align-items: center; justify-content: center; gap: 0.3rem;
  width: var(--pill); box-sizing: border-box;
  padding: 0.3rem 0.6rem; border: 1px solid transparent; border-radius: var(--radius);
}
/* A LINKED done step is two boxes: the <li> matches .step.done and the <a> inside it matches
   .step a, so the li's own padding squeezed the anchor and the green border drew at 115px beside
   a 136px fill — finished steps read as smaller controls than the current one. Where there is an
   anchor, the li is only a wrapper and the anchor is the pill. */
/* border: 0 too, or the transparent 1px each side makes the wrapper 138px and nudges the
   connector beside it. */
.step.done:has(a) { padding: 0; width: auto; border: 0; }
/* An unlinked done step is history, not a control: the same green border as its linked siblings
   so the run still reads as one sequence, but quieter, since there is nothing to click.
   Quieter is the BORDER, not the text. `opacity: 0.75` on the whole box composited --ink-2 down
   to 3.74:1, and the agent pays for a stylistic dimming with the label they were reading. */
.step.done:not(:has(a)) {
  color: var(--ink-2);
  border-color: color-mix(in oklch, var(--ok) 45%, transparent);
}
.step a { color: var(--ink-2); text-decoration: none; }
/* The tick is absolutely positioned so it cannot shift the centred label: with it in flow, a
   done step's text sat off-centre against its neighbours by the width of the glyph. */
/* .step.done included: without it an unlinked done step (Photos, after rendering starts) had no
   positioned ancestor, so its absolutely-placed tick escaped to the far left of the whole bar. */
.step a, .step.now, .step.todo, .step.done { position: relative; }
.step-tick { position: absolute; left: 0.55rem; }

/* done — green border, the work is behind you and you may go back to it */
.step.done a { border-color: var(--ok); color: var(--ink); }
.step.done a:hover { background: color-mix(in oklch, var(--ok) 12%, transparent); }
.step-tick { color: var(--ok); font-size: 0.75rem; }
/* An empty tick span on a not-done step must not reserve space or draw a box. */
.step:not(.done) .step-tick { display: none; }

/* current — filled, the one place colour alone would have been ambiguous */
.step.now { background: var(--accent); color: var(--on-accent); font-weight: 500; }
.step.now .step-tick { color: var(--on-accent); }

/* ahead — not reached, not clickable, and visibly so. It keeps a border because five equal
   boxes read as one sequence of stages; three boxes and two bits of floating text do not. The
   border is --line (decorative), not --line-firm, precisely because this is NOT a control.
   No opacity. --ink-3 is already the quiet floor DESIGN.md sets (5.05:1); the 0.55 that used to
   sit here composited it to 2.18:1 and failed WCAG 1.4.3 on every job screen. "Ahead" is carried
   by the --line border, the absent tick and the absent link — three signals that cost nothing to
   read. */
.step.todo { color: var(--ink-3); border-color: var(--line); }

/* The same chevron the scene rows use (\203A, --ink-3), between each pair of steps. It is a
   flex item in its own right rather than a ::before on the label — that was the earlier
   mistake: attached to a centred label it sat against the label's left edge and read as
   punctuation stuck to the word, instead of sitting midway between the two boxes. */
.steps-bar ol { gap: 0.35rem; }
.step-sep {
  flex: none; align-self: center;
  color: var(--ink-3); font-size: 1.25rem; line-height: 1;
}
/* One row needs 959px. Measured, not guessed: two 69px shoulders, 40px of frame padding, 16px of
   grid gap and an ol of 5x136 + 4 chevrons + 8 gaps = 753. Below that the run does not fit beside
   the shoulders, and what it did instead was shrink — 53/37/52/51/47px at 320, inside a 122px
   scroller with `scrollbar-width: none`. What an agent actually saw on a phone was
   "Plan › Render ›" clipped mid-chevron: no sign the arc has five stages, and Photos — the one
   step you are allowed to go BACK to — scrolled out of sight with nothing to say so.

   So below 959 the stages get their own row, which is 291px even at 320 and enough for five real
   labels once the chevrons go. The chevrons go because they are the only thing here carrying no
   information: aria-hidden already, and five equal boxes in a row read as a sequence without them.

   Grid, not flex: `minmax(0, var(--pill))` makes the stages exactly equal at every width, capped
   at the desktop pill so a 900px window does not hand them 170px each, and nothing can shrink
   below its share or overflow the row. `auto-flow: column` rather than repeat(5) so the rule
   survives JOB_STEPS gaining or losing a stage.

   Row order is shoulders, then stages: Cancel stays directly under Log out in the same corner,
   which is the pairing the .step-esc rule above exists to preserve. DOM order is unchanged, so
   a keyboard user tabs Reels -> stages -> Cancel, one row back up at the end. */
@media (max-width: 959px) {
  .steps-bar { grid-template-columns: auto auto; justify-content: space-between; row-gap: 0.4rem; }
  .step-back { grid-area: 1 / 1; }
  .step-esc  { grid-area: 1 / 2; }
  .steps-bar ol {
    grid-area: 2 / 1 / 3 / 3;
    display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, var(--pill));
    justify-content: center; gap: 0.25rem; overflow: visible;
  }
  .step-sep { display: none; }
  .step a, .step.now, .step.todo, .step.done { width: 100%; }
}
@media (max-width: 640px) {
  .steps-bar { padding: 0.35rem 0.9rem 0.5rem; }
  .step { font-size: 0.75rem; }
  .step a, .step.now, .step.todo, .step.done { padding: 0.25rem 0.15rem; gap: 0.2rem; }
  /* In flow, not absolute. At 55px a pill has no margin for a glyph parked over its left padding,
     and the tick landed on the P of Photos. Equal columns mean the BOXES still line up, which is
     what the absolute positioning was protecting; a label sitting a few px off its own centre is
     not something anyone can see across five boxes. */
  .step-tick { position: static; font-size: 0.625rem; }
}
.top a { color: inherit; }
/* ...but not a FILLED button. At 0-1-1 the rule above beat .btn's own colour (0-1-0), so every
   accent button in the masthead — this one and Sign up — drew the masthead's dark ink on the
   accent fill: 2.91:1, which fails AA outright. White on accent measures 5.64:1. */
.top a.btn { color: var(--on-accent); }
/* ...but a QUIET button is not filled, so on-accent white would put it on a transparent
   background and make it invisible. (0,3,1) beats the rule above, which is (0,2,1). */
.top a.btn.quiet { color: var(--ink); }
.mark { font-weight: 500; letter-spacing: -0.01em; text-decoration: none; }
/* `.topline nav`, NOT `.top nav`: the steps bar is also a <nav> inside .top, and at 0-1-1 this
   rule beat .steps-bar's own 0-1-0 display:grid — so the workflow silently laid itself out as a
   flex row, refusing to centre and dragging the escape link in behind it. */
.topline nav { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; color: var(--ink-2); }
/* The agency's mark, last in the masthead. Contained and height-capped so a wide lockup and a
   square badge both sit on the same line as Log out without changing the bar's height. */
.nav-logo { height: 1.75rem; width: auto; max-width: 7rem; object-fit: contain; flex: none; }
@media (max-width: 560px) { .nav-logo { display: none; } }   /* the row is already tight */
@media (max-width: 640px) {
  /* Wordmark and nav alone fill a 360px phone, so the CTA takes its own row rather than
     squeezing the balance off the screen. It is the action the agent came for — a full-width
     bar under the masthead is a better phone control than a centred one nobody can hit. */
  .topline { padding: 0.6rem 0.9rem; grid-template-columns: auto 1fr; }
  .topline nav { gap: 0.7rem; font-size: 0.8125rem; grid-column: 2; }
  /* Its own row, full width, and back to a full tap target — this is a thumb, not a cursor. */
  .topline a.cta { grid-row: 2; grid-column: 1 / -1; width: auto; height: auto; min-height: 44px; }
}

/* --- landing page -------------------------------------------------------
   The only page a stranger sees. Same restraint as the app: no gradients, no drop shadows, one
   accent used only on actions. An agent reselling this under their own brand is not helped by us
   looking like an AI startup. */

.hero { padding: 1.5rem 0 0.5rem; }
.hero h1 {
  font-size: clamp(1.75rem, 5vw, 2.6rem); line-height: 1.12; letter-spacing: -0.025em;
  max-width: 16ch; margin-bottom: 0.75rem;
}
.lede { font-size: clamp(1rem, 2.2vw, 1.15rem); color: var(--ink-2); max-width: 54ch; margin: 0 0 1.5rem; }
.lede b { color: var(--ink); }
.cta { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; margin: 0 0 0.6rem; }
.btn.small { padding: 0.35rem 0.7rem; font-size: 0.875rem; }

/* --- video hero ---------------------------------------------------------
   Footage behind the words, because the product is the footage. Breaks the .wrap margin to run
   full-bleed, then re-inflates its own gutter. The scrim is not decoration: white text over
   arbitrary listing footage is a contrast lottery, and a bright kitchen would take the headline
   to 1.5:1. A near-opaque left-weighted gradient keeps it past 4.5:1 whatever is playing. */
.hero-video {
  position: relative; isolation: isolate; overflow: clip;
  margin: -1.5rem calc(50% - 50vw) 2.5rem;
  padding: clamp(2.5rem, 9vw, 5.5rem) max(1.25rem, calc(50vw - 450px));
  min-height: min(66vh, 560px);
  display: flex; align-items: center;
  background: oklch(0.22 0.006 70);
}
.hero-bg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: -2; border: 0;
}
.hero-video::after {          /* the scrim */
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(100deg,
    oklch(0.18 0.006 70 / 0.94) 0%,
    oklch(0.18 0.006 70 / 0.86) 42%,
    oklch(0.18 0.006 70 / 0.52) 100%);
}
.hero-copy { max-width: 40rem; }
.hero-video h1 { color: oklch(0.99 0.002 70); }
.hero-video .lede { color: oklch(0.90 0.004 70); }
.hero-video .lede b, .hero-video .hint { color: oklch(0.99 0.002 70); }
.hero-video .hint { opacity: 0.75; }
/* A quiet button on a dark ground: the light-surface border and ink would vanish. */
.btn.on-dark {
  background: transparent; color: oklch(0.99 0.002 70);
  border-color: oklch(0.99 0.002 70 / 0.5);
}
.btn.on-dark:hover { background: oklch(1 0 0 / 0.14); border-color: oklch(0.99 0.002 70 / 0.8); }

/* Someone who has asked the OS to stop moving things means it. The poster frame stays, so the
   hero still shows a real room — it simply does not play. */
@media (prefers-reduced-motion: reduce) {
  .hero-bg { display: none; }
  .hero-video { background: oklch(0.22 0.006 70) center/cover; }
}

/* --- gallery ------------------------------------------------------------ */
.gallery {
  display: grid; gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
}
.gcard { margin: 0; }
.gcard video {
  width: 100%; aspect-ratio: 16 / 9; display: block;
  background: oklch(0.22 0.006 70); border-radius: var(--radius);
}
.gcard figcaption { display: grid; gap: 0.15rem; padding: 0.6rem 0.1rem 0; }
.gcard .gnote { font-size: 0.875rem; color: var(--ink-3); }

/* Video sits on a dark ground, matching the review screens: the app is light because it is used
   in daylight, but footage always gets a dark surround so nothing competes with the picture. */
.stage.placeholder {
  display: grid; place-content: center; text-align: center; gap: 0.3rem;
  aspect-ratio: 16 / 9; background: oklch(0.22 0.006 70); color: oklch(0.99 0.002 70);
  border-radius: var(--radius);
}
.stage.placeholder .hint { color: oklch(0.75 0.006 70); }

.wrap > section { margin: 2.75rem 0; }
.wrap > section > p, .wrap > section > ul, .wrap > section > ol { max-width: var(--measure); }

/* Numbered because the order IS the promise — nothing costs anything until step 3. */
.steps { counter-reset: s; list-style: none; padding: 0; margin: 0; }
.steps li {
  counter-increment: s; position: relative; padding: 0 0 0 2.4rem; margin-bottom: 0.9rem;
}
.steps li::before {
  content: counter(s); position: absolute; left: 0; top: 0.05rem;
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  display: grid; place-content: center;
  background: var(--surface-2); border: 1px solid var(--line-firm);
  font-size: 0.8125rem; color: var(--ink-2);
}

/* Pack tiles are anchors on the landing page and forms on /buy — same look either way. */
a.pack { text-decoration: none; color: inherit; display: block; }
a.pack:hover { border-color: var(--accent); }
a.pack .btn { display: inline-block; margin-top: 0.2rem; }

.close {
  border-top: 1px solid var(--line); padding-top: 2rem;
}

/* --- drag to reorder -----------------------------------------------------
   Layered over the up/down arrows, which stay: they need no JS, work one-handed on a phone and
   are keyboard accessible. This is the faster path for someone at a desk moving a scene four
   places at once. */
/* One width for the grip and for the space the un-draggable rows leave in its place, so the
   thumbnails down the plan stay on a single line however the grip is styled later. */
/* Wide enough for ☰ at its rendered size. Too narrow and the glyph overflows its own column,
   pushing the thumbnails on draggable rows a couple of pixels right of the intro/outro rows —
   which is exactly the misalignment the shared column exists to prevent. */
:root { --grip-w: 1rem; }

.grip, .grip-gap {
  align-self: center; flex: 0 0 var(--grip-w);
  margin-left: -0.25rem; line-height: 1;
}
.grip {
  padding: 0.4rem 0;
  font-size: 1.05rem; line-height: 1;   /* ☰ is one glyph, so no letter-spacing correction */
  color: var(--ink-3); cursor: grab; user-select: none;
}
.grip:active { cursor: grabbing; }
/* The intro and outro are not scenes — they are text over the first and last clips, so they
   have no position to drag to. They still need the grip's column, or their thumbnails sit a
   grip-width left of every scene below them and the plan reads as two misaligned lists. */
.grip-gap { visibility: hidden; }

/* Only show the affordance where dragging actually works. A grip on a touch device that cannot
   start an HTML5 drag is a control that lies; the arrows are the touch path and they are right
   there in the same row. Both go together, so the alignment holds. */
@media (hover: none) {
  .grip, .grip-gap { display: none; }
}

.scene.dragging {
  opacity: 0.5; background: var(--surface-2);
  outline: 2px dashed var(--line-firm); outline-offset: -2px;
}
.scene.saving { opacity: 0.6; pointer-events: none; }

/* Legal prose. The only pages here with real body copy, and they are the ones someone reads
   under pressure — a bullet list of what a credit does should not run edge to edge.

   `:not(.scenes)` and `:not(.sub)`: this was capping ul.scenes at 68ch, so the plan list sat at
   587px inside an 1180px page and every scene card was half width. A reading measure belongs on
   prose, not on a list of controls — the same over-broad-selector mistake as `.top nav`. */
/* One number for "as wide as prose may get". It was 68ch here and 66ch on the legal sections —
   two values for one idea, differing by less than a word. */
:root { --measure: 68ch; }

.wrap > ul:not(.scenes):not(.packs):not(.gallery) {
  padding-left: 1.2rem; margin: 0 0 1rem; max-width: var(--measure);
}
.wrap > ul:not(.scenes) li { margin-bottom: 0.4rem; }
/* `.sub` was excluded here, and it is the longest single paragraph on most screens — 122ch on
   /new and 158ch on the plan board, measured. The rule capped everything except the text that
   most needed capping. `.cta` stays out: it is a centred action line, not a paragraph. */
.wrap > p:not(.cta) { max-width: var(--measure); }

/* Footer. Quiet by design — it is a legal and support obligation, not a navigation surface,
   so it sits below the fold's business and never competes with the page's own actions. */
.foot {
  border-top: 1px solid var(--line); margin-top: 3rem;
  font-size: 0.8125rem; color: var(--ink-2);
}
/* The footer is frame, not content: --frame regardless of which width the page chose, so its
   left edge matches the wordmark on every page rather than following the content in and out. */
.foot .wrap {
  max-width: var(--frame);
  display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; padding: 1.25rem;
}
.foot a { color: inherit; text-decoration: none; }
.foot a:hover { color: var(--accent-in); text-decoration: underline; }
.foot-abn { margin-left: auto; }        /* pushed right on desktop, wraps under on a phone */

/* --- job header ----------------------------------------------------------
   Title left, counters right. Run together in a sentence they read as prose to be parsed;
   stacked and right-aligned they are a status panel to be glanced at, which is what they are —
   the agent checks them repeatedly while a job runs, and always in the same place.

   The number is bold, never coloured: this stylesheet reserves the accent for actions, and a
   counter is not one. The single exception is .warnish on "needs a look", which is state rather
   than emphasis and would lose its force if every number nearby were also tinted. */
.jobhead {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem;
  margin-bottom: 0.35rem;
}
.jobhead h1 { margin-bottom: 0; }
.counters {
  flex: none; text-align: right;
  font-size: 0.875rem; color: var(--ink-2); line-height: 1.45;
}
.counters p { margin: 0; }
.counters b { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
/* A skipped count is context, not progress — it should not read as loudly as the live one. */
.counters .quiet-count, .counters .quiet-count b { color: var(--ink-3); }
.counters .warnish b { color: inherit; }
@media (max-width: 560px) {
  .jobhead { flex-direction: column; gap: 0.4rem; }
  .counters { text-align: left; }
}

h1 { font-size: 1.5rem; font-weight: 500; letter-spacing: -0.015em; margin: 0 0 0.35rem; }
h2 { font-size: 1.125rem; font-weight: 500; margin: 2rem 0 0.75rem; }
.sub { color: var(--ink-2); margin: 0 0 1.75rem; }
/* The measure has to follow the prose, not the position. `.wrap > p` only reaches paragraphs that
   are direct children of the page, and almost none of this product's supporting copy is — it
   lives inside a form, a .note or a <details>. Measured on a 1180px board before this rule: hints
   at 134-138ch, .kv at 175-182ch. `max-width` only ever reduces, so it is inert in the many narrow
   containers a hint also appears in, and it does not apply to the inline `<span class=hint>`. */
.hint { font-size: 0.875rem; color: var(--ink-2); max-width: var(--measure); }
.warnish { color: var(--warn); }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

/* --- controls ------------------------------------------------------------ */

button, .btn {
  font: inherit; font-size: 0.9375rem;
  padding: 0.55rem 1.1rem; border-radius: var(--radius);
  border: 1px solid var(--accent); background: var(--accent); color: var(--on-accent);
  cursor: pointer; text-decoration: none; display: inline-block;
  transition: background 160ms var(--ease);
  min-height: 44px;                    /* field surfaces are used one-handed outdoors */
}
button:hover, .btn:hover { background: var(--accent-in); }
button.quiet, .btn.quiet { background: transparent; color: var(--ink); border-color: var(--line-firm); }
button.quiet:hover, .btn.quiet:hover { background: var(--surface-2); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Matches by exclusion, not by listing types: `input[type=text]` does NOT match `<input name=x>`
   with no type attribute, so every bare input on the upload form was rendering unstyled at the
   browser's default width. It went unnoticed because the two that sit in a grid got stretched by
   the grid — only the address field, which sits alone, showed the real size. */
/* min-width: 0 because `width: 100%` is not the whole story for a form control. Its automatic
   minimum size is its intrinsic one — a bare <input> is 20 characters wide, 275px here — and as a
   grid item that becomes the floor the track has to clear. Needed alongside the minmax(0, 1fr)
   tracks below; neither alone lets a field narrower than 275px exist. */
input:not([type=file]):not([type=checkbox]):not([type=radio]):not([type=submit]), select, textarea {
  font: inherit; width: 100%; min-width: 0; padding: 0.55rem 0.7rem; min-height: 44px;
  border: 1px solid var(--line-firm); border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
}

/* Placeholder text was Chrome's default #757575 — 4.61:1, clearing AA by 0.11 on a number this
   system does not own and no other browser is obliged to match. It is also load-bearing here: the
   plan screen's address block uses placeholders as its field names. --ink-3 is the documented
   quietest-legible tier and the AA floor at 14px, which is exactly the job. */
::placeholder { color: var(--ink-3); opacity: 1; }

/* --- job summary --------------------------------------------------------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; margin: 1.25rem 0 1.75rem; }
.stat { background: var(--surface-2); border-radius: var(--radius); padding: 0.9rem 1rem; }
.stat dt { font-size: 0.8125rem; color: var(--ink-2); margin: 0 0 0.2rem; }
.stat dd { font-size: 1.5rem; font-weight: 500; margin: 0; letter-spacing: -0.02em; }
.stat dd small { font-size: 0.875rem; font-weight: 400; color: var(--ink-3); letter-spacing: 0; }

/* --- scene rows ---------------------------------------------------------- */
/* Rows, not a card grid. Sixteen identical cards is noise; rows stay scannable at that
   count and collapse to mobile without reflowing into something else. */

.scenes { border-top: 1px solid var(--line); margin: 0; padding: 0; list-style: none; }
/* 10px each end so the thumbnail and the row's controls are not flush against the card edge.
   The inset lives on a variable because the hover surface has to reclaim it: shading that stops
   10px short of the card edge reads as a stripe rather than as "this row is live". */
:root { --card-inset: 10px; }
.scene { border-bottom: 1px solid var(--line); padding-left: var(--card-inset); padding-right: var(--card-inset); }

/* Collapsed row is the scannable list; everything scene-specific lives inside it.
   Native <details> rather than JS: keyboard and screen-reader behaviour for free. */
.scene summary {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.8rem 0; cursor: pointer; list-style: none;
}
.scene summary::-webkit-details-marker { display: none; }
.scene summary::after {
  content: "\203A"; color: var(--ink-3); font-size: 1.25rem; line-height: 1;
  transition: transform 160ms var(--ease);
}
.scene details[open] summary::after { transform: rotate(90deg); }
/* Negative margin + matching padding: the summary stretches back out over the card's inset so
   the hover fill runs to both edges, while its contents stay exactly where they were. */
.scene summary {
  margin-left: calc(var(--card-inset) * -1); margin-right: calc(var(--card-inset) * -1);
  padding-left: var(--card-inset); padding-right: var(--card-inset);
}
.scene summary:hover { background: var(--surface-2); }
/* The grid `gap` is the ONLY vertical spacing in here. Three systems were stacking: the gap,
   .scenebar's own margin-bottom, and the browser's default 1em margins on every <p> — so the
   notes sat roughly three times further apart than the controls above them, which is what read
   as uneven. Children contribute no margins of their own. */
/* minmax(0, 1fr), not the implicit `auto` column. An auto track is sized to its widest child's
   min-content, and one of these children is the add-photos row, whose file input has a native
   275px intrinsic width that `min-width: 0` does not lower. The track grew to the input and the
   whole drawer sat 3px past a 320px screen. Same trap as the address grids. */
.scene-detail { padding: 0.25rem 0 1rem; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.55rem; }
.scene-detail > * { margin-top: 0; margin-bottom: 0; }
/* Consecutive hint lines are one block of small print, not separate sections. */
.scene-detail > .hint + .hint { margin-top: -0.3rem; }
.stills { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.stills img { width: 108px; height: 61px; object-fit: cover; border-radius: 4px; border: 1px solid var(--line); }
/* 0.35rem, down from 0.8: the thumbnail is the tallest thing in the row, so trading padding for
   picture keeps the row height where it was while the image gets meaningfully bigger. */
.planrow { display: flex; align-items: center; gap: 0.9rem; padding: 0.35rem 0; }
.scene.off { opacity: 0.5; }
/* No strikethrough on a skipped scene's name. The row is already dimmed, sits under a "Skipped"
   divider and carries an Include button — the rule added nothing but made the name harder to
   read, on the one control the agent needs in order to decide whether to put it back. */

/* Why we skipped it, indented to the row's text column rather than the card edge: it explains
   the scene above it, so it should start where that scene's name starts. Grip + gap + thumb +
   gap, in the same variables the row itself uses, so it cannot drift when those change. */
.card-why {
  margin: 0 0 0.7rem;
  padding-left: calc(var(--grip-w) + 0.9rem + 144px + 2px + 0.9rem);   /* +2px: thumb border */
  font-size: 0.875rem; color: var(--ink-2);
}
@media (max-width: 560px) { .card-why { padding-left: 0; } }
.planrow .actions button { padding: 0.5rem 0.7rem; min-width: 44px; }
button[disabled] { opacity: 0.35; cursor: default; }
/* wrap, because one of these rows holds a file input and a button whose label does not shrink.
   At 390px that is 244px + 115px in a 311px card: the row overflowed its note and took the whole
   plan screen 10px sideways. Wrapping drops the button under the input on a phone and changes
   nothing at any width where both fit. */
.fb { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.fb input { flex: 1; min-width: 0; }
.actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.actions form { margin: 0; }
.kv { font-size: 0.875rem; color: var(--ink-2); margin: 0; max-width: var(--measure); }
.kv b { font-weight: 500; color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .scene summary::after { transition: none; } }
/* 144x81. The row thumbnail is how an agent tells "bedroom 2" from "bedroom 3" without opening
   anything, so it earns the space. The row's vertical padding shrinks as the thumb grows (see
   .planrow) so the row height barely moves — the picture takes the padding, not the page. */
.thumb {
  width: 144px; height: 81px; flex: none; border-radius: 4px;
  background: var(--surface-2) center/cover no-repeat; border: 1px solid var(--line);
}
/* A logo is artwork with its own margins, not a photo: contain it and give it a light ground
   so a dark mark stays legible. Cropping it to fill would be the one thing an agency notices. */
.thumb.logo { background-size: contain; background-color: var(--surface); }
.scene-main { flex: 1; min-width: 0; }
.scene-name { font-size: 0.9375rem; margin: 0; }
.scene-note { font-size: 0.875rem; color: var(--ink-2); margin: 0; }
.scene-note.flag { color: var(--warn); }

/* Name and shot type share the top line, half each. The shot type is what gets scanned down
   the column before starting a render, and under a long room name it wrapped into the subject
   and stopped forming a column at all. Baseline-aligned so the two sizes sit on one line. */
/* Name then shot type, one line, the type sitting immediately after the name rather than in a
   50% column of its own — at 50% the two read as separate fields with a gulf between them, and
   on a wide job screen that gulf was most of the row. The name keeps whatever width it needs and
   the type follows it, ellipsised if the row is tight. */
.scene-head { display: flex; gap: 0.6rem; align-items: baseline; flex-wrap: wrap; }
.scene-head .scene-name { flex: 0 1 auto; min-width: 0; }
/* These open the scene AND focus the control they name, so they have to look like a way in
   rather than a caption. Underline on hover only — a permanently underlined row header would
   read as a link to another page, and it is not one. */
[data-opens] { cursor: pointer; text-underline-offset: 2px; }
.plansum:hover [data-opens]:hover { text-decoration: underline; }
.scene-kind {
  flex: 0 1 auto; min-width: 0; margin: 0;
  font-size: 0.875rem; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A separator, so two greys on one line do not read as one run-on phrase. */
.scene-head .scene-kind::before { content: "· "; color: var(--ink-3); }
@media (max-width: 560px) {
  .scene-head { display: block; }
  .scene-head .scene-kind::before { content: none; }
}

/* Status is never colour alone — every state carries an icon glyph and a word.
   Red/green-only fails ~8% of male users. */
.status { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.875rem; white-space: nowrap; color: var(--ink-2); }
.status::before { font-size: 0.9em; }
.status.done  { color: var(--ok); }
.status.done::before { content: "\2713"; }
.status.flag  { color: var(--warn); }
.status.flag::before { content: "\26A0"; }
.status.busy::before { content: "\25CF"; animation: pulse 1.4s var(--ease) infinite; }
.status.wait { color: var(--ink-3); }
.status.wait::before { content: "\25CB"; }
/* Waiting on the agent. Accent is reserved for actions, and this is the one row that needs one. */
.status.act { color: var(--accent-in); font-weight: 500; }
.status.act::before { content: "\2192"; }
@keyframes pulse { 50% { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { .status.busy::before { animation: none; } }

.bar { height: 3px; border-radius: 2px; background: var(--surface-2); overflow: hidden; margin-top: 0.4rem; }
/* Waived on THIS LINE only, never by rule or by file. A rule-wide or file-wide ignore would also
   swallow the next layout animation someone adds, which is the one thing this exception must not
   buy. Scoped here, a second one anywhere in the stylesheet still fails the pass.

   Do not name the flagged property in prose above a waived line: the detector matches the file by
   regex and does not skip comments, so writing it out raises a second finding on the sentence
   explaining the first. That is how this comment read on its first draft. */
.bar span { display: block; height: 100%; background: var(--accent); transition: width 400ms var(--ease); } /* impeccable-disable-line layout-transition: width IS this meter's value */
/* The meter still moves and the megabytes beside it still count up — only the easing goes. A
   progress bar that stops reporting progress is not an accessibility improvement. */
@media (prefers-reduced-motion: reduce) {
  .bar span { transition: none; }
  .drop { transition: none; }
}

/* --- video surfaces go dark --------------------------------------------- */

.stage {
  background: oklch(0.20 0.006 70); border-radius: 12px; padding: 0.75rem;
  color: oklch(0.95 0.003 70);
}
.stage video { width: 100%; display: block; border-radius: 6px; background: #000; }
.stage figcaption { font-size: 0.8125rem; color: oklch(0.72 0.005 70); padding: 0.6rem 0.25rem 0.15rem; }

/* --- notices ------------------------------------------------------------- */

/* Button first, breakdown beside it: the button is the action and the counts are its caption,
   so they sit at caption size rather than competing with it. */
.startbar { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
.startbar form { margin: 0; }
.billed {
  list-style: none; margin: 0; padding: 0;
  font-size: 0.8125rem; color: var(--ink-2); line-height: 1.4;
}
.billed b { font-weight: 500; color: var(--ink); }

/* The price, on its own line and at heading size. It is the single fact the agent is deciding
   on; the sentences under it are context and read as such. */
.cost {
  font-size: 1.125rem; font-weight: 500; letter-spacing: -0.01em;
  margin: 1.1rem 0 0.3rem;      /* collapses with the .hint below, so it sets that gap */
}

/* The tile's own rhythm. Its <p>s carried the browser's default 1em margins, so the two
   explanatory sentences sat as far apart as the price does from the button — three unrelated
   blocks rather than a price with its explanation under it. The gap between the sentences is
   now smaller than the gap that separates them from the price above. */
.note > .cost + .hint { margin: 0.15rem 0 0.5rem; }
.note > .hint + .hint { margin: 0; }

/* --- account section -----------------------------------------------------
   My Reels, Profile, Agency, Team and Credits are one errand — your stuff, rather than the
   walkthrough in front of you — so they share a sidebar instead of adding five items to a
   masthead that was starting to read as a menu bar.

   The nav sits at the page's left edge, level with the wordmark above it: .wrap already places
   that edge, so the sidebar simply starts the grid there rather than being indented into the
   content column, which would make it look like part of the page instead of the frame. */
.acct { display: grid; grid-template-columns: 11rem 1fr; gap: 2.5rem; align-items: start; }
/* Down one of its own rows (36px item + 0.1rem gap), which drops the first item off the h1's
   line and onto the content below it — the menu reads as a sibling of the page rather than of
   its heading. */
.acct-nav { display: grid; gap: 0.1rem; position: sticky; top: 5.5rem; margin-top: 2.375rem; }
.acct-nav a {
  padding: 0.4rem 0.6rem; border-radius: 0;
  color: var(--ink-2); text-decoration: none; font-size: 0.9375rem;
}
.acct-nav a:hover { background: var(--surface-2); color: var(--ink); }
/* The current page is filled, matching the workflow bar's "you are here" — one visual idea for
   one meaning, rather than a second convention for the same job. */
.acct-nav a[aria-current=page] {
  background: var(--surface-2); color: var(--ink); font-weight: 500;
  box-shadow: inset 2px 0 0 var(--accent);
}
/* The sidebar needs the page's full measure to sit under the wordmark, but the CONTENT beside
   it does not want 924px: three price tiles stretched to fill that are billboards, and a list
   of job rows becomes a thumbnail marooned from its own text. Capped at the reading width the
   rest of the site uses, left-aligned under the heading rather than centred in the gap. */
.acct-body { min-width: 0; max-width: 760px; }
/* ...except a table, which is not prose and does not want a reading measure. The cap exists so
   three price tiles do not become billboards; six columns with an address in them need the
   frame. */
.acct-body.full { max-width: none; }
.acct-body h1 { margin-top: 0; }
/* Who you are on the left, your agency's logo on the right. The text is one block, not a stack
   of body-copy paragraphs — default <p> margins had the lines 16px apart and reading as
   unrelated. The name leads at full ink; the rest is supporting detail, which is what `p + p`
   colours. */
.whoami { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 1rem; align-items: stretch; }
.whoami p { margin: 0; }
.whoami p + p { margin-top: 0.2rem; color: var(--ink-2); font-size: 0.9375rem; }
.whoami .role-badge { vertical-align: 0.05em; margin-left: 0.15rem; }
/* The logo fills the height of the lines beside it — and must not set that height. Left in the
   flow, a tall logo makes the row as tall as ITSELF and the text stretches to match, which grew
   the tile from 104px to 176px for a square logo. Absolutely positioned, it takes no part in
   sizing the row: the column stretches to whatever the text needs and the image fits inside it.
   Contained, never cropped or squashed — same rule as the outro card. A square logo therefore
   fills the height; a wide lockup runs out of width first and sits shorter, which is right. */
.whoami-ag { position: relative; width: 11rem; }
.whoami-logo {
  position: absolute; top: 0; right: 0; height: 100%; width: 100%;
  object-fit: contain; object-position: right center;
}
/* A face, not a mark — so `cover` and a circle, where a logo needs `contain` and its own shape.
   Cropping a portrait to fill the circle is right; cropping a logo would not be. Square, so it
   sizes off the row height and does not stretch to the 11rem column. */
.whoami-avatar {
  position: absolute; top: 0; right: 0; height: 100%; aspect-ratio: 1;
  object-fit: cover; border-radius: 50%; background: var(--surface-2);
}
@media (max-width: 560px) {
  /* Stacked, there is no column of text to match, so the logo goes back into the flow under a
     fixed cap. Absolute positioning here would collapse it to nothing — no row to fill. */
  .whoami { grid-template-columns: 1fr; gap: 0.75rem; }
  .whoami-ag { position: static; width: auto; }
  .whoami-logo { position: static; height: auto; width: auto; max-height: 2.75rem; }
  .whoami-avatar { position: static; height: 3.5rem; width: 3.5rem; }
}
/* The preview above the file input on /account, where there is no row to size against. */
.note > p > .whoami-avatar { position: static; height: 4.5rem; width: 4.5rem; }

@media (max-width: 720px) {
  /* Stacked, and the nav becomes a scrolling row: a 11rem column beside content on a phone
     leaves neither enough room. */
  .acct { grid-template-columns: 1fr; gap: 1.25rem; }
  .acct-nav {
    position: static; grid-auto-flow: column; justify-content: start; gap: 0.25rem;
    overflow-x: auto; scrollbar-width: none; padding-bottom: 0.25rem;
    margin-top: 0;      /* stacked, there is no h1 beside it to step down from */
  }
  .acct-nav::-webkit-scrollbar { display: none; }
  .acct-nav a { white-space: nowrap; }
  .acct-nav a[aria-current=page] { box-shadow: inset 0 -2px 0 var(--accent); }
}

/* --- agency ------------------------------------------------------------- */
/* Suburb, state and postcode on one line, sized to what each holds: a suburb name is long, a
   state is three letters, a postcode is four digits. Equal thirds would give the state five
   times the room it needs and the suburb less than it wants. */
/* minmax(0, 1fr), not 1fr, wherever a track holds a form control. `1fr` is minmax(auto, 1fr), and
   an <input>'s auto minimum is its intrinsic 20-character width — 275px — which `width: 100%`
   does not lower, because a percentage resolves to auto while the track is being sized. The track
   would grow to the input rather than the input shrinking to the track. Defensive rather than a
   fix for something seen: the 320px overflow measured here came from the file input, which is a
   worse case again (see input[type=file]). These tracks were one longer label away from it. */
.addr-line { display: grid; grid-template-columns: minmax(0, 1fr) 7rem 7rem; gap: 0.6rem; }
.addr-line p { margin: 0; }
@media (max-width: 560px) { .addr-line { grid-template-columns: minmax(0, 1fr); } }
/* Street address and the agency's listing reference. Not equal halves: a street address runs to
   "14a Hillcrest Avenue" and a reference is rarely more than a handful of characters. */
.prop-line { display: grid; grid-template-columns: 12rem; gap: 0.6rem; }
@media (max-width: 560px) { .prop-line { grid-template-columns: minmax(0, 1fr); } }
/* A checkbox in a two-up beside a labelled input: the input has a label line above it and is
   44px tall, so left alone the checkbox floats at the top of its cell and reads as belonging to
   nothing. Bottom-aligned and matched to the input's height, it sits level with the field it
   qualifies. The hint goes underneath rather than after the word, where it would push the box
   off the grid line on a narrow screen. */
.checkline {
  align-self: end; display: flex; align-items: center; min-height: 44px; margin: 0;
}
.checkline label { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.checkline .hint { margin: 0; }
/* 1.1rem, not the 13px default: beside a 44px field the browser's box reads as an afterthought,
   and this one decides who can spend the agency's money. */
.checkline input {
  width: 1.1rem; height: 1.1rem; min-height: 0; margin: 0; flex: none;
  accent-color: var(--accent);
}
.agency-logo {
  max-width: 200px; max-height: 80px; object-fit: contain;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 0.5rem;
}
/* Only admins are badged. An unmarked row is an agent, which is the common case, and badging
   every row twice over would make the list harder to scan for the thing you are looking for —
   who can spend money. Aligned with the buttons because it is the same kind of fact: a property
   of the account, not a way to contact them. */
.role-badge {
  align-self: center; font-size: 0.75rem; font-weight: 500;
  color: var(--accent-in); border: 1px solid var(--accent); border-radius: 999px;
  padding: 0.1rem 0.5rem; white-space: nowrap;
}
/* Fixed slots, not a flex run: the list is a column of near-identical rows, and with flex the
   Edit button sat at a different x on every row depending on whether there was a badge and how
   wide it was. Three columns means one vertical line for the pill and one for each button, on
   every row, whether or not that row fills them. */
/* The pill sits on the centre of the ROW, between equal tracks — the same trick as the masthead.
   minmax(0, 1fr) and not plain 1fr: a bare 1fr floors at its content, so one long name would
   push the pill off centre and every row would disagree about where the middle is. With the
   floor at 0 the name's own column shrinks and the text wraps inside it.
   A track, rather than absolute positioning: absolute would centre it just as well, and then a
   long name would run underneath it. Nothing can overlap a column. */
.planrow.team-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: 0.6rem;
}
/* Named here too. An agent's row has no badge, so auto-placement put its buttons in the middle
   column and they landed 300px left of the admin row's. */
.team-row .scene-main   { grid-column: 1; }
.team-row .role-badge   { grid-column: 2; }
.team-row .team-actions { grid-column: 3; justify-self: end; }
.team-actions {
  display: grid; grid-template-columns: var(--rowbtn) var(--rowbtn);
  align-items: center; justify-items: center; gap: 0.5rem;
}
.team-actions form { display: contents; }   /* so the button, not the form, is the grid item */
/* Named columns, not auto-placement. A row with no Remove would otherwise slide Edit into the
   empty slot — the ragged edge the grid is here to fix. Same lesson as the masthead: an absent
   element does not hold its place. */
.team-actions a.rowbtn      { grid-column: 1; }
.team-actions button.rowbtn { grid-column: 2; }
@media (max-width: 560px) {
  /* A phone has no room for a centre track — the name would be squeezed into a word per line to
     hold a column of whitespace. The pill goes above the controls on the right instead. */
  .planrow.team-row { grid-template-columns: minmax(0, 1fr) auto; }
  .team-row .scene-main   { grid-row: 1 / span 2; }
  .team-row .role-badge   { grid-column: 2; grid-row: 1; justify-self: end; }
  .team-row .team-actions { grid-column: 2; grid-row: 2; }
}
/* Square, and sized by the widest thing in one: "Edit" at 0.8125rem needs ~26px, so 2.4rem
   leaves a margin either side without the box growing into a standing button. */
:root { --rowbtn: 2.4rem; }
.rowbtn {
  width: var(--rowbtn); height: var(--rowbtn); min-height: 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.8125rem;
}
/* `.planrow .actions button` sets a 44px min-width and its own padding at 0-2-1, which beat the
   rule above and left the cross 44px wide while Edit — an <a>, so untouched — sat at 38. Only
   the <button> was wrong, which is exactly the kind of difference that reads as a rendering
   glitch rather than a CSS one. */
.planrow .team-actions .rowbtn { min-width: var(--rowbtn); padding: 0; }
/* Red on the destructive one. --bad is a red at the same lightness as the other tokens, so it
   reads as this palette rather than a browser default; the border stays quiet until hover so a
   row of people is not a row of alarms. */
.rowbtn.danger { color: var(--bad); font-size: 0.9rem; }
.rowbtn.danger:hover { background: var(--bad-bg); border-color: var(--bad); }
/* The row you are editing, so the form below is obviously about this person and not the last
   one you clicked. */
.scene.editing { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
/* Marks your own row in the team list — you are the one person there who cannot be identified
   by name alone, because you already know who you are. */
.you {
  font-size: 0.75rem; font-weight: 500; color: var(--ink-3);
  border: 1px solid var(--line-firm); border-radius: 999px; padding: 0.05rem 0.4rem;
  margin-left: 0.3rem;
}

/* Two short promises about the money. A list, because they are two separate commitments and a
   sentence joining them reads as one hedge. Sized like .hint — this is supporting copy. */
.terms { margin: 0.6rem 0; padding-left: 1.1rem; color: var(--ink-2); font-size: 0.875rem; }
.terms li { margin: 0.15rem 0; }
/* Revenue-by-month chart. A dozen rects drawn server-side — no charting library, so no script
   tag and nothing to keep up to date. The viewBox scales it to whatever width the note gives it.
   Named `chart-bar` because `.bar` above is the scene-progress meter: an SVG <rect> inheriting
   its `height: 3px` renders as nothing at all. */
/* Sized by HEIGHT, not width. `width: 100%` scales a 270-unit viewBox by 3.3x on a desktop
   note, which blows the 11px labels up to 36px — the whole chart rendered as a billboard.
   Fixing the height pins the scale near 1:1 and lets the width follow the number of months. */
/* No `max-width: 100%`. It looks like a responsive safety net and is the opposite: it outranks the
   height on a narrow window and shrinks the whole drawing, and every label in an SVG is sized in
   viewBox units, so they shrink with it. The width may overflow; the scale may not. */
.chart { height: 170px; width: auto; overflow: visible; display: block; }
.chart-scroll { overflow-x: auto; }
.chart-bar { fill: var(--accent); }
/* These two sizes are viewBox USER UNITS, not pixels, and the rule above is what makes that safe:
   the scale is pinned, so they land above the readable floor at every viewport. Waived per line,
   never per value — a stray small size in ordinary CSS elsewhere must still fail the pass.
   (Do not write the numbers in this comment: the detector matches the file by regex and does not
   skip comments, so naming them here would re-raise exactly what these lines waive.) */
.chart-x, .chart-n { text-anchor: middle; fill: var(--ink-2); font-size: 11px; } /* impeccable-disable-line design-system-font-size: viewBox user unit, see The Fixed Scale Rule */
.chart-n { fill: var(--ink-3); font-size: 10px; } /* impeccable-disable-line design-system-font-size: viewBox user unit, see The Fixed Scale Rule */
.money { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The authenticator setup key. Wide letter-spacing and a monospace face because this gets read
   off the screen and typed into a phone: 0/O and 1/l are the whole problem. `user-select: all`
   so one click takes the lot — a half-copied secret enrols an app that then never matches. */
.secret {
  display: inline-block; user-select: all; font-size: 1rem; letter-spacing: 0.08em;
  padding: 0.45rem 0.6rem; background: var(--surface-2); border-radius: var(--radius);
  word-break: break-all;
}
/* QR stays black on white in both themes — it is a scanning target, not decoration.
   The padding belongs to the WRAPPER, never to the <svg>. With `box-sizing: border-box` above,
   padding on the svg is subtracted from its width, and a symbol with no viewBox then clips
   rather than scales: the quiet zone and an edge of a finder pattern silently disappear and the
   code stops decoding while still looking like a QR. The viewBox in totp.qr_svg is the real
   guard; keeping padding off the element means the two cannot conspire again.
   `crispEdges` stops the browser antialiasing module boundaries into grey mush when the CSS
   size is not an exact multiple of the module count. */
.qr { margin: 0 0 1rem; text-align: center; }
.qr-frame { display: inline-block; background: #fff; padding: 0.5rem; border-radius: 4px; }
.qr svg { display: block; width: 280px; height: auto; max-width: 100%; }
.qr figcaption { margin-top: 0.4rem; }
.note { border: 1px solid var(--line); background: var(--surface); border-radius: var(--radius); padding: var(--pad); }
.note.warn { border-color: color-mix(in oklch, var(--warn) 35%, var(--line)); }
.disclosure { font-size: 0.875rem; color: var(--ink-2); }

@media (max-width: 560px) {
  .wrap { padding: 1rem 1rem 3rem; }
  .thumb { width: 104px; height: 59px; }  /* proportional to the desktop bump, still 16:9 */
  .scene { gap: 0.7rem; }
}
/* Below 480 the row stops being a row. Thumbnail on the left spanning both lines, the name and
   its shot type on the first, the controls on the second.

   Squeezing the four parts onto one line instead was tried and measured to zero overflow, and it
   was still wrong: it left "Front of house" breaking after "Front", the three buttons stacked into
   a tower, and a row taller than this one. Passing the measurement is not the same as fixing the
   screen — the number said 0 and the result was unusable, which is only visible by looking.

   Stacking also gives the picture its size back. It was going to be cut to 88px to win 16px on one
   line; here it keeps the 104px the 560 rule sets, which is the whole argument DESIGN.md makes for
   it — big enough to tell "bedroom 2" from "bedroom 3" without opening anything.

   The grip goes at this width whatever the pointer is. A touch device already loses it to
   `@media (hover: none)`; a desktop window this narrow keeps it and was the last thing still
   overflowing. Dragging a row around 250px is not a workflow anyone has, and the up and down
   arrows are right there — the same argument the hover rule makes, with width as the reason
   instead of input. */
@media (max-width: 480px) {
  .planrow {
    display: grid; align-items: start;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "thumb main" "thumb actions";
    column-gap: 0.6rem; row-gap: 0.4rem;
  }
  .planrow .thumb { grid-area: thumb; align-self: center; }
  .planrow .scene-main { grid-area: main; }
  /* nowrap now that they have a line of their own: three 44px targets fit 240px, and wrapping
     them here would rebuild the tower this layout exists to avoid. */
  .planrow .actions { grid-area: actions; flex-wrap: nowrap; }
  .grip, .grip-gap { display: none; }
}

.divider {
  font-size: 0.8125rem; color: var(--ink-3); padding: 1rem 0 0.4rem;
  border-bottom: 1px solid var(--line);
}

/* --- forms --------------------------------------------------------------- */

.stack { display: grid; gap: 1rem; }
.stack .note { display: grid; gap: 0.6rem; }
.lbl { font-weight: 500; font-size: 0.9375rem; margin: 0; }
.lbl .hint { font-weight: 400; }
.two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.6rem; }
.stack p { margin: 0; }
/* max-width and min-width, because a file input is the one control here with a native intrinsic
   width it will not give up: "Choose File / No file chosen" measures 275px, and at 320px that is
   wider than the card it sits in. It grew its own .note to 309px inside a 273px form and took the
   document 5px sideways — the only thing on /new that overflowed a phone. */
input[type=file] { font: inherit; font-size: 0.9375rem; max-width: 100%; min-width: 0; }
code { background: var(--surface-2); padding: 0.05em 0.35em; border-radius: 4px; font-size: 0.9em; }
button.small { min-height: 0; padding: 0.3rem 0.7rem; font-size: 0.8125rem; }
.topline nav form { margin: 0; }
@media (max-width: 480px) { .two { grid-template-columns: 1fr; } }

/* --- pricing ------------------------------------------------------------- */

.packs { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; }
.pack {
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  padding: 1.1rem; display: grid; gap: 0.4rem; align-content: start; margin: 0;
}
.pack.best { border-color: var(--accent); }
.pack .tag {
  font-size: 0.75rem; color: var(--accent-in); font-weight: 500;
  text-transform: none; margin: 0;
}
.pack .price { font-size: 1.75rem; font-weight: 500; letter-spacing: -0.02em; margin: 0; }
.pack .hint { margin: 0 0 0.4rem; }
.err { color: var(--warn); margin: 0; }
.ok { color: var(--ok); margin: 0; }

/* Credit statement. Narrow on purpose — three columns, and the amount is the one the eye
   goes to, so it is the only one that is right-aligned and tabular. */
.ledger { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.ledger th { text-align: left; font-weight: 500; color: var(--ink-2); font-size: 0.8125rem; }
.ledger th:last-child, .ledger td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
/* Six columns need air between them. The outer edges stay flush, so the table lines up with the
   note's own padding instead of sitting indented inside its box. */
.ledger th, .ledger td { padding: 0.45rem 0.75rem 0.45rem 0; border-bottom: 1px solid var(--line); }
.ledger th:last-child, .ledger td:last-child { padding-right: 0; }
.ledger tr:last-child td { border-bottom: 0; }
.ledger td:first-child { color: var(--ink-2); white-space: nowrap; }
/* A job id is a reference, not prose: it must not wrap mid-token, and it earns less weight than
   the address beside it, which is what the agent actually recognises. */
.ledger .jobid { white-space: nowrap; font-size: 0.8125rem; }
.ledger th { white-space: nowrap; }     /* headings are single words; never break one */
/* Right-aligned and tabular, by class rather than by position: a count is read by comparing it
   down the column, and four tables share `.ledger` with the number in a different place in each.
   `td:last-child` already does this for whatever ends the row; this is for the ones that do not. */
.ledger .num { text-align: right; font-variant-numeric: tabular-nums; }

/* --- column widths, per table -------------------------------------------------------------
   These were written about the statement's columns and applied to every `.ledger`, which held
   only while it was the sole six-column table. It is not any more: the admin sales table also
   has six, and its fifth is Credits, not Address. */

/* The address is the only column whose length varies and the one an agent actually reads, so it
   takes the slack. Without this the browser hands the extra width to whichever column happens to
   hold the longest word — which was User. */
.ledger.statement th:nth-child(5), .ledger.statement td:nth-child(5) { width: 100%; }
/* Date and User take exactly what they need. Giving all the slack to the address squeezed User
   to 71px and broke "Dean Sheppard" across two lines — a name is a unit. */
.ledger.statement td:first-child, .ledger.statement td:nth-child(3) { white-space: nowrap; }
/* Type carries a short phrase, so it wraps rather than forcing the table wide — but it needs a
   floor. `width:100%` on the address leaves every other column at its longest word, and on rows
   with no address to compete that crushed Type to 40px: "test / top- / up", three lines. */
.ledger.statement td:nth-child(2) { min-width: 10rem; }

/* Sales: the slack goes to Item, the last column before the figures. Date, Agency and the buyer
   then stay packed to the left where they are read together, and the empty space falls in front
   of the right-aligned numbers where a gap is expected.

   It was on Agency first, and that is wrong in a way worth naming: a wide cell does not centre
   its text, it strands it. The agency name stayed hard left in a ~380px cell and every column
   after it was pushed to the right edge, so each row read as two clumps with a canyon between
   them. The cells were left-aligned the whole time; the columns were not where the eye needed
   them. Slack belongs beside the numbers, never between two things being compared. */
.ledger.sales th:nth-child(4), .ledger.sales td:nth-child(4) { width: 100%; }
/* A name and a date are units — neither may break across two lines. Item is the one left free to
   wrap, because it holds the slack and is the only cell with room to give. */
.ledger.sales td:first-child, .ledger.sales td:nth-child(2), .ledger.sales td:nth-child(3) {
  white-space: nowrap;
}
/* Six columns will not fit a phone. Scroll the table inside its own box rather than letting it
   push the page sideways. */
.note:has(.ledger) { overflow-x: auto; }
.ledger .plus { color: var(--ok); }
.ledger .minus { color: var(--ink-2); }
/* The filter sits above the table: just the control, whose first option says what it does. */
.filter { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.85rem; flex-wrap: wrap; }
.filter select { min-height: 0; padding: 0.3rem 0.5rem; font-size: 0.875rem; width: auto; }
.filter button { min-height: 0; padding: 0.3rem 0.7rem; font-size: 0.8125rem; }

a.planrow { text-decoration: none; color: inherit; }
a.planrow:hover { background: var(--surface-2); }

a { color: var(--accent-in); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent); }
.topline nav a, a.btn, a.planrow { text-decoration: none; }
.topline nav a:hover { color: var(--accent-in); }

/* Five across, computed rather than a fixed width: a grid of equal columns fills the row
   whatever the container is, so the photo strip and the scene card it sits in share one right
   edge instead of the strip stopping short wherever a fixed tile width happened to run out.

   Five, not seven: this is the picker where the agent decides WHICH photo a shot is built from
   and, for a pair, which end it starts at — at 155px two angles of the same room are hard to
   tell apart. 220px (+42%) makes that judgement possible; a 15-photo scene simply takes three
   rows, which costs nothing but scroll.

   Below the breakpoint it falls back to as-many-as-fit, because five on a phone is a contact
   sheet nobody can tap. */
.pics { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.6rem; }
.pic { display: grid; gap: 0.3rem; justify-items: stretch; min-width: 0; }
/* aspect-ratio, not a fixed height: the tile width is now fluid, and a fixed height would crop
   harder as the column narrows. 16:9 matches the output the agent is choosing frames for. */
.pic img {
  width: 100%; aspect-ratio: 16 / 9; height: auto; object-fit: cover;
  border-radius: 4px; border: 1px solid var(--line);
}
@media (max-width: 900px) {
  .pics { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
}
/* --- the uploader's drop zone ------------------------------------------------------------
   A <label> wrapping its own file input, so the whole surface opens the picker with no script.
   Before this, the target was the leftmost ~90px of a 23px-tall control. --line-firm rather than
   --line because this IS a control and owes 3:1, dashed because a drop target that looks like a
   filled card reads as something already holding content. */
.drop {
  display: grid; justify-items: center; align-content: center; gap: 0.25rem;
  min-height: 120px; padding: 1.25rem 1rem; text-align: center; cursor: pointer;
  border: 1px dashed var(--line-firm); border-radius: var(--radius);
  background: var(--surface);
  transition: background 160ms var(--ease), border-color 160ms var(--ease);
}
.drop:hover, .drop:focus-within { border-color: var(--accent); background: var(--surface-2); }
/* Drag-over is the accent, the one place this screen uses it — the page is asking for exactly one
   thing and this is the moment it is about to get it. */
.drop.over {
  border-color: var(--accent); border-style: solid;
  background: color-mix(in oklch, var(--accent) 7%, var(--surface));
}
.drop-lead { font-size: 1.125rem; font-weight: 500; }
.drop-sub { font-size: 0.875rem; color: var(--ink-2); }
/* The control shows itself by DEFAULT and is hidden only once the script that replaces it has
   actually run — `.js` is added by UPLOAD_JS, not printed by the server. The other way round, a
   script that throws on an old browser leaves an agent looking at a drop zone with no visible
   control and no "3 files selected" to tell them anything happened.
   Visually hidden, never `display:none`: it must stay keyboard-reachable and in the
   accessibility tree, because the label around it is what people see. */
.drop.js input[type=file] {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.drop:not(.js) input[type=file] { margin-top: 0.5rem; max-width: 100%; }

.picked { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 0.6rem; }
.pick { position: relative; margin: 0; display: grid; gap: 0.25rem; min-width: 0; }
.pick img, .pick-none {
  width: 100%; aspect-ratio: 16 / 9; height: auto; object-fit: cover;
  border-radius: 4px; border: 1px solid var(--line);
}
/* A photo the browser cannot decode is still a photo the SERVER can read — an iPhone HEIC in
   Chrome. It gets a tile with no picture rather than a broken image, because refusing to show it
   would suggest it is not going to be uploaded. */
.pick-none { display: grid; place-content: center; background: var(--surface-2); color: var(--ink-3); }
.pick figcaption {
  font-size: 0.75rem; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 28px, not 44: it sits ON a 112px tile and a 44px square would cover a quarter of the picture
   the agent is using to decide. Spaced from its neighbours by the grid gap, and every tile also
   has the strip's own remove reachable by keyboard. */
.pick-x {
  position: absolute; top: 0.25rem; right: 0.25rem;
  min-height: 0; width: 28px; height: 28px; padding: 0; line-height: 1;
  display: grid; place-content: center; font-size: 1rem;
  border-radius: 999px; border: 1px solid var(--line);
  background: color-mix(in oklch, var(--surface) 85%, transparent);
  backdrop-filter: blur(4px); color: var(--ink); cursor: pointer;
}
.pick-x:hover { background: var(--bad-bg); border-color: var(--bad); color: var(--bad); }
.warn-text { color: var(--warn); }

.pic.out img { opacity: 0.35; filter: grayscale(1); }
.pic form { margin: 0; }
.pic button { width: 100%; }
/* Vertical padding stays 0 — the .planrow inside supplies it — but the horizontal inset must
   survive, or the hover fill loses the card edges it was just given. */
.plansum { padding-top: 0 !important; padding-bottom: 0 !important; }
.plansum .planrow { flex: 1; }

/* Editable-in-place room name: the field is the heading, not a form below it. */
/* Name and move control on one row, on the SAME five-column grid as the photo strip below: the
   name spans two tile columns, the move control the third. Aligning to the grid rather than to
   chosen widths is what makes the card read as one object, and it stays true if the tile count
   ever changes. */
.scenebar {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.6rem;
  align-items: center;    /* no margin: .scene-detail's gap is the only vertical rhythm here */
}
.scenebar .namebar { grid-column: span 2; }
/* The dropdown needs two columns; the stated-fact box needs three, because it carries a full
   sentence plus its reason and at two columns the mirror one wrapped to a second line. The name
   field keeps its two either way, so the row still lines up with the photo grid below. */
.scenebar .moveform { grid-column: span 2; margin: 0; }
.scenebar .movefixed { grid-column: span 3; margin: 0; }
.scenebar .moveform select { width: 100%; }

/* Where a scene has no move to choose — a pair's path comes from its two photos, a mirrored
   room is pinned to the safest move — the same slot states the fact instead. Styled as a
   disabled field on purpose: it occupies the control's position, so it should look like a
   control that cannot be changed rather than like a stray sentence. */
/* Block, not grid: the reason is a continuation of the sentence before it, so it flows on the
   same line when there is room and wraps naturally when there is not — as a grid row it always
   broke, whatever the width. */
.movefixed {
  padding: 0.5rem 0.7rem; min-height: 44px;
  display: flex; align-items: center; flex-wrap: wrap; gap: 0 0.35rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2); color: var(--ink-2); font-size: 0.875rem;
}
.movefixed span { font-size: 0.8125rem; color: var(--ink-3); }
/* Mid-swap: the slot is asking for something rather than stating a settled fact. */
.movefixed.warn {
  border-color: color-mix(in oklch, var(--warn) 45%, var(--line));
  background: color-mix(in oklch, var(--warn) 7%, var(--surface));
  color: var(--ink);
}

@media (max-width: 900px) {
  .scenebar { grid-template-columns: 1fr; }
  .scenebar .namebar, .scenebar .moveform, .scenebar .movefixed { grid-column: auto; }
}

.namebar { display: flex; align-items: center; gap: 0.4rem; }
.namefield {
  font: inherit; font-size: 1rem; font-weight: 500; flex: 1; min-height: 40px;
  border: 1px solid transparent; border-radius: var(--radius);
  background: transparent; color: var(--ink); padding: 0.3rem 0.5rem;
}
.namefield:hover { border-color: var(--line); }
.namefield:focus { border-color: var(--line-firm); background: var(--surface); }
button.icon {
  min-height: 0; padding: 0.35rem 0.6rem; line-height: 1;
  background: transparent; color: var(--ink-2); border-color: var(--line-firm);
}
button.icon:hover { background: var(--surface-2); color: var(--ink); }

.arrows { display: flex; gap: 0.3rem; }
.arrows form { flex: 1; margin: 0; }
.arrows button { width: 100%; padding: 0.3rem 0; }

/* Intro and outro: pinned, skippable, never movable — they are overlays, not scenes. */
/* Centres the placeholder glyph only. Deliberately NOT the `background` shorthand: this rule
   outranks .thumb, and the shorthand resets background-size to auto, which silently un-covered
   the card thumbnails the moment they started carrying a photo or a logo. */
.scene.card .thumb {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-3); font-size: 1.1rem;
}
.scene.card .scene-name { font-weight: 500; }
