/* ── Design tokens ───────────────────────────────────────────────────────── */
:root {
  color-scheme: dark;
  --scroll-track: #101216;
  --scroll-thumb: #505660;
  scrollbar-color: var(--scroll-thumb) var(--scroll-track);
  --bg:           #0a0b0d;
  --surface:      #101216;
  --surface-2:    #15181d;
  --surface-3:    #1b1f26;
  --line:         rgba(255, 255, 255, .07);
  --line-strong:  rgba(255, 255, 255, .13);

  --text:         #e7e9ec;
  --text-dim:     #9aa1ab;
  /* Round 26e, defect #6: #656c77 on the row/card background (rgb(16,18,22))
     measured 3.54:1 at 10px, below AA's 4.5:1 -- 261 instances (every
     trash-row meta line x249, every project card's proj-when x12).
     Raised to 5.90:1 (rgb(138,145,156)); still visibly the dimmest tier,
     well under --text-dim's 7.20:1 and --text's 15.41:1, so the hierarchy
     (text > text-dim > text-faint) holds -- it just no longer fails AA. */
  --text-faint:   #8a919c;

  --accent:       #f5c518;
  --accent-dim:   #b08f0f;
  --accent-glow:  rgba(245, 197, 24, .16);
  /* Round 6: success reads as restrained silver, not green -- the palette is
     black + silver/white + the ONE gold; red stays for destructive/error only. */
  --ok:           #c9d1dc;
  --warn:         #f0883e;
  --err:          #f2555a;
  --live:         #5b9dff;

  --r-sm: 6px;
  --r:    9px;
  --r-lg: 14px;

  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px;
  --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;

  --shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -12px rgba(0,0,0,.8);
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Menlo", monospace;

  --topbar-h: 52px;
}

* { box-sizing: border-box; }

/* A class rule carrying `display` outranks the UA's [hidden] rule, which
   silently un-hides anything toggled via the hidden attribute. Restore it. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 12px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -.012em; }
h1 { font-size: 17px; }
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-dim); }
h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-dim); }
p  { margin: 0; }

.muted { color: var(--text-dim); font-size: 12px; }

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: var(--scroll-track); }
::-webkit-scrollbar-corner { background: var(--scroll-track); }
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: #737b87; }

/* Round 11, defect #6: no ::selection rule anywhere meant the browser's own
   UA blue showed up on text selection -- the same "only non-gold saturated
   colour anywhere in this app's chrome" complaint D6 already made about the
   search-cancel-button, just for a different native default. */
::selection { background: rgba(245,197,24,.35); color: var(--text); }

/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 30px; padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  background: var(--surface-3);
  color: var(--text);
  font: inherit; font-size: 12px; font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background .13s ease, border-color .13s ease, opacity .13s ease, transform .06s ease;
  text-decoration: none;
}
.btn:hover:not(:disabled) { background: #232830; border-color: rgba(255,255,255,.2); }
.btn:active:not(:disabled) { transform: translateY(.5px); }
.btn:disabled { opacity: .38; cursor: not-allowed; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The primary CTA is the brand gold (same metal as the QB mark), not white. */
.btn-primary {
  background: linear-gradient(180deg, #fae584 0%, #f5c518 55%, #c69a0c 100%);
  color: #1a1400; border-color: var(--accent); font-weight: 600;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.35);
}
.btn-primary:hover:not(:disabled) {
  background: linear-gradient(180deg, #fceca3 0%, #ffd42e 55%, #d4a70f 100%);
  border-color: #ffd42e;
}

/* Metal is reserved for a few key moments (export, the playhead cap, the
   trim handles, the brand mark) -- everywhere else stays flat so those few
   places still read as special. Same vertical gradient + hairline top-sheen
   as the QB brand mark in the header. */
.btn-accent {
  background: linear-gradient(180deg, #fae584 0%, #f5c518 55%, #c69a0c 100%);
  color: #1a1400; border-color: var(--accent); font-weight: 600;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.35);
}
.btn-accent:hover:not(:disabled) {
  background: linear-gradient(180deg, #fceca3 0%, #ffd42e 55%, #d4a70f 100%);
  border-color: #ffd42e;
}

.btn-secondary { background: var(--surface-2); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-dim); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }

.btn-sm { height: 25px; padding: 0 8px; font-size: 12px; }
.btn-icon { width: 30px; padding: 0; }
.btn-block { width: 100%; }

/* ── Top bar ─────────────────────────────────────────────────────────────── */
.topbar {
  height: var(--topbar-h);
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 0 var(--sp-4);
  border-bottom: 1px solid var(--line);
  background: rgba(10,11,13,.85);
  backdrop-filter: blur(12px);
  position: relative; z-index: 30;
}

.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; letter-spacing: -.02em; font-size: 14px; }
.brand-mark {
  width: 26px; height: 26px; border-radius: 8px;
  box-shadow: 0 0 18px var(--accent-glow);
}
.brand-dim { color: var(--text-faint); font-weight: 500; }

/* ── Breadcrumb ───────────────────────────────────────────────────────────
   Replaces the old project <select>. It answers one question -- where am I --
   and offers one move: back to all of them. */
.crumb { display: flex; align-items: center; gap: var(--sp-2); flex: 1; min-width: 0; }
.crumb-home {
  height: 28px; padding: 0 8px;
  background: transparent; border: 1px solid transparent; border-radius: var(--r-sm);
  color: var(--text-dim); font: inherit; font-size: 12px; font-weight: 500;
  cursor: pointer; white-space: nowrap;
}
.crumb-home:hover { color: var(--text); background: var(--surface-2); border-color: var(--line); }
.crumb-home:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.crumb-sep { color: var(--text-faint); font-size: 12px; }
.crumb-current {
  font-size: 12px; font-weight: 600; color: var(--text);
  max-width: 420px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── Library ──────────────────────────────────────────────────────────────
   The home screen: one card per project, click to open its clips. */
.layout[hidden], .library[hidden], .library-grid[hidden], .library-empty[hidden] {
  display: none !important;
}
.library {
  height: calc(100vh - var(--topbar-h));
  overflow-y: auto;
  padding: var(--sp-6) var(--sp-6) 64px;
  display: flex; flex-direction: column;
  /* A quiet spotlight from the header down, so the floor of the screen reads
     as the same room as the cards instead of a flat void once the shelf runs
     out. Same radial-gradient language as the find panel on the Clips page. */
  background: radial-gradient(120% 46% at 50% 0%, rgba(245,197,24,.05), transparent 62%);
}
.library-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); margin: 0 auto var(--sp-5); max-width: 1440px; width: 100%;
}
/* Stays on the house 10/12/14/17 scale (design_rules_premium_ui rule 4) --
   hierarchy here comes from full-white weight against the dimmer body text
   around it, not a bigger size. */
.library-title { font-size: 17px; font-weight: 600; letter-spacing: -.02em; margin: 0; color: var(--text); }
.library-actions { display: flex; align-items: center; gap: var(--sp-2); }

/* Round 26b. Track count was stuck at 4 at every width (1920 down to 760):
   `.library` is a flex column, and `.library-grid` is one of its flex items.
   A flex item's default min-width is `auto`, which is its content's minimum
   size -- here, the sum of every grid track's minmax(260px,...) floor. Below
   ~1088px of available width that floor exceeds the flex container, so the
   grid item just grows past its parent instead of dropping a column, and the
   overflow is invisible because .library scrolls vertically only; the extra
   width sat there as a silent horizontal pan. min-width: 0 lets the flex item
   actually shrink to the container, so auto-fill can drop tracks for real. */
.library-grid {
  display: grid; gap: var(--sp-4); min-width: 0;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  max-width: 1440px; width: 100%; margin: 0 auto;
}
.library-head, .library-empty { min-width: 0; }

/* ── Round 27: the create surface ─────────────────────────────────────────
   The home screen's own visual anchor -- a generous drop zone first, the
   shelf of projects second. It shares the grid's own max-width so the two
   never drift out of alignment as the viewport grows, and it deliberately
   stays quiet (silver/chrome, dashed rather than solid) rather than
   competing with the header's one gold "Import video" button -- gold here
   is reserved for the single transient moment a file is actually about to
   land on it (.is-drag-active below), never a resting state. */
/* ONE bar, one row, 64px tall. The predecessor was a 236px-tall letterbox
   beside a 320px card: 354,708px of container around 6,360px of ink. Height
   is the whole defect -- a drop target does not need to be big to be a drop
   target (the document-wide handler accepts a file dropped ANYWHERE on the
   page; this bar is the affordance that says so, not the hit area). */
.create-bar {
  display: flex; align-items: center; gap: var(--sp-4);
  max-width: 1440px; width: 100%; margin: 0 auto var(--sp-5); min-width: 0;
  /* flex: none matters. .library is a flex COLUMN, so a bare `height` is only
     a flex-basis: once the shelf overflows, the bar shrank to 46px at 1366 and
     stayed 64px at 1920 -- the same control, two heights, purely as a function
     of how many projects were below it. Measured before this line. */
  flex: none; height: 64px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 0 14px;
}
.create-drop {
  flex: 1 1 auto; min-width: 0; height: 44px;
  display: flex; flex-direction: row; align-items: center; justify-content: flex-start;
  gap: 10px; padding: 0 12px;
  background: transparent;
  border: 1.5px dashed var(--line-strong); border-radius: var(--r-sm);
  color: var(--text-dim); font: inherit; cursor: pointer; text-align: left;
  transition: border-color .15s ease, background .15s ease;
}
.create-drop-icon { flex: none; color: var(--text-faint); transition: color .15s ease; }
.create-drop-title { font-size: 14px; font-weight: 600; color: var(--text); letter-spacing: -.01em; }
.create-drop-sub { font-size: 12px; color: var(--text-faint); }
.create-drop:hover, .create-drop:focus-visible {
  border-color: rgba(245, 197, 24, .45);
  background: var(--surface-2);
}
.create-drop:hover .create-drop-icon, .create-drop:focus-visible .create-drop-icon { color: var(--accent); }
.create-drop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* No "OR" rule and no second bordered card: the divider and the panel were
   two more objects earning nothing. The field sits in the same bar, and the
   unlabelled chain glyph that used to head that panel is gone -- the
   placeholder already says what the field takes. */
.create-bar-link {
  flex: 0 0 auto; min-width: 0;
  display: flex; flex-direction: row; align-items: center; gap: var(--sp-2);
}
#createSurfaceUrl {
  width: 260px; max-width: 40vw; background: var(--bg); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  padding: 9px 11px; font: 400 12px/1.3 var(--font);
}
#createSurfaceUrl::placeholder { color: var(--text-faint); }
#createSurfaceUrl:focus {
  outline: none; border-color: rgba(245, 197, 24, .55);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
#createSurfaceGoBtn { flex: none; padding: 0 16px; }

/* The one gold moment on this screen: a file is actually about to land here.
   Transient by nature (removed the instant the drag ends or drops), so it
   never competes with the header's own persistent gold button. */
.create-bar.is-drag-active .create-drop {
  border-style: solid; border-color: var(--accent);
  background: linear-gradient(180deg, rgba(245, 197, 24, .12) 0%, var(--surface-2) 70%);
  box-shadow: 0 0 0 1px rgba(245, 197, 24, .2), 0 10px 30px -18px rgba(245, 197, 24, .55);
}
.create-bar.is-drag-active .create-drop-icon,
.create-bar.is-drag-active .create-drop-title { color: var(--accent); }

/* Below 720px the link field would squeeze the drop label to nothing; drop
   the field rather than stack two half-width controls -- the header's own
   entry point still reaches the same importFromUrl(). */
@media (max-width: 720px) {
  .create-bar-link { display: none; }
}

.project-card {
  display: flex; flex-direction: column; text-align: left;
  padding: 0; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); color: var(--text); font: inherit;
  cursor: pointer;
  transition: border-color .12s ease, transform .12s ease;
}
.project-card:hover { border-color: var(--line-strong); transform: translateY(-1px); }
/* State at arm's length: a real frame, not a tint. */
.project-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Round 15, D1. Three cards in ONE row measured 593.7 / 576.3 / 183.8px tall
   and their title baselines disagreed by 409.9px, because `aspect-ratio` was
   losing to the poster inside it: a box with a preferred aspect ratio gets a
   CONTENT-BASED automatic minimum size in the ratio-dependent axis, so a 9:16
   filmstrip 549.7px tall pushed its own 16:9 frame to 550.7px. Measured here
   on a landscape/portrait/square trio: 175.7 / 550.7 / 312.5px before, and
   `overflow: hidden` (or `min-height: 0`) put all three on 175.2px. Both are
   set: overflow is what makes `object-fit: cover` actually crop, min-height is
   what states the intent. */
.proj-thumb {
  position: relative; display: block; aspect-ratio: 16 / 9;
  min-height: 0; overflow: hidden;
  background: #06070a; border-bottom: 1px solid var(--line);
}
.proj-thumb img {
  width: 100%; height: 100%; object-fit: contain; display: block;
  /* Preserve the uploaded picture's colour and contrast. */
  filter: none;
}
/* The frame every thumbnail gets regardless of what is inside it: a hairline
   inner edge and a floor-to-ceiling scrim so the duration chip and the hover
   cue below always sit on a readable surface. */
.proj-thumb-frame {
  position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .05);
  background: none;
}
.proj-length {
  position: absolute; right: 8px; bottom: 8px;
  padding: 4px 8px; border-radius: var(--r-sm);
  background: rgba(6, 7, 10, .82); border: 1px solid var(--line);
  font-size: 10px; font-variant-numeric: tabular-nums; color: var(--text);
}
/* Round 27: hover/focus reveals the card's one real action. Hidden on a card
   that cannot be opened yet (still uploading, or no server row at all) --
   an affordance for an action that would fail is worse than none. */
.proj-open-cue {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600; letter-spacing: .01em; color: var(--text);
  background: rgba(6, 7, 10, .5);
  opacity: 0; transition: opacity .15s ease;
}
.project-card:hover .proj-open-cue,
.project-card:focus-visible .proj-open-cue { opacity: 1; }
.project-card[data-importing] .proj-open-cue,
.project-card.is-unopenable .proj-open-cue { display: none; }

.proj-body { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-3); }
.proj-name {
  font-size: 14px; font-weight: 600; letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Round 27: status (tone-coloured, what this card is doing) and duration ·
   date (quiet, when/how long) used to be two stacked lines with no visual
   relationship. One row now, the way opus.pro reads a card at a glance. */
.proj-meta { display: flex; align-items: baseline; gap: 6px; min-width: 0; flex-wrap: wrap; }
/* P1: a status is allowed two lines; a clipped sentence said nothing. */
.proj-meta .proj-status { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.proj-meta .proj-when:not(:empty)::before { content: "·"; margin-right: 6px; color: var(--text-faint); }
.proj-status { font-size: 12px; color: var(--text-dim); }
.proj-status.tone-busy  { color: var(--accent); }
.proj-status.tone-error { color: var(--err); }
.proj-status.tone-warn  { color: var(--text-dim); }   /* P1: not an alarm, and no foreign hue */
.proj-retry { margin-left: var(--sp-2); }
.proj-when { font-size: 10px; color: var(--text-faint); }

.proj-bar {
  display: block; height: 3px; margin-top: var(--sp-1);
  border-radius: 2px; background: var(--surface-3); overflow: hidden;
}
/* Metal at a key moment only: the thing that is actually moving. */
.proj-bar-fill {
  display: block; height: 100%; width: 0;
  background: linear-gradient(180deg, #fae584 0%, var(--accent) 55%, #c69a0c 100%);
  transition: width .3s ease;
}
.proj-bar-fill.is-indeterminate { width: 40%; animation: projBarSlide 1.4s ease-in-out infinite; }
@keyframes projBarSlide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}

/* ── A video that is still being processed ────────────────────────────────
   The owner's most-repeated complaint in this whole project is "nothing
   happens / no feedback / blank screen". So the card he just added is the one
   card on the screen that is MOVING, and it is obvious at a glance which one
   it is: a gold rim, a breathing sheen across the thumbnail, and a clock that
   ticks every second underneath. Everything else on the shelf stays still. */
.project-card[data-importing] {
  border-color: rgba(245, 197, 24, .42);
  box-shadow: 0 0 0 1px rgba(245, 197, 24, .10), 0 8px 26px -18px rgba(245, 197, 24, .5);
}
.project-card[data-importing] .proj-thumb::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg,
    transparent 35%, rgba(245, 197, 24, .13) 50%, transparent 65%);
  background-size: 260% 100%;
  animation: projThumbSheen 2.4s linear infinite;
}
@keyframes projThumbSheen {
  0%   { background-position: 140% 0; }
  100% { background-position: -140% 0; }
}
/* Not yet a project row on the server: nothing to open, but never greyed out
   -- grey reads as broken, which is the exact impression this round exists to
   remove. */
.project-card.is-unopenable { cursor: default; }
.project-card.is-unopenable:hover { transform: none; }
.proj-elapsed {
  font: 600 10px/1 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--text-faint); font-weight: 600;
}
.proj-status.tone-busy .proj-elapsed { color: var(--accent-dim, var(--accent)); }
.proj-status.tone-done { color: var(--ok, var(--accent)); }

/* Motion is the message here, so it is not removed under reduced-motion --
   it is slowed to a state change the eye still reads without any sliding. */
@media (prefers-reduced-motion: reduce) {
  .proj-bar-fill.is-indeterminate { width: 100%; animation: projBarPulse 2s ease-in-out infinite; }
  .project-card[data-importing] .proj-thumb::after { animation: projBarPulse 2.6s ease-in-out infinite; }
  @keyframes projBarPulse { 0%, 100% { opacity: .28; } 50% { opacity: 1; } }
}

.library-empty {
  max-width: 460px; margin: 12vh auto 0; text-align: center; color: var(--text-dim);
}
.library-empty svg { color: var(--text-faint); }
.library-empty h2 {
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: 14px; font-weight: 600; color: var(--text); letter-spacing: -.01em;
  /* Inherited uppercase from a rule outside this block rendered "IMPORT A
     VIDEO" in caps at NEGATIVE tracking (-0.17px) -- caps tighten badly and
     the screen's other caps run sits at +0.6px. The HTML already reads
     "Import a video"; sentence case matches "Drop a video" in the bar. */
  text-transform: none;
}
.library-empty p { margin: 0 0 var(--sp-5); font-size: 12px; }
.library-empty-actions {
  display: flex; align-items: center; justify-content: center; gap: var(--sp-2);
}

/* Round 26b. When the crumb/view-switch are hidden (Projects has no project
   loaded yet), this used to sit wherever flex left it -- right after the
   brand, near x=129 at 1920px. On Clips it sat near x=1891. Same control
   jumping sides between the two views it lives in. margin-left:auto pins it
   to the header's right edge unconditionally, in both views. */
.topbar-actions { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.auth-signout { gap: 6px; }
.auth-signout .signout-icon { flex: none; }
/* Round 26b. A `position: fixed` corner tag at 1366x768 measured one full
   overlap with a clip card -- it was floating on top of the product surface
   it was supposedly staying out of the way of. It carries no information a
   person acts on day to day (it exists so someone CAN say a number when
   asked), so it no longer sits on the UI surface at all: it lives inside the
   Trash sheet's header, in flow, where it can never overlap anything. */
.build-stamp {
  font: 10px/1 var(--mono); color: var(--text-faint);
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
  user-select: all;
}
.build-stamp:empty { display: none; }

.chip {
  display: inline-flex; align-items: center; gap: 8px;
  height: 24px; padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 99px;
  font-size: 10px; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
#healthText { font-size: 12px; }
.chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-faint); flex: none; }
/* Round 26b. The label used to vanish in the two most common states (muted,
   ok) via display:none, leaving a bare 24px dot with the actual status --
   "Ready" -- only reachable through a hover tooltip. State that's supposed to
   be visible at arm's length can't depend on a hover. Text stays on screen in
   every state now; only the border/background go quiet when there's nothing
   wrong. */
.chip-muted, .chip-ok { border-color: transparent; background: transparent; }
.chip-ok   { color: var(--ok); }
.chip-ok   .dot { background: var(--ok); box-shadow: 0 0 7px var(--ok); }
.chip-warn { color: var(--warn); border-color: rgba(240,136,62,.3);  background: rgba(240,136,62,.07); }
.chip-warn .dot { background: var(--warn); }
.chip-err  { color: var(--err);  border-color: rgba(242,85,90,.3);   background: rgba(242,85,90,.07); }
.chip-err  .dot { background: var(--err); }

/* ── Import-by-URL dropdown ──────────────────────────────────────────────── */
.import-pop {
  position: fixed; top: calc(var(--topbar-h) + 8px); right: var(--sp-4);
  z-index: 60; width: min(440px, calc(100vw - 2 * var(--sp-4)));
  background:
    radial-gradient(120% 130% at 50% -50%, rgba(245,197,24,.08), transparent 60%),
    var(--surface-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.05);
  animation: toast-in .18s ease;
}
.import-pop h3 { margin-bottom: 9px; }
.import-pop-row { display: flex; gap: var(--sp-2); align-items: stretch; }
#importUrl {
  flex: 1; min-width: 0;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  padding: 9px 11px; font: 400 12px/1.3 var(--font);
}
#importUrl:focus {
  outline: none; border-color: rgba(245,197,24,.55);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
#importUrl::placeholder { color: var(--text-faint); }
/* Round 12, defect #3: #importGoBtn demoted from .btn-gold to .btn-secondary
   (see index.html) -- this sizing rule follows the id, not the removed
   modifier class, so the popover's button keeps its compact height either
   way. */
#importGoBtn { padding: 0 16px; font-size: 12px; }
.import-pop .muted { margin-top: 9px; font-size: 12px; color: var(--text-faint); }

/* ── Layout ──────────────────────────────────────────────────────────────── */
/* CapCut's shape: media list left, player centre, properties right, and ONE
   timeline spanning the full width underneath all three. The timeline is the
   thing you scrub while looking at the player, so it gets the whole width
   rather than being stacked in the same column as the picture. */
.layout {
  display: grid;
  /* The two splitter columns are the drag handles between the panels. The
     track sizes are driven by --rail-w / --panel-w so a drag is one variable
     write, not a layout rewrite. A 9:16 picture can never use a wide centre
     column, so the leftover width goes to the properties panel instead of
     sitting empty either side of the frame. */
  grid-template-columns:
    var(--rail-w, 230px) 5px minmax(240px, 1fr) 5px var(--panel-w, 420px);
  grid-template-rows: minmax(0, 1fr) 5px var(--dock-h, auto);
  height: calc(100vh - var(--topbar-h));
  overflow: hidden;
}
.layout:not(.simple-mode) .rail   { grid-row: 1; grid-column: 1; }
.layout:not(.simple-mode) #splitRail  { grid-column: 2; }
.layout:not(.simple-mode) .stage  { grid-row: 1; grid-column: 3; }
.layout:not(.simple-mode) #splitPanel { grid-column: 4; }
.layout:not(.simple-mode) .editor { grid-row: 1; grid-column: 5; }
.layout:not(.simple-mode) #splitDock  { grid-row: 2; }
.timeline-dock {
  grid-row: 3; grid-column: 1 / -1;
  display: flex; flex-direction: column;
  background: var(--surface);
  border-top: 1px solid var(--line-strong);
  /* Round 13, defect 5. This cap and dockHeightCeiling() in editor.js were
     two different answers to one question, and the CSS one was much the
     smaller: at 1600x1000 `max(150px, 18vh)` resolved to 180px against 317px
     of content, so the dock hid 138px of itself -- the PUNCH-IN, SPEED and
     MEDIA lanes and the time ruler, i.e. exactly the four rows round 11 made
     permanent so they would teach themselves. Meanwhile the JS said 275px was
     affordable in that same window.
     The cap IS dockHeightCeiling() now, expressed in CSS: clamp(140px,
     45vh - 175px, 480px), the formula derived from the picture's own budget
     (the stage loses ~57px of the window and ~116px of itself, and the
     portrait frame keeps >=55% of the viewport). Measured at 1600x1000:
     dock 179 -> 275, every lane and the ruler above the fold, player frame
     655 -> 560px = 56.0% of the viewport. At 2304x1142 the whole 317px of
     content fits with the cap at 338. A splitter drag still writes --dock-h,
     which owns the height up to its own 480px max, so the cap never fights
     the user's own drag and the drag persists. */
  max-height: var(--dock-h, clamp(140px, calc(45vh - 175px), 480px));
  overflow-y: auto;
  /* Round 15, D12: overlay scrollbars made the dock's own scroll invisible, so
     four lanes and the ruler read as cut off rather than as scrolled past. A
     permanent thin gutter is half the answer; #dockMore below is the other. */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  overscroll-behavior: contain;
}
.timeline-dock::-webkit-scrollbar { width: 10px; }
.timeline-dock::-webkit-scrollbar-track { background: var(--scroll-track); }
::-webkit-scrollbar-corner { background: var(--scroll-track); }
.timeline-dock::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: 5px;
  border: 3px solid transparent; background-clip: content-box;
}
.timeline-dock::-webkit-scrollbar-thumb:hover { background-color: var(--text-faint); background-clip: content-box; }

/* D12: the "there is more down here" bar. Sticky at the dock's bottom edge so
   it rides the scroll, and `hidden` (not merely faded) whenever the dock is
   fully scrolled -- an affordance that lies about there being more below is
   worse than none. */
.dock-more {
  position: sticky; bottom: 0; z-index: 3;
  flex: none; width: 100%; margin-top: auto;
  appearance: none; border: 0; border-top: 1px solid var(--line);
  padding: 5px 12px; cursor: pointer; text-align: center;
  font: 600 10px/1.4 var(--font); letter-spacing: .04em;
  color: var(--accent);
  background: linear-gradient(180deg, rgba(11,12,15,0) 0%, var(--surface) 55%);
}
.dock-more:hover { color: #fae584; }
.dock-more[hidden] { display: none; }
.layout.view-board .timeline-dock,
.layout.view-board .splitter { display: none; }

/* Collapsed properties panel: zero the WIDTH VARIABLE, don't rewrite the grid.
   Restating grid-template-columns here looked equivalent and was not -- it
   overrode the narrow-viewport rule below, which uses a different set of
   tracks, so on a narrow window collapsing the panel made .stage SHRINK from
   435px to 210px instead of growing. Measured, not guessed. Both grid
   definitions already size the panel from --panel-w, so setting it to zero is
   correct in every layout mode, including any added later.

   ...and it was still beaten by the cascade. `--panel-w` is written INLINE on
   .layout by the splitter drag (setupSplitters), and inline wins over a class,
   so for anyone who had ever dragged the panel this rule did nothing: measured
   at 1280x800 with a 520px drag, collapsing gave `0 5 750 5 520` with .editor
   at display:none -- 520px of the window reserved for a hidden element, which
   is the exact black Quincy ringed. !important is the narrow tool for exactly
   this: a remembered width is a preference about a panel that is ON SCREEN, and
   it cannot outvote the panel not being on screen. Same reasoning as
   .panel-empty below; they are two doors into one hole. */
.layout.panel-collapsed { --panel-w: 0px !important; }
.layout.panel-collapsed .editor,
.layout.panel-collapsed #splitPanel { display: none; }

/* An EMPTY properties column is not a panel, it is a hole. Measured at
   1366x768 on a 9:16 clip with no caption: the picture had 361px, the timeline
   621px, and the last track held 369px of pure black because #postCard -- its
   only child -- was `hidden`. That is 27% of his window reserved for nothing,
   and it is the black he ringed with "DONT WASTE SPACE".
   Separate from .panel-collapsed on purpose: that is a CHOICE the user made
   and it earns a reveal button to undo it. This is "there is nothing to show",
   so there is nothing to reveal and no button -- when a caption exists the
   column comes back on its own.
   !important because the splitter drag writes --panel-w INLINE on .layout
   (setupSplitters/savePanelSizes), and an inline value beats a class rule --
   without it, a user who had ever dragged the panel would keep their old
   width as a gap next to a display:none element. A remembered width is a
   preference about a panel that HAS content; it cannot outvote the panel
   being empty. */
.layout.panel-empty { --panel-w: 0px !important; }
.layout.panel-empty .editor,
.layout.panel-empty #splitPanel { display: none; }

/* ── Round 10, item 1a: collapsing the panel must actually grow the picture ──
   It did not. Measured on main at 2304x1142: hiding the properties panel
   frees 420px of WIDTH and the video stayed 358x661 -- it only re-centred.
   The reason is that a 9:16 picture in this layout is HEIGHT-bound, never
   width-bound (the stage column was 1644px wide holding a 354px frame), so
   freed width is worth exactly nothing to it.
   The fix is to spend the freed width on the thing that IS eating the height:
   with the panel gone, the timeline moves out of the full-width row under the
   stage and into the column the panel vacated. The stage then owns the whole
   height of the window instead of the leftover above a dock, and the timeline
   trades some horizontal time-resolution for ~3x the lane height it had.
   Only above 1400px, where there is genuinely width to spare -- on a narrow
   window the timeline needs every pixel of width it can get and the dock
   stays where it is. */
@media (min-width: 1400px) {
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) {
    grid-template-columns:
      var(--rail-w, 230px) 5px minmax(240px, 1fr) 5px var(--dock-side-w, 44%);
    grid-template-rows: minmax(0, 1fr);
  }
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) #splitDock { display: none; }
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) .timeline-dock {
    grid-row: 1; grid-column: 5;
    max-height: none;                 /* the row track is the budget now */
    border-top: 0; border-left: 1px solid var(--line-strong);
  }
}

/* ── Round 19: a 9:16 clip puts the timeline BESIDE the picture ───────────
   Quincy, twice, looking at his own editor:

     "WHAT IS THIS?"  (ringed round the black either side of the picture)
     "i also like this view better where i can see the full 9:16 clip when the
      timeline is under it, it forces the view of the clip small"
     "for a 16:9 its fine with the timeline under but for a 9:16 its much
      better on the right side"

   Both complaints are one fact seen twice. A portrait picture is HEIGHT-hungry
   and WIDTH-poor. A timeline underneath it takes the only axis it can grow on,
   while the axis it cannot use sits empty right next to it. Measured on the
   running app at 2000x1160, editor view, 9:16 clip:

       stage column 1340 wide, picture 382x679
       -> 958px of black beside the picture, 71.5% of the column
       -> picture 679px tall, 58.5% of the viewport

   So for a PORTRAIT output the two swap axes: the timeline moves into a column
   of its own on the right, and the stage takes the full height of the window
   instead of surrendering its bottom to a dock.

   16:9 is his explicit carve-out and does not move: the whole block is gated on
   :has(.player-frame.ar-portrait), which a landscape or square output never
   matches. The gate is also why nothing here measures anything -- the shape is
   read off a class the aspect already sets, and every size below is a function
   of the VIEWPORT, which no write of ours can move. That is deliberate: the
   last change to this area shipped a 30Hz feedback loop because a measurement
   resized the element its own observer was watching.

   Only above 1400px. Below that there is no spare width to give the lanes, and
   splitting the row would cost the picture height as well -- measured at
   1366x768 the side-by-side arrangement makes the picture SMALLER, so it stays
   stacked there. --portrait-picture-h is the picture's height budget, and the
   stage column is that budget times 9/16 plus the wrap's own padding; because
   both derive from the same constant they can never disagree, and because the
   frame below is sized WIDTH-first with a max-height, a squeezed column makes
   the picture smaller rather than breaking its shape. */
@media (min-width: 1200px) {
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) {
    /* topbar 52 + stage head (two clamped title lines) 62 + the wrap's own
       strip, scrub bar, gaps and padding 72. */
    --portrait-picture-h: calc(100vh - 186px);
    grid-template-columns:
      var(--rail-w, 230px) 5px
      minmax(240px, calc(var(--portrait-picture-h) * 9 / 16 + 34px)) 5px
      minmax(320px, 1fr) 5px
      /* Round 22, defect 3: THE PICTURE WAS NOT EVEN AS BIG AS IT WAS ALLOWED
         TO BE. `--portrait-picture-h` caps a 9:16 picture at 614x345 in a
         1280x800 window, which is 22.1% of the editing area -- that ceiling is
         arithmetic and no layout can beat it. But the picture MEASURED
         282x501: only 14.8%, two thirds of its own ceiling. The reason was
         this track. 1280 - 230 rail - 15 splitters - 320 dock-minimum - 420
         panel leaves the stage 295px, so the picture was squeezed to 282 wide
         and then lost the height to match, to stay 9:16.
         420px is a good width for the panel on a big screen and a third of a
         1280px one. Making the DEFAULT responsive gives that width back to the
         picture where it is scarce and changes nothing at 1600 or 2000, where
         27vw is already past 420. It is the default, not a cap: a user who has
         dragged the splitter has set --panel-w, and their own drag still wins
         outright -- the mistake round 19 recorded was a guard that fought the
         drag. */
      var(--panel-w, clamp(300px, 27vw, 420px));
    grid-template-rows: minmax(0, 1fr);
  }
  /* The row splitter has no row to size any more. Same call the panel-collapsed
     side-dock above already makes, for the same reason. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) #splitDock { display: none; }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) .stage { grid-row: 1; grid-column: 3; }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) .timeline-dock {
    grid-row: 1; grid-column: 5;
    max-height: none;                 /* the row track is the whole budget now */
    border-top: 0; border-left: 1px solid var(--line-strong);
  }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) #splitPanel { grid-column: 6; }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) .editor { grid-row: 1; grid-column: 7; }

  /* WIDTH-first, so the shape survives a squeeze. `height: 100%` with a
     max-width is what lets aspect-ratio lose when the column is the binding
     constraint -- the box keeps the full height and just gets narrow, and the
     video letterboxes inside it, which is the dead black all over again one
     level down. Driving the width and deriving the height means both bounds
     agree by construction: the picture gets smaller, never squatter.
     :not(.is-source) because the source view deliberately takes the SOURCE's
     own aspect while you are moving the frame. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.ar-portrait) .player-frame.ar-portrait:not(.is-source),
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.focus):has(.player-frame.vertical) .player-frame.vertical:not(.is-source) {
    width: 100%; height: auto;
    max-width: calc(var(--portrait-picture-h) * 9 / 16);
    max-height: var(--portrait-picture-h);
    aspect-ratio: 9 / 16;
    /* A flex item's automatic minimum width is TRANSFERRED through the aspect
       ratio from its max-height, so the frame would refuse to go below
       (picture height x 9/16) and shoved .player-wrap out of the column --
       measured at 1280x800: stage 315px, wrap 398.9px, picture 345.4px, i.e.
       30px of picture outside the column. min-width:0 lets `width: 100%`
       actually mean the column. */
    min-width: 0;
  }
}

.panel-collapse {
  margin-left: auto;
  background: none; border: 0; cursor: pointer;
  color: var(--muted); font-size: 17px; line-height: 1;
  padding: 4px 8px; border-radius: 5px;
  transition: color .15s ease, background .15s ease;
}
.panel-collapse:hover { color: var(--text); background: rgba(255,255,255,.06); }

/* Only exists while collapsed, pinned to the edge the panel came from so the
   way back is where the panel used to be. Used to be a bare 17px sliver --
   just the chevron glyph plus a hair of padding -- easy to miss and hard to
   aim at. Now a real ≥32px tab with its own label, same idiom as any NLE's
   collapsed-panel tab. */
.panel-reveal {
  display: none;
  flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-1);
  position: fixed; right: 0; top: 50%; transform: translateY(-50%);
  z-index: 40;
  min-width: 32px; min-height: 84px;
  background: var(--surface); border: 1px solid var(--line-strong); border-right: 0;
  border-radius: 6px 0 0 6px;
  color: var(--muted);
  padding: var(--sp-3) var(--sp-2); cursor: pointer;
  transition: color .15s ease, background .15s ease;
}
.panel-reveal-chevron { font-size: 17px; line-height: 1; }
.panel-reveal-label {
  writing-mode: vertical-rl; transform: rotate(180deg);
  font: 10px/1 var(--mono); letter-spacing: .12em; color: var(--text-faint);
}
.panel-reveal:hover { color: var(--text); background: rgba(255,255,255,.06); }
.panel-reveal:hover .panel-reveal-label { color: var(--text); }
body:has(.layout.panel-collapsed) .panel-reveal { display: flex; }
/* The board has no properties panel to reveal. */
body:has(.layout.view-board) .panel-reveal { display: none; }

/* ── The clip list is a drawer in the Editor, not a wall ──────────────────
   Quincy ringed the entire left column and wrote HIDE down it. In the Editor
   the clip list has exactly one errand -- go and pick a different clip -- and
   a column that is only occasionally an errand should not hold a fifth of the
   window while he edits. It is hidden on every entry into the Editor, the
   CLIPS tab on the left edge brings it back, and picking a clip puts it away
   again. The Clips BOARD is untouched: there the list is the whole point. */
/* The THIRD door into the same hole as .panel-collapsed and .panel-empty
   below: `--rail-w` is written INLINE on .layout by setupSplitters when the
   clips rail is dragged, and inline beats a class, so for anyone who had ever
   widened the rail, HIDING it left its remembered width reserved as pure
   black. Measured at 1920x1080 with a 500px remembered rail, rail hidden:

       columns   500px 5px 536.875px 5px 448.125px 5px 420px
       stage.x   5 -> 505      (the picture shoved right by 500px)
       timeline  1340 -> 840   (-37% of the width it should have)

   which is exactly the screenshot Quincy sent -- an empty column on the left,
   the video pushed off-centre, and the timeline squeezed into what was left.
   Same narrow tool and the same reasoning as .panel-collapsed: a remembered
   width is a preference about a rail that is ON SCREEN, and it cannot outvote
   the rail not being on screen. */
.layout.rail-hidden { --rail-w: 0px !important; }
.layout.rail-hidden .rail,
.layout.rail-hidden #splitRail { display: none; }
.rail-hide { margin-left: auto; }
.layout.view-board .rail-hide { display: none; }
.rail-reveal {
  display: none;
  flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-1);
  position: fixed; left: 0; top: 50%; transform: translateY(-50%);
  z-index: 40;
  min-width: 32px; min-height: 84px;
  background: var(--surface); border: 1px solid var(--line-strong); border-left: 0;
  border-radius: 0 6px 6px 0;
  color: var(--muted);
  padding: var(--sp-3) var(--sp-2); cursor: pointer;
  transition: color .15s ease, background .15s ease;
}
.rail-reveal-label {
  writing-mode: vertical-rl;
  font: 10px/1 var(--mono); letter-spacing: .12em; color: var(--text-faint);
}
.rail-reveal:hover { color: var(--text); background: rgba(255,255,255,.06); }
.rail-reveal:hover .rail-reveal-label { color: var(--text); }
body:has(.layout.rail-hidden) .rail-reveal { display: flex; }
/* The board IS the clip list; there is nothing to reveal. */
body:has(.layout.view-board) .rail-reveal { display: none; }


/* ── Narrow windows: three permanent columns do not fit ───────────────────
   The desktop grid demands 230 + 5 + 240 + 5 + 420 = 900px minimum. Quincy's
   real window is the 780px deck dock, where grid cannot shrink the fixed
   tracks any further, so with overflow:hidden on the layout the rail and the
   properties panel were crushed to 0x0 — the caption picker was not cramped,
   it was INVISIBLE. No amount of tuning fixes 900 into 780; the layout has to
   change shape.

   Below 1180px the clips list drops out of the editor (it is a whole view of
   its own, one click away on the Clips tab) and the picture keeps the room.
   The panel width is additionally clamped to a share of the viewport so a
   previously-dragged --panel-w cannot re-break the layout on a small screen. */
@media (max-width: 1180px) {
  .layout:not(.simple-mode) {
    grid-template-columns: minmax(200px, 1fr) 5px min(var(--panel-w, 340px), 46vw);
  }
  .layout:not(.simple-mode) .rail,
  .layout:not(.simple-mode) #splitRail { display: none; }
  .layout:not(.simple-mode) .stage      { grid-column: 1; }
  .layout:not(.simple-mode) #splitPanel { grid-column: 2; }
  .layout:not(.simple-mode) .editor     { grid-column: 3; }
}

/* ── Clip rail ───────────────────────────────────────────────────────────── */
.rail {
  border-right: 1px solid var(--line);
  background: var(--surface);
  display: flex; flex-direction: column;
  min-height: 0;
}
.rail-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
/* The heading takes the slack so the two controls sit together on the right,
   rather than Import floating alone in the middle. */
.rail-head h2 { flex: 1 1 auto; min-width: 0; }
.rail-import { flex: none; cursor: pointer; }
.rail-prompt {
  display: flex; gap: 6px;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  border-bottom: 1px solid var(--line);
}
#promptInput {
  flex: 1; min-width: 0; height: 26px; padding: 0 9px;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 12px;
}
#promptInput::placeholder { color: var(--text-faint); }
#promptInput:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
#targetCount {
  width: 46px; height: 26px; padding: 0 2px 0 6px; flex: none;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--text); font: 12px var(--mono); text-align: center;
  font-variant-numeric: tabular-nums;
}
#targetCount:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Round 12, defect #5 sweep: same treatment as #boardCount's own
   .find-count rule just above -- a native spinner is still a native widget
   this app's chrome does not otherwise use. */
#targetCount::-webkit-inner-spin-button,
#targetCount::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
#targetCount { -moz-appearance: textfield; }

.rail-status {
  padding: var(--sp-3) var(--sp-4);
  font-size: 12px; color: var(--text-dim);
  border-bottom: 1px solid var(--line);
  min-height: 38px;
}
/* Round 13, defect 7: why this batch of clips is named and scored the way it
   is, in the server's own words, directly above the batch. One gold hairline
   on the leading edge marks it as the app explaining itself rather than a
   clip -- the same weight the rail status above it carries, so it never
   competes with the list for attention. Hidden outright when there is
   nothing to say. */
.clip-notice {
  margin: 0; padding: var(--sp-2) var(--sp-4) var(--sp-2) calc(var(--sp-4) - 2px);
  border-left: 2px solid var(--accent);
  border-bottom: 1px solid var(--line);
  font-size: 12px; line-height: 1.45; color: var(--text-dim);
  background: rgba(245,197,24,.05);
}
.clip-notice[hidden] { display: none; }
.clip-list { list-style: none; margin: 0; padding: var(--sp-2); overflow-y: auto; flex: 1; }

.clip-card {
  position: relative;
  padding: 10px 11px 10px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface-2);
  cursor: pointer;
  margin-bottom: 8px;
  transition: border-color .13s ease, background .13s ease;
}
.clip-card::before {
  content: ""; position: absolute; left: 0; top: 9px; bottom: 9px; width: 2px;
  border-radius: 0 2px 2px 0; background: transparent; transition: background .13s ease;
}
.clip-card:hover { background: var(--surface-3); border-color: var(--line-strong); }
.clip-card.is-active { border-color: rgba(245,197,24,.45); background: rgba(245,197,24,.06); }
.clip-card.is-active::before { background: var(--accent); }

.clip-card-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.clip-score {
  font: 600 10px/1 var(--mono); color: var(--accent);
  background: rgba(245,197,24,.1); border: 1px solid rgba(245,197,24,.22);
  padding: 4px 5px; border-radius: 4px; flex: none;
}
.clip-name {
  font-size: 12px; font-weight: 500; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.clip-meta {
  margin-top: 5px; display: flex; align-items: center; gap: 7px;
  font: 10px/1 var(--mono); color: var(--text-faint);
}
/* Virality subscores: Hook / Flow / Value / Trend, on cards and in the stage head. */
.clip-subs {
  margin-top: 5px; display: flex; gap: 4px;
  font: 10px/1 var(--mono);
}
.sub-chip {
  color: var(--text-faint);
  background: var(--surface-3);
  border: 1px solid var(--line);
  padding: 2px 4px; border-radius: 3px;
  letter-spacing: .02em; white-space: nowrap;
}
/* The four subscores are numbers you read too, and #9aa1ab on the dark chip
   was the dimmest text in the app. Gold, like every other score: the detail
   panel has always drawn these digits gold (see .clip-detail-subs below),
   and the rail card drawing the same four numbers grey was the odd one out.
   The WORD stays faint and the NUMBER carries the gold, so a row of chips
   still reads as one quiet strip rather than four badges. Quincy, 2026-09-16. */
.sub-chip b { color: var(--accent); font-weight: 600; }
/* Round 14, D5: a subscore the analysis could not measure. The em dash is
   already the whole message, so the chip just steps back rather than
   competing with the three that carry real numbers. */
.sub-chip.is-unmeasured { opacity: .62; }
/* An em dash is not a score, so it never takes the gold -- everywhere, not
   just in the detail panel (it was detail-only while the rail digits were
   grey anyway; now that they are gold the rule has to cover both). */
.sub-chip.is-unmeasured b { color: var(--text-faint); }
/* A chip that explains itself on hover should say so with the cursor, the
   same way the score badge does. */
.sub-chip[title] { cursor: help; }

.clip-state { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; padding: 2px 5px; border-radius: 3px; }
.clip-state.ready     { color: var(--ok);   background: rgba(201,209,220,.12); }
.clip-state.rendering,
.clip-state.queued    { color: var(--live); background: rgba(91,157,255,.12); }
.clip-state.error     { color: var(--err);  background: rgba(242,85,90,.12); }

.clip-progress { height: 2px; margin-top: 7px; background: rgba(255,255,255,.08); border-radius: 2px; overflow: hidden; }
.clip-progress i { display: block; height: 100%; background: var(--live); transition: width .2s ease; }

/* ── Stage ───────────────────────────────────────────────────────────────── */
/* Grid, not flex, so the player's row has a DEFINITE height. A flex item's
   resolved height does not reliably anchor a percentage height on its child,
   which left the frame falling back to its min-height; a `minmax(0, 1fr)` grid
   track does. */
.stage {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  /* And a DEFINITE column. With only rows declared the stage had one implicit
     `auto` column, and an auto track sizes to its content's MAX-content -- so
     .player-wrap was sized by the framing strip's uncollapsed sentence plus the
     Crop size slider plus the picture's own max-width, and simply overflowed
     the column. Measured at 1280x800: stage column 315px, .player-wrap 398.9px;
     at 2000x1160 before this round, .player-wrap 1042px inside a 1340px stage.
     .stage's overflow:hidden was quietly shaving the difference off the right
     edge, which is where the Crop size slider and Reset-to-auto live. */
  grid-template-columns: minmax(0, 1fr);
  border-right: 1px solid var(--line);
  min-width: 0; min-height: 0;
  overflow: hidden;      /* the picture fits by construction now */
}
.stage-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4);
  flex-wrap: wrap;   /* P1: the switches drop under a long title instead of clipping */
  padding: var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.stage-head h1 { line-height: 1.35; }
.stage-head p { margin-top: 3px; max-width: 62ch; }
.stage-toggles { display: flex; gap: var(--sp-3); flex: none; flex-wrap: wrap; row-gap: var(--sp-2); }
.layout:not(.focus) .stage-toggles { flex: 1 1 100%; min-width: 0; }
#clipTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* P1: no instructional copy in the chrome. The hint lives in the ? help. */
.editor-foot #editorHint { display: none; }

/* ── The Post card ────────────────────────────────────────────────────────
   The caption, the hashtags, the four subscores and the analyst's reason,
   in the properties panel above the tab strip. Quiet by construction: no
   frame of its own beyond the panel's own hairline, no second accent, and
   the four type steps only (14 caption / 12 reason / 10 chips + pills).
   The card only exists while a clip does — #postCard[hidden] otherwise, so
   an empty panel shows an empty panel, not an empty box. */
.post-card {
  flex: none;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.post-card-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}
/* The caption is the thing he came for, so it is the largest text in the card
   and the only one at body size. It scrolls rather than growing without limit:
   a long caption must not push the tab strip off the top of the panel. */
.post-card .clip-desc {
  margin-top: 0; font-size: 14px; color: var(--text);
  max-width: none; max-height: 128px; overflow-y: auto;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.post-card #clipRationale { margin-top: var(--sp-2); color: var(--text-faint); }

/* PA2: the four post fields. Each row is label | text | copy. The text is an
   input dressed as text: no box until it is focused, so the card reads as
   copy, not as a form -- and one tap on the glyph at the end takes it. */
.post-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.post-fields { display: flex; flex-direction: column; gap: 2px; }
.post-field {
  display: grid; grid-template-columns: 52px minmax(0, 1fr) 26px; align-items: start; gap: 6px;
  padding: 4px 0; border-top: 1px solid var(--line);
}
.post-field:first-child { border-top: 0; }
.post-label {
  padding-top: 5px; font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-dim);
}
.post-input {
  width: 100%; min-width: 0; margin: 0; padding: 4px 6px; box-sizing: border-box;
  border: 1px solid transparent; border-radius: 6px; background: transparent;
  color: var(--text); font: inherit; font-size: 14px; line-height: 1.4; resize: none;
  overflow: hidden; overflow-wrap: anywhere; white-space: pre-wrap;
}
.post-input::placeholder { color: var(--text-faint); font-style: italic; }
.post-input:hover { background: var(--line); }
.post-input:focus { outline: none; border-color: var(--accent); background: var(--surface-3); }
.post-card #postHook { font-weight: 600; }
.post-card .post-input.clip-desc {
  margin-top: 0; font-size: 14px; color: var(--text); max-width: none; max-height: 160px;
  overflow-y: auto;
}
.post-tags-wrap { min-width: 0; display: flex; flex-direction: column; gap: 4px; padding-top: 3px; }
.post-tags-wrap .clip-hashtags { margin-top: 2px; cursor: text; }
.post-tags-input { font-family: var(--mono); font-size: 12px; }
.post-copy {
  width: 26px; height: 26px; margin-top: 1px; padding: 0; border: 1px solid var(--line);
  border-radius: 6px; background: var(--surface-3); color: var(--text-dim);
  font-size: 14px; line-height: 1; cursor: pointer;
}
.post-copy:hover, .post-copy:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }
.post-note { margin: var(--sp-2) 0 0; font-size: 12px; line-height: 1.4; color: var(--text-faint); }
/* Says only that the copy happened, next to the button that did it. Fades in
   and out under `hidden`, never a toast. */
.post-copied {
  display: block; margin-top: var(--sp-2);
  font: 10px/1 var(--mono); letter-spacing: .06em; color: var(--accent);
}

/* Clip detail: subscore chips, social description, hashtag pills. */
.clip-detail-subs { display: flex; gap: 5px; margin-top: 7px; font: 10px/1 var(--mono); }
.clip-detail-subs .sub-chip { padding: 4px 6px; }
.clip-desc { margin-top: 6px; font-size: 12px; color: var(--text-dim); max-width: 62ch; }
.clip-hashtags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.hashtag {
  font: 10px/1 var(--mono); color: var(--text-dim);
  background: var(--surface-2);
  border: 1px solid var(--line);
  padding: 4px 8px; border-radius: 99px;
  white-space: nowrap;
}

.switch { display: inline-flex; align-items: center; gap: 7px; font-size: 12px; color: var(--text-dim); cursor: pointer; user-select: none; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch span {
  width: 26px; height: 15px; border-radius: 99px;
  background: var(--surface-3); border: 1px solid var(--line-strong);
  position: relative; transition: background .15s ease, border-color .15s ease; flex: none;
}
.switch span::after {
  content: ""; position: absolute; top: 1.5px; left: 1.5px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--text-faint); transition: transform .15s ease, background .15s ease;
}
.switch input:checked + span { background: rgba(245,197,24,.24); border-color: rgba(245,197,24,.5); }
.switch input:checked + span::after { transform: translateX(11px); background: var(--accent); }
.switch input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch-block { justify-content: space-between; width: 100%; flex-direction: row-reverse; }

/* The player takes the stage's LEFTOVER height, so it always fits under the
   clip header instead of being pushed off the bottom of the window. Sizing the
   frame against a raw vh budget ignored the header entirely: in the 780x493
   deck dock the frame ran to y=608 in a 493px viewport, putting the caption
   block and its resize handles below the fold — the handles were unreachable,
   which read as "I can't resize the captions". min-height keeps it usable
   rather than collapsing to nothing when the column really is short (a
   collapsed player drives the caption font toward 0). */
.player-wrap {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  /* `1 1 0` — a DEFINITE flex basis, not `auto`. The frame is sized with
     height:100%, which only resolves against a parent whose height is itself
     definite; with an auto basis the parent sized to its content, the 100% fell
     back to auto, and the frame took its max-width and overflowed the column.
     Now the trimmer and the export controls live in the bottom dock, nothing
     else competes for this space, so the wrap can simply take what is left. */
  min-height: 0;
  /* AND min-width. A grid item's automatic minimum is its content's min-content
     size, and .stage-strip's min-content is the framing sentence plus the Crop
     size slider plus Reset-to-auto -- so the wrap refused to go below ~1042px.
     Measured at 1280x800: stage column 315px, .player-wrap 1042px, i.e. 727px
     of the strip hanging outside the column and clipped away by .stage's
     overflow:hidden, taking the Crop size slider and Reset with it. Every
     child inside the strip already knows how to shrink (min-width:0 and
     ellipsis on #frameStatus); the wrap was the one box that would not let
     them. It also makes `width: 100%` on the frame mean the column, which is
     what keeps a 9:16 picture inside its column at every width. */
  min-width: 0;
  height: 100%;
  align-items: center;
  justify-content: center;
}
.player-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 54vh;
  background: #000;
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
/* The picture is inset by --stage-pad on every side. That gutter is what
   guarantees the gold export border is ALWAYS drawn inside the visible box:
   before this, a border sitting exactly on the video's edge was shaved off by
   .player-frame's overflow:hidden whenever the column got narrow, so the frame
   looked open on the right and you could not tell where the export ended. */
.player-frame { --stage-pad: 14px; }
.player-frame video {
  width: calc(100% - var(--stage-pad) * 2);
  height: calc(100% - var(--stage-pad) * 2);
  object-fit: contain; display: block; background: #000;
}

/* Exported / live framing. `width: min(100%, height-budget x ratio)` is the
   whole trick: capping the height alone lets aspect-ratio lose (the box keeps
   the full width and just gets squat), so the width has to carry the height
   budget for the shape to survive in a short column. */
/* Height comes from the flex leftover (100%), width follows from the shape.
   The vh term stays as a second cap so the frame cannot grow absurdly wide on
   a very tall, very wide window. */
/* Round 10, item 1a: these vh caps are a SECOND ceiling on top of the column's
   own leftover height, and they were binding before the column was. Measured
   at 2304x1142 the column offered more than 66vh once the second control row
   was folded away, so the cap -- not the layout -- was what kept the picture
   short. Raised to 92vh: the column stays the real constraint, and the cap
   survives only as a guard against an absurdly wide frame on a very tall
   window. */
.player-frame.vertical,
.player-frame.ar-portrait  {
  height: 100%; min-height: 220px;
  width: auto; max-width: min(100%, calc(92vh * 9 / 16));
  aspect-ratio: 9 / 16;  max-height: none; margin: 0 auto;
}
/* PA3: square and wide frames are WIDTH-bound. With height:100% the box took
   the column's full height and only its width was clamped, so "16:9" drew a
   762x612 box (5:4) at 1440 and "1:1" a 355x425 one on a phone -- the preview
   lied about the shape. Width first, height from the ratio, and a cap so a
   very tall window cannot push the frame past the stage. */
.player-frame.ar-square    {
  height: auto; min-height: 0;
  width: min(100%, 92vh); max-width: 100%;
  aspect-ratio: 1 / 1;   max-height: 100%; margin: 0 auto;
}
.player-frame.ar-landscape {
  height: auto; min-height: 0;
  width: min(100%, calc(92vh * 16 / 9)); max-width: 100%;
  aspect-ratio: 16 / 9;  max-height: 100%; margin: 0 auto;
}

/* SOURCE VIEW (round 12, defect 4). Unpressing Full frame shows the whole
   source, but the stage kept the EXPORT's shape -- so a 16:9 source was drawn
   as a small band letterboxed on all four sides of a 9:16 box, and the gold
   guide you are meant to drag was a ~107x190px target. In this view the stage
   takes the SOURCE's own aspect (written as --source-ar by layoutFrame), so
   the footage fills the picture and the guide is a real thing to grab. The
   moment the finished view comes back, so does the export shape. */
.player-frame.is-source,
.player-frame.ar-portrait.is-source,
.player-frame.ar-square.is-source,
.player-frame.ar-landscape.is-source {
  aspect-ratio: var(--source-ar, 16 / 9);
  height: auto; width: 100%;
  max-width: 100%; max-height: 100%;
}

/* In a short window (the 780x493 deck dock) the clip header's prose ate most
   of the column and squeezed the player to its minimum. The metadata is all
   readable on the clip card; in the editor the picture is what matters, so
   drop the prose and reclaim the height for the frame. */
/* CapCut's player column is picture, near enough end to end. The clip's
   rationale, description, subscores and hashtags were taking 263px off the top
   of it, so they were hidden here with `display: none` and a comment saying
   they were "already on the clip's card". They were not on the card, and
   hiding them was the whole reason the caption and the hashtags could not be
   reached from the app at all. They now live in #postCard at the top of the
   properties panel — out of the picture's height, and on screen. Nothing in
   .stage-head needs hiding any more, because nothing but the title is left in
   it. */
/* Round 10: sp-2 vertical, was sp-3. Every pixel this header takes comes out
   of a picture that is height-bound, and the title is one clamped line. */
.stage-head { padding: var(--sp-2) var(--sp-4); align-items: center; }
.stage-head h1 {
  font-size: 17px; -webkit-line-clamp: 1;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (max-height: 720px) {
  .stage-head { padding: var(--sp-2) var(--sp-3); }
  .stage-head h1 { font-size: 17px; -webkit-line-clamp: 1; }   /* the title outranks the body in EVERY window */
  .player-wrap { padding: var(--sp-2) var(--sp-3); }
  /* Tighten the control rows too, so less of the stage has to be scrolled to
     reach the export button. */
  .trimmer, .audio-row, .caption-colors, .export-row { padding: var(--sp-2) var(--sp-3); }
  /* Vertical budget in a 493px-tall dock: topbar 52 + dock + splitter + stage.
     A 46vh dock left the stage 148px while the frame's own minimum was 260,
     so the picture overflowed its column. Give the dock less and let it
     scroll, and lower the frame's floor so it FITS rather than spilling. */
  /* Clamp the grid TRACK, not just the painted element. max-height on the dock
     limits what it draws, but the row track still reserved the full --dock-h
     (a value the user may have dragged larger on a big screen, and which
     persists), so the stage was starved and the picture spilled out of it. */
  .layout { grid-template-rows: minmax(0, 1fr) 5px min(var(--dock-h, 200px), 38vh); }
  .player-frame.ar-portrait, .player-frame.vertical { min-height: 120px; }
}

/* ── The WYSIWYG overlay ──────────────────────────────────────────────────
   #stageOverlay spans the frame; #cropGuide is positioned onto the exact
   rectangle the export will keep (moving with the face track), and the caption
   layer lives inside it so caption size and position are measured against the
   OUTPUT canvas, exactly like the burn. */
/* Inset by exactly the same gutter as the video, so this box IS the video's box.
   videoContentRect() measures against it, which keeps the crop guide, captions
   and imported media in the picture's coordinate space with no offset. */
.stage-overlay { position: absolute; inset: var(--stage-pad, 0); pointer-events: none; display: none;
  /* Above the cover-up layer (z 3) and the transition holds: the export
     composites captions after the blend, so the preview shows them on top. */
  z-index: 4; }
/* Preview-only emulation of a join's transition: opacity is driven purely by
   playhead time (applyTransitionPreviewAtTime), so pause/seek reset it for
   free. Dip-to-black ramps THIS; every other type dips the <video> itself
   (el.video.style.opacity) instead of this element. Covers the whole frame,
   including captions -- an export-time transition sits over everything too. */
.transition-overlay {
  position: absolute; inset: 0; background: #000; opacity: 0;
  pointer-events: none; z-index: 4;
}
.player-frame.has-media .stage-overlay.is-live { display: block; }

/* Round 6, defect E: the last good frame, held over the video only while a
   programmatic gap-skip / loop-wrap seek is in flight (1-2 frames of removed
   source used to paint at every notch jump). Sits right after the video in
   the DOM so captions and the transition overlay still paint above it. */
.seek-mask {
  position: absolute;
  top: var(--stage-pad); left: var(--stage-pad);
  width: calc(100% - var(--stage-pad) * 2);
  height: calc(100% - var(--stage-pad) * 2);
  object-fit: contain;
  pointer-events: none;
  visibility: hidden;
}
.seek-mask.is-on { visibility: visible; }

/* The crop guide marks WHAT WILL BE KEPT. When it covers the whole picture --
   a 9:16 source going to a 9:16 export, which is most of Quincy's footage --
   there is nothing being cropped away, so a heavy gold rectangle sitting
   exactly on the video's edge just made it impossible to tell where the video
   ended and the UI began ("i can't see all the bordering sides"). It now shows
   corner brackets rather than a full box, and the dark scrim only appears when
   something is actually being cut. */
.crop-guide {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  /* content-box added 1px of border on EACH side, pushing the right edge 1px
     past .player-frame's overflow:hidden -- so the right border was clipped
     away and the box looked open on that side. */
  box-sizing: border-box;
  box-shadow: none;                       /* scrim only while actually cropping */
  border: 1px solid transparent;
  border-radius: 2px;
  transition: box-shadow .18s ease, border-color .18s ease;
  pointer-events: auto;
  touch-action: none;
  cursor: move;
}
/* Corner brackets — the universal language for "this is a crop". They appear
   only when a crop is actually happening, or on hover to advertise that the
   frame can be grabbed. If NOTHING is being cut away, drawing a crop box at all
   is a lie that makes the user ask "is my video going to be cut off?" -- so we
   draw nothing and the answer is self-evident. */
.crop-guide::before, .crop-guide::after {
  content: ''; position: absolute; width: 15px; height: 15px;
  border: 2px solid rgba(245,197,24,.9); pointer-events: none;
  opacity: 0; transition: opacity .15s ease;
}
/* ALWAYS on. Quincy: "i want to always know where the border of the exported
   video will be, and where the actual video size sits in relationship to it."
   Hover-only meant the answer vanished the moment the mouse left, which is
   exactly when you are looking at the picture. Quiet by default, louder when it
   is actually cutting something away. */
.crop-guide::before, .crop-guide::after { opacity: 1; }
.crop-guide { border-color: rgba(245,197,24,.30); }
.player-frame:hover .crop-guide { border-color: rgba(245,197,24,.55); }

/* The caption centre line. Positioned as a PERCENTAGE of .crop-guide, so it is
   always the middle of what actually ships and needs no recalculation when the
   player resizes or the aspect changes -- the browser rescales it for free.
   Only visible while a caption is being dragged: a permanent line through the
   middle of the picture would be in the way of the thing you are framing. */
.centre-line {
  position: absolute; left: 50%; top: 0; bottom: 0;
  width: 0; margin-left: -1px; border-left: 1px dashed rgba(245,197,24,.55);
  opacity: 0; transition: opacity .12s ease;
  pointer-events: none; z-index: 6;
}
.centre-line.is-live { opacity: 1; }
/* Snapped: brighter and solid, so "it is locked to centre" reads at a glance
   rather than needing the number. */
.centre-line.is-snapped {
  border-left-style: solid;
  border-left-color: rgba(245,197,24,.95);
  box-shadow: 0 0 6px rgba(245,197,24,.5);
}

/* The source picture's own edge. Only meaningful when it differs from the export
   frame -- when they coincide, drawing both would just double the line, so JS
   hides it (.is-same). Neutral, not gold: gold means "this ships". */
.source-extent {
  position: absolute; box-sizing: border-box;
  border: 1px dashed rgba(226,232,240,.28);
  border-radius: 2px; pointer-events: none;
  transition: opacity .18s ease;
}
.source-extent.is-same { opacity: 0; }
.crop-guide::before { left: -1px; top: -1px; border-right: 0; border-bottom: 0; }
.crop-guide::after { right: -1px; bottom: -1px; border-left: 0; border-top: 0; }
.crop-guide.is-tracking.is-cropping { border-color: rgba(245,197,24,.85); }
/* Manual mode alone is not a reason to draw the box -- at 1.00x on a matching
   aspect it crops nothing, and a visible frame there just asks "is my video
   about to be cut off?". Only show it when it is cutting, or on hover. */
.crop-guide.is-manual.is-cropping { border-color: rgba(245,197,24,.55); }
.player-frame:hover .crop-guide.is-manual { border-color: rgba(245,197,24,.45); }
.crop-guide.is-manual.is-cropping {
  box-shadow: 0 0 0 2000px rgba(4,5,7,.72), 0 0 22px rgba(245,197,24,.18);
}
.crop-guide.is-dragging { cursor: grabbing; }
.crop-guide.is-tracking::before,
.crop-guide.is-tracking::after {
  content: ''; position: absolute; width: 13px; height: 13px;
  border: 2px solid rgba(245,197,24,.95);
}
.crop-guide.is-tracking::before { left: -1px; top: -1px; border-right: 0; border-bottom: 0; }
.crop-guide.is-tracking::after { right: -1px; bottom: -1px; border-left: 0; border-top: 0; }
/* Something IS being cut away: dim it and firm up the edge so the kept region
   is obvious. This is the only state that earns the heavy treatment. */
.crop-guide.is-cropping {
  box-shadow: 0 0 0 2000px rgba(4,5,7,.55);
  border-color: rgba(245,197,24,.42);
}
.crop-guide.is-full { box-shadow: none; border-color: rgba(245,197,24,.12); }

/* Full frame — WHAT the picture is (the finished clip, or the source you frame
   from). Text rather than a glyph on purpose: the expand toggle next to it is
   the glyph and changes something else entirely, and two lookalike icons doing
   different jobs is how a strip of chrome becomes unreadable. Pressed is the
   resting state, so it wears chrome-silver rather than gold: the accent is
   reserved for the control the user has actively switched ON. */
.stage-fit {
  flex: none; margin-left: auto;
  font-size: 10px; font-weight: 500; line-height: 1; letter-spacing: .02em;
  padding: 5px 9px;
  background: none; border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--text-faint); cursor: pointer; white-space: nowrap;
  transition: color .15s ease, background .15s ease, border-color .15s ease;
}
.stage-fit:hover { color: var(--text); background: rgba(255,255,255,.06); }
.stage-fit[aria-pressed="true"] {
  color: var(--text);
  border-color: rgba(226,232,240,.22);
  background: linear-gradient(180deg, rgba(255,255,255,.09), rgba(255,255,255,.03));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10);
}
.stage-fit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── The framed view ──────────────────────────────────────────────────────
   Full frame pressed: the export's crop is scaled up to fill the player, so
   the picture on screen IS the clip -- no letterbox, no guide box, nothing to
   draw an X over. editor.js writes the transform onto .player-frame as two
   custom properties and every picture layer reads them, so the video and the
   still frames drawn in its place (the seek mask, the two transition holds)
   are framed identically instead of one of them snapping back mid-seek. */
.player-frame video,
.player-frame .hide-layer,
.player-frame .seek-mask,
.player-frame .transition-hold,
.player-frame .transition-incoming {
  transform-origin: var(--frame-origin, 50% 50%);
  transform: var(--frame-transform, none);
  /* The source's baked-in dead margins (editor.js applyDeadCrop). It sits on
     the SAME four layers as the framing transform on purpose: the seek mask
     and the two transition holds are canvases painted from the full decoded
     frame, so a crop applied only to the video would make the bar flash back
     the instant a scrub or a transition swapped one of them in. */
  object-view-box: var(--dead-view-box, initial);
  /* ...and the clip that actually removes it. object-view-box only moves the
     fitted rect; the rest of the frame is still painted into the letterbox
     gutters beside it. editor.js/updateDeadClip writes the fitted rect here in
     px, so the picture is clipped to exactly what object-view-box selected. */
  clip-path: var(--dead-clip, none);
}
/* ...but NOT over a rendered clip. `.vertical` is the export player, and
   render.py already cropped the dead margin out of that file (source_crop ->
   the first crop filter in the chain). Cropping it a second time here would
   eat 10% of a finished clip. The custom properties are written inline on
   .player-frame, so they cannot be overridden by a class on the same element
   -- the two properties are turned off on the picture itself instead. */
.player-frame.vertical video,
.player-frame.vertical .seek-mask,
.player-frame.vertical .transition-hold,
.player-frame.vertical .transition-incoming {
  object-view-box: initial;
  clip-path: none;
}
/* The gutter exists so the gold export border always has room to draw INSIDE
   the visible box. In the framed view there is no border to draw -- the frame's
   own edge is the export's edge -- and the gutter would show scaled-up picture
   in a strip that is not part of the export, so it goes to zero and the clip
   reaches the edge. */
.player-frame.is-framed { --stage-pad: 0px; }
/* Same reasoning as .is-framed, for the other state where the guide is the
   whole frame: no handles are on screen, so the inset is pure black bar. */
.player-frame.guide-full { --stage-pad: 0px; }
/* Nothing to compare the export against here: the source's edge is off screen. */
.player-frame.is-framed .source-extent { display: none; }
/* And the guide has nothing to mark, so it draws nothing and takes no pointer
   input -- a drag across the finished clip has no honest source coordinate to
   map to. Full frame is the way back to the view where framing is done. */
.player-frame.is-framed .crop-guide.is-framed {
  border-color: transparent;
  box-shadow: none;
  pointer-events: none;
  cursor: default;
}
.player-frame.is-framed .crop-guide.is-framed::before,
.player-frame.is-framed .crop-guide.is-framed::after { opacity: 0; }
.player-frame.is-framed .crop-guide.is-framed .frame-handle { display: none; }

/* ── The stage strip (round 10, item 1a) ──────────────────────────────────
   ONE line above the picture carrying the framing readout, the framing
   controls and the expand toggle. Before this the readout was its own centred
   row ABOVE the picture and the framing controls were a second 54px row BELOW
   it: two strips of chrome, ~90px of vertical budget, taken straight out of a
   picture the owner already called too small. Merged, the same content costs
   about a third of that and the difference goes to the video. */
.stage-strip {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; flex: none; min-height: 0;
  /* A last-resort fold, not the plan. The plan is that the finished view shows
     the readout and Full frame and nothing else (renderFrameControls), so this
     line fits a ~580px stage column with room to spare. In the SOURCE view --
     where you ARE framing, and Crop size and Reset-to-auto come back -- a
     narrow column can still run out of line, and a second row there is the
     honest outcome: measured without it, the "Reset to auto" and "Full frame"
     labels OVERLAPPED. */
  flex-wrap: wrap;
}
/* The readout sits left; the framing controls take the middle and only exist
   in manual mode; the expand button is pinned right. */
/* `flex: 1 1 0`, not `1 1 auto`. Flexbox wraps BEFORE it shrinks, so with an
   auto basis the row's full content width decided whether the strip broke to a
   second line -- measured at 1366x768 the finished view's single status
   sentence pushed the strip to 71px (three rows) even though the sentence
   ellipsizes happily at any width. A zero basis lets it take the leftover and
   ellipsize, so the strip stays one 22px line wherever the sentence fits at
   all, and only the SOURCE view's un-shrinkable Crop size + Reset can force
   the fold. */
.stage-strip .frame-row { flex: 1 1 0; min-width: 0; }
/* ...except when the framing TOOLS are on the strip, which is only the source
   view. Crop size and Reset cannot shrink, so with a zero basis they overflowed
   their line and drew on top of Full frame -- measured at 1366x768. An auto
   basis there lets the strip do what flexbox does first, which is wrap: the row
   takes a line of its own and nothing collides. The finished view keeps the
   zero basis, and its one 22px line. */
.stage-strip:has(#frameZoomWrap:not([hidden])) .frame-row { flex: 1 1 auto; }
.stage-strip .frame-row[hidden] { display: none; }
.layout.view-board .stage-strip { display: none; }

/* Framing readout: above the picture, never on it. */
.frame-readout {
  flex: none;
  font: 10px/1 var(--mono); letter-spacing: .04em;
  color: rgba(245,197,24,.75);
  padding: 4px 8px; border-radius: 99px;
  background: rgba(245,197,24,.07);
  border: 1px solid rgba(245,197,24,.16);
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.layout.view-board .frame-readout { display: none; }

/* What is on RIGHT NOW at the playhead, now that the SPEED and PUNCH-IN lanes
   are gone: "1.5×", "Punch", or both. Same chip idiom as the framing readout
   it sits beside, deliberately in silver rather than gold -- the app has one
   accent and it means "this is doing something you pressed", not "here is a
   fact about the frame". Absent, not empty, when nothing is on. */
.effect-readout {
  flex: none;
  font: 10px/1 var(--mono); letter-spacing: .04em;
  color: var(--text-dim);
  padding: 4px 8px; border-radius: 99px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  white-space: nowrap;
}
.layout.view-board .effect-readout { display: none; }

/* A control that cannot do anything on THIS video says why, in the same words
   a person would use, rather than being greyed out and left to be guessed at.
   (app/render.py drops a punch-in entirely when the source is narrower than
   the export shape.) */
.panel-group.is-unavailable button { opacity: .45; cursor: not-allowed; }
.panel-note {
  margin-top: var(--sp-2);
  font-size: 10px; line-height: 1.5; color: var(--text-faint);
}

/* The expand toggle. Quiet chrome-silver glyph, gold only once it is ON --
   the accent means "this is doing something", never decoration. */
.stage-expand {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 22px; padding: 0;
  background: none; border: 1px solid transparent; border-radius: var(--r-sm);
  color: var(--text-faint); cursor: pointer;
  transition: color .15s ease, background .15s ease, border-color .15s ease;
}
.stage-expand:hover { color: var(--text); background: rgba(255,255,255,.06); }
.stage-expand[aria-pressed="true"] {
  color: var(--accent);
  border-color: rgba(245,197,24,.35);
  background: rgba(245,197,24,.08);
}
.stage-expand:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Expanded ("theatre") stage ───────────────────────────────────────────
   Everything except the picture and its one strip steps out of the way, so a
   9:16 frame gets the whole window instead of the leftover under a header and
   over a timeline. Esc, a second press of the toggle, or a second double-click
   on the picture puts it all back. Nothing is destroyed -- these are display
   rules over the same DOM, so the panel/dock keep their scroll and state. */
.layout.stage-expanded { grid-template-columns: minmax(0, 1fr); }
.layout.stage-expanded .rail,
.layout.stage-expanded .editor,
.layout.stage-expanded .splitter,
.layout.stage-expanded .timeline-dock,
.layout.stage-expanded .stage-head { display: none; }
.layout.stage-expanded .stage { grid-row: 1 / -1; grid-column: 1; border-right: 0; }
body:has(.layout.stage-expanded) .panel-reveal { display: none; }
/* No vh ceiling in here: the column IS the budget. */
.layout.stage-expanded .player-frame.ar-portrait,
.layout.stage-expanded .player-frame.vertical { max-width: min(100%, calc(99vh * 9 / 16)); }
.layout.stage-expanded .player-frame.ar-square  { max-width: min(100%, 99vh); }
.layout.stage-expanded .player-frame.ar-landscape { max-width: min(100%, calc(99vh * 16 / 9)); }

/* Round 10, item 1b: the corners are LIVE in AUTO framing too.
   `pointer-events: none` until `.is-manual` meant elementFromPoint at a corner
   returned the VIDEO, so the press fell through to a MOVE -- which is a no-op
   at zoom 1.0 and says nothing. The arrows on the owner's screenshot were him
   dragging exactly those dead corners. beginFrameDrag already takes manual
   control BEFORE it looks for a handle, so a corner grab in auto now enters
   manual and starts the resize in ONE gesture, no mode switch first.
   They stay invisible until the picture is hovered so nothing new is drawn on
   a resting frame. */
.frame-handle {
  position: absolute; width: 15px; height: 15px; z-index: 8;
  border: 2px solid var(--accent); background: rgba(10,11,13,.94);
  border-radius: 3px; opacity: 0; pointer-events: auto;
  transition: opacity .12s ease, transform .12s ease;
}
.player-frame:hover .frame-handle { opacity: .55; }
.crop-guide.is-manual .frame-handle { opacity: 1; }
.frame-handle:hover { transform: scale(1.12); }
.frame-handle.tl { left: -8px; top: -8px; cursor: nwse-resize; }
.frame-handle.tr { right: -8px; top: -8px; cursor: nesw-resize; }
.frame-handle.bl { left: -8px; bottom: -8px; cursor: nesw-resize; }
.frame-handle.br { right: -8px; bottom: -8px; cursor: nwse-resize; }

/* Caption overlay. Every metric is a CSS variable written by editor.js from the
   server's CaptionStyle, so a preset edit in Python reaches the preview too. */
.cap-layer {
  position: absolute; inset: 0; overflow: visible;
  font-family: var(--cap-font, 'Arial Black', sans-serif);
  font-weight: 900;
  --cap-size: 24px;
}
.cap-block {
  position: absolute;
  width: max-content;
  padding: 0;
  pointer-events: auto; cursor: grab;
  outline: 1px dashed transparent; outline-offset: 6px;
  transition: outline-color .12s ease, background-color .12s ease;
  display: flex; flex-wrap: wrap; justify-content: center; align-content: center;
  gap: var(--cap-gap, .16em) var(--cap-space, .3em);
  font-size: var(--cap-size);
  line-height: var(--cap-line, 1.2);
  text-transform: var(--cap-case, uppercase);
  text-align: center;
}
.cap-w {
  color: var(--cap-idle, #fff);
  -webkit-text-stroke: var(--cap-stroke, 0px) var(--cap-stroke-color, #000);
  paint-order: stroke fill;
  text-shadow: var(--cap-shadow, none);
  white-space: pre;
}
/* PA1: the spoken word pops in -- lands bigger and settles, like the burn. */
@keyframes cap-pop {
  from { transform: translateZ(0) scale(calc(var(--cap-active-scale, 1) * var(--cap-pop, 1))); }
  to   { transform: translateZ(0) scale(var(--cap-active-scale, 1)); }
}
.cap-w.is-active.is-entering { animation: cap-pop 140ms ease-out both; }
.cap-emoji { display: inline-block; margin-left: .25em; -webkit-text-stroke: 0; text-shadow: none; }
.cap-w.is-active {
  color: var(--cap-active, var(--accent));
  /* Mirrors CaptionStyle.active_scale. The renderer draws the bigger glyph
     centred on the base glyph's slot so the line never reflows; inline-block +
     a centred transform is the CSS equivalent -- scale() does not affect
     layout, so neighbouring words stay put here too. */
  display: inline-block;
  transform: scale(var(--cap-active-scale, 1));
}
/* box_idle=false plates ONLY the active word: a highlight bar that travels
   along the line rather than a wall of plates. */
.cap-layer.mode-box.box-active-only .cap-w { background: transparent; }
/* Direct manipulation: the caption is a real object you can grab and scale.
   The affordance only appears on hover so it never pollutes a clean preview. */
.cap-block:hover, .cap-block.is-dragging {
  outline-color: rgba(245,197,24,.75);
  background: rgba(245,197,24,.05);
}
.cap-block.is-dragging { cursor: grabbing; }
.cap-block.is-placed { padding: 0; }
/* Forced line break inside the flex-wrapped word row: takes a whole line and
   no height, so the words after it start a new line at the balanced split. */
.cap-br { flex: 0 0 100%; width: 100%; height: 0; }
.cap-handle {
  position: absolute; width: 11px; height: 11px;
  border: 2px solid rgba(245,197,24,.95);
  background: rgba(10,11,13,.85);
  border-radius: 2px;
  opacity: 0; transition: opacity .12s ease;
  pointer-events: auto; cursor: nwse-resize;
}
.cap-block:hover .cap-handle, .cap-block.is-dragging .cap-handle { opacity: 1; }
.cap-handle.tl { left: -12px; top: -12px; }
.cap-handle.tr { right: -12px; top: -12px; cursor: nesw-resize; }
.cap-handle.bl { left: -12px; bottom: -12px; cursor: nesw-resize; }
.cap-handle.br { right: -12px; bottom: -12px; }

/* Plate styles ("hormozi"): every word gets a rounded box, the active one gold. */
.cap-layer.mode-box .cap-w {
  background: var(--cap-box, rgba(17,18,21,.92));
  padding: var(--cap-box-pad-y, .16em) var(--cap-box-pad-x, .3em);
  border-radius: var(--cap-box-radius, .22em);
}
.cap-layer.mode-box .cap-w.is-active {
  background: var(--cap-box-active, #f5c518);
  color: var(--cap-box-active-text, #111);
}

.player-empty {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2);
  color: var(--text-faint); font-size: 12px; text-align: center; padding: var(--sp-4);
}
.player-frame.has-media .player-empty { display: none; }

/* Readouts sit in a strip under the video so the frame gets the full column. */
.player-side {
  flex-wrap: wrap; row-gap: var(--sp-1);   /* never clip a readout; wrap instead */
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-1) var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
/* Label BESIDE the value, not above it. Stacked, the row is ~32px tall and the
   label is the half that gets clipped first when the timeline dock is dragged
   short -- which leaves four bare numbers with nothing saying what they are.
   Inline, the row is ~16px, so it survives any dock height, and it reads as a
   sentence: "Position 0:17.50". */
.readout { display: flex; align-items: baseline; gap: var(--sp-2); white-space: nowrap; }
.readout-k { font-size: 10px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-faint); }
.readout-v { font: 12px/1 var(--mono); color: var(--text); font-variant-numeric: tabular-nums; }
.player-side .btn { margin-left: auto; width: auto; flex: none; }
.player-side .switch-block { width: auto; flex: none; }

.frame-row {
  /* Compact on purpose: it only appears during manual framing, and every
     pixel it takes comes straight out of the picture's height (defect #7). */
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-1) var(--sp-3); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-sm);
}
.frame-copy { min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); flex: 1; }
.frame-copy b { font-size: 10px; letter-spacing: .04em; text-transform: uppercase; color: var(--text); }
.frame-copy span { font-size: 10px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Wraps and shrinks, for the same reason .frame-row does: in the source view of
   a ~360px stage column this control is the widest un-shrinkable thing on the
   strip, and at 1366x768 it drew ON TOP of Full frame. Measured. */
.frame-zoom {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: 10px; color: var(--text-dim);
  min-width: 0; flex-wrap: wrap;
}
.frame-zoom input { width: 112px; min-width: 48px; flex: 1 1 48px; accent-color: var(--accent); }
.frame-zoom b { width: 38px; font: 10px/1 var(--mono); color: var(--accent); text-align: right; }

.caption-colors {
  display: flex; align-items: center; gap: var(--sp-3); min-height: 42px;
  padding: var(--sp-2) var(--sp-4); border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line); background: rgba(255,255,255,.012);
}
/* Round 22, minor: this was `.caption-colors-title`, and by the time the
   critic found it the Speed heading, the Transition heading, every pill
   group's heading and "Imported media" were all wearing a class named after
   the caption colour pickers. The class was never about captions -- it is the
   small uppercase title of a group inside the properties panel -- so it is
   named that now. No visual change: same declarations, same elements. */
.panel-group-title { margin-right: var(--sp-1); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.caption-colors label { display: flex; align-items: center; gap: var(--sp-1); font-size: 10px; color: var(--text-dim); }
.caption-colors input[type="color"] { width: 24px; height: 24px; padding: 0; border: 1px solid var(--line-strong); border-radius: 5px; background: transparent; cursor: pointer; }
.caption-colors input[type="color"]::-webkit-color-swatch-wrapper { padding: var(--sp-1); }
.caption-colors input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 3px; }

/* Plain-text switches. Sits directly under the palette because it belongs to
   the same question -- what the words look like -- and turning both off is the
   route to plain text. */
.ov-fade {
  background: var(--surface-2); color: var(--text); border: 1px solid var(--line-strong);
  border-radius: 5px; font: 10px/1 var(--mono); padding: var(--sp-1);
}

.caption-plain {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-4); border-bottom: 1px solid var(--line);
}

/* ── Trimmer ─────────────────────────────────────────────────────────────── */
.trimmer { padding: 0 var(--sp-4) var(--sp-2); }
.trimmer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.trimmer-tools { display: flex; align-items: center; gap: var(--sp-1); }
.trimmer-tools h3 { margin-right: var(--sp-2); }
.trimmer-tools .btn-sm { min-width: 25px; padding: 0 var(--sp-2); font-variant-numeric: tabular-nums; }
.trimmer-tools-sep { width: 1px; height: 16px; background: var(--line-strong); margin: 0 var(--sp-1); }
/* The ? that replaced the permanent instruction sentence: a fixed-size icon
   button, never grows no matter how long its title/tooltip text gets. */
.trimmer-hint-btn {
  flex: none; width: 25px; min-width: 25px; padding: 0;
  border-radius: 50%; font-weight: 700; color: var(--text-faint);
}

.track {
  position: relative; height: 56px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
  touch-action: none;
  cursor: pointer;
}

/* Actual video frames, tiled edge to edge across the current view window.
   Sits after .density in the DOM (so it paints on top of the speech-density
   band, letting it show through faintly as texture rather than deleting it)
   but before every interactive control below, so with z-index:auto on all of
   them DOM order alone keeps the frames underneath and every control stays
   clickable. Painted with a solid backdrop colour so a slow/failed fetch
   never leaves a see-through gap. */
.filmstrip {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: fill; display: block;
  background: #0a0b0d;
  opacity: .85;
}

/* Compressed (content-adjacent) axis active: the base strip's pixels are laid
   out in source time and stop lining up past the first notch -- the piece
   blocks window the same image correctly, so they carry the frames alone. */
.track.has-cuts .filmstrip { visibility: hidden; }

.track-dim { position: absolute; top: 0; bottom: 0; background: rgba(6,7,9,.72); pointer-events: none; }
.track-dim-left  { left: 0;  width: 0; }
.track-dim-right { right: 0; width: 0; }

.track-sel {
  position: absolute; top: 0; bottom: 0;
  border-left: 1px solid var(--accent); border-right: 1px solid var(--accent);
  background: rgba(245,197,24,.07);
  pointer-events: none;
}
/* The playhead is the thing you reach for most, so it is a real object:
   white/silver, grabbable, with a head you can put a cursor on. It used to be
   a 1px white hairline with pointer-events:none -- visible if you looked for
   it, impossible to grab. Gold is reserved for the trim/accent language (the
   in/out handles below): a gold-on-gold playhead sitting inside the in-handle
   was unreadable, so the playhead is silver metal instead -- still its own
   distinct object, never confusable with the selection wash. */
.playhead {
  position: absolute; top: 0; bottom: 0; width: 2px; left: 0;
  background: var(--text); box-shadow: 0 0 8px rgba(231,233,236,.5);
  pointer-events: none; z-index: 6;
}
.playhead::before {
  content: ''; position: absolute; left: 50%; top: -1px;
  width: 13px; height: 13px; transform: translateX(-50%);
  background: linear-gradient(180deg, #fbfcfd 0%, #e7e9ec 45%, #b7bcc4 100%);
  border-radius: 3px;
  box-shadow: 0 1px 4px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.7);
}
/* The grab strip spans the whole timeline height but is invisible: it exists so
   the 2px line has a 14px-wide target, which is the difference between a
   scrubber that feels precise and one that feels broken. It sits BELOW the
   trim handles in stacking order (z-index 7 vs .handle's 8, set alongside
   .handle below) and is pointer-events:none in the span it shares with a
   handle's own hit box -- see onTrackPointerDown, which now checks the
   handle's hit-test first so the handle always wins the grab there. */
.playhead-grab {
  /* 18px, was 14: the owner's Opus reference is "a visible line I can grab
     and move to any point" -- the grab has to be easy to land on. */
  position: absolute; top: 0; bottom: 0; width: 18px; margin-left: -9px;
  cursor: ew-resize; z-index: 7; touch-action: none;
}
.track.is-scrubbing { cursor: ew-resize; }

/* A toolbar toggle has to show its state without being read. Gold when on. */
.btn.is-on { color: var(--accent); border-color: rgba(245,197,24,.45); }
/* Engaged-but-reversible, so silver rather than alarm red (round 6). */
.btn.is-muted { color: var(--text); border-color: rgba(201,209,220,.45); }

/* The line a drag has snapped to, shown only while it is snapping -- feedback
   at the moment of the decision, gone the instant it stops mattering. */
.snap-line {
  position: absolute; top: 0; bottom: 0; width: 1px; z-index: 8;
  background: rgba(125,211,252,.95);
  box-shadow: 0 0 6px rgba(125,211,252,.6);
  pointer-events: none;
}

.handle {
  position: absolute; top: 0; bottom: 0; width: 15px;
  transform: translateX(-50%);
  background: none; border: 0; padding: 0; cursor: ew-resize;
  display: flex; align-items: center; justify-content: center;
  touch-action: none;
  z-index: 8;
}
.handle span {
  width: 5px; height: 24px; border-radius: 3px;
  background: linear-gradient(180deg, #fae584 0%, #f5c518 55%, #c69a0c 100%);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 0 12px var(--accent-glow), inset 1px 0 0 rgba(255,255,255,.4);
  transition: height .12s ease, background .12s ease;
}
.handle:hover span, .handle.dragging span {
  height: 34px;
  background: linear-gradient(180deg, #fceca3 0%, #ffd42e 55%, #d4a70f 100%);
}
.handle:focus-visible span { outline: 2px solid var(--accent); outline-offset: 2px; }

.track-scale {
  position: relative; height: 20px;
  margin-top: var(--sp-1); font: 10px/1 var(--mono); color: var(--text-faint);
}
.track-scale span { position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap; }

/* Drag-and-drop from the desktop onto the timeline: a clear indicator of
   where the file will land, matching the exact x the drop handler reads. */
.track.is-drop-target { outline: 2px solid var(--accent); outline-offset: -2px; }
.drop-indicator {
  position: absolute; top: 0; bottom: 0; width: 2px;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
  pointer-events: none; z-index: 6;
}
.player-frame.is-drop-target { outline: 2px solid var(--accent); outline-offset: -3px; }

/* Overlay lane: one bar per imported overlay, in the same coordinate space
   as the trim handles above it. */
/* The clip's own audio, in its own lane -- "i dont want the audio on the video
   file, i want it below in its own lane". Painted over the filmstrip it was
   decoration; down here it is a thing you can work on. */
.audio-lane {
  position: relative; height: 40px; margin-top: var(--sp-1);
  background: #0d0f13; border: 1px solid var(--line); border-radius: var(--r-sm);
  overflow: hidden;
}
.audio-wave { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.audio-regions { position: absolute; inset: 0; }
/* Both lane labels sit ON the waveform. That was survivable while the wave
   was never actually drawn (it was a 1px canvas stretched to a flat grey
   slab); the moment it draws, dim text lands on bright silver bars and
   disappears. They get the smallest thing that restores contrast without
   adding a second visual language: the lane's own background, as a chip. */
.audio-lane-tag {
  position: absolute; left: 6px; top: 4px; z-index: 3;
  font: 10px/1 var(--mono); letter-spacing: .1em;
  color: var(--text-faint); pointer-events: none;
  padding: 2px 4px; border-radius: 3px; background: rgba(13, 15, 19, .78);
}
/* A muted stretch has to look silenced, not just unlabelled. */
.audio-region {
  position: absolute; top: 0; bottom: 0; cursor: pointer;
  border-left: 1px solid rgba(255,255,255,.10);
  border-right: 1px solid rgba(255,255,255,.10);
}
/* Muted = silenced, not destroyed: dimmed silver hatching (round 6 -- red is
   reserved for destructive/error, and a mute is fully reversible). */
.audio-region.is-muted {
  background: repeating-linear-gradient(135deg,
    rgba(8,9,12,.88) 0 5px, rgba(154,161,171,.30) 5px 10px);
}
.audio-region.is-muted::after {
  content: 'MUTED'; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font: 10px/1 var(--mono); letter-spacing: .1em; color: var(--text-dim);
}
.audio-region.is-faded { background: rgba(231,233,236,.10); }
.audio-region-tag {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font: 10px/1 var(--mono); letter-spacing: .06em; color: var(--text);
  white-space: nowrap; pointer-events: none;
  padding: 2px 5px; border-radius: 3px; background: rgba(13, 15, 19, .82);
}
.audio-region.is-selected { box-shadow: inset 0 0 0 1px var(--accent); }

/* The grab handle on an audio cut. Before this the regions rendered, selected
   and right-clicked but could not be moved at all -- there was no handle and
   no drag code, so "I can't slide anything on 1 or 2" was exactly right.
   The visible bar is thin; the HIT AREA is deliberately much wider than it
   looks, because a 2px target on a 46px lane is a target you miss. */
.audio-edge {
  position: absolute; top: 0; bottom: 0; z-index: 6;
  width: 12px;
  cursor: ew-resize; touch-action: none;
  background: transparent;
}
/* Each grip sits just INSIDE its own piece, so the two that meet at a shared
   boundary sit side by side rather than on top of each other -- otherwise only
   one could ever be hit and a piece could never be shrunk from both ends. */
.audio-edge--left { margin-left: 0; }
.audio-edge--right { margin-left: -12px; }
.audio-edge::before {
  content: ''; position: absolute; top: 3px; bottom: 3px;
  width: 2px; border-radius: 1px;
  background: rgba(226,232,240,.55);
}
.audio-edge--left::before { left: 1px; }
.audio-edge--right::before { right: 1px; }
.audio-edge:hover::before,
.audio-edge.dragging::before {
  background: var(--accent);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 0 8px rgba(245,197,24,.5);
  top: 0; bottom: 0; width: 3px;
}
/* ── Fade handles, ON the wave ─────────────────────────────────────────────
   "we need actual fade in audio handles that i can change how long the fade in
   or out is, not a set time". The RAMP is the fade drawn as the wedge of sound
   it takes away, curved by the region's own fade shape; the GRIP sits at the
   end of that wedge and is what you pull. Silver, not gold: gold on this
   timeline is the playhead and the trim handles, and a third gold thing on the
   same 40px lane would read as one of those. */
.audio-fade-ramp {
  position: absolute; top: 0; bottom: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 2;
}
.audio-fade-ramp polygon { fill: rgba(8,9,12,.72); }
/* The grip lives in the lane's top strip so it never fights the boundary grip
   (.audio-edge), which owns the full height of the same few pixels. */
.audio-fade-grip {
  position: absolute; top: 0; height: 16px; width: 14px; margin-left: -7px;
  z-index: 7; cursor: ew-resize; touch-action: none;
  opacity: 0; transition: opacity .12s ease;
}
.audio-fade-grip::before {
  content: ''; position: absolute; left: 5px; top: 2px;
  width: 4px; height: 12px; border-radius: 2px;
  background: linear-gradient(180deg, #f2f4f7, #9aa1ab);
  box-shadow: 0 0 0 1px rgba(0,0,0,.6);
}
/* Quiet until you mean it: visible on hover, on the selected region, on focus,
   and whenever the fade is actually set. */
.audio-region:hover .audio-fade-grip,
.audio-region.is-selected .audio-fade-grip,
.audio-fade-grip.is-set,
.audio-fade-grip:focus,
.audio-fade-grip.dragging { opacity: 1; }
.audio-fade-grip:focus { outline: none; }
.audio-fade-grip:focus::before,
.audio-fade-grip:hover::before,
.audio-fade-grip.dragging::before {
  background: var(--accent);
  box-shadow: 0 0 0 1px rgba(0,0,0,.6), 0 0 8px rgba(245,197,24,.5);
}
/* The live seconds under the finger -- the speed track's readout, on the lane. */
.audio-region-tag.is-live { color: var(--accent); }

/* The Piece panel's two fade sliders. Not a second control with its own
   choices: the same number the grip on the wave holds, on a 0 -> 10s scale.
   Built to the speed track's geometry (4px rail, 28px hit area, silver
   everywhere, the single gold on the live value) because it IS the same
   control on a different value. */
.fade-slider {
  display: flex; flex-direction: column; gap: var(--sp-1);
  flex: 1 1 100%; min-width: 0;
}
.fade-slider-head { display: flex; align-items: baseline; gap: var(--sp-2); }
.fade-slider-label { font: 12px/1.4 var(--font); color: var(--text-dim); }
.fade-slider-val {
  margin-left: auto; font: 600 12px/1 var(--mono); color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.fade-slider.is-set .fade-slider-val { color: var(--text); }
/* The one gold: the number while it is moving under the finger. */
.fade-slider.is-live .fade-slider-val { color: var(--accent); }
.fade-slider-reset { height: 22px; padding: 0 9px; font-size: 10px; }
.fade-slider-reset:disabled { opacity: .35; cursor: default; }

.fade-slider-track {
  position: relative; height: 28px; width: 100%;
  cursor: ew-resize; touch-action: none; border-radius: var(--r-sm);
}
.fade-slider-track:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fade-slider-rail {
  position: absolute; left: 0; right: 0; top: 50%; height: 4px; margin-top: -2px;
  background: #0d0f13; border: 1px solid var(--line-strong); border-radius: 3px;
}
/* The stretch this region is too short to reach -- hatched off, so a knob that
   stops early has a visible reason. Only drawn when there is one. */
.fade-slider-cap {
  position: absolute; top: 50%; height: 4px; margin-top: -2px;
  border-radius: 3px; pointer-events: none; opacity: 0;
  background: repeating-linear-gradient(
    -45deg, rgba(231,233,236,.16) 0 2px, transparent 2px 5px);
}
.fade-slider-track.is-capped .fade-slider-cap { opacity: 1; }
.fade-slider-fill {
  position: absolute; left: 0; top: 50%; height: 4px; margin-top: -2px;
  background: rgba(231,233,236,.28); border-radius: 3px; pointer-events: none;
}
.fade-slider-knob {
  position: absolute; top: 50%; left: 0; width: 14px; height: 14px;
  margin: -7px 0 0 -7px; border-radius: 50%;
  background: linear-gradient(160deg, #ffffff 0%, #c7ccd3 42%, #7d838c 100%);
  box-shadow: 0 1px 2px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.85);
  cursor: grab;
}
.fade-slider-track:active .fade-slider-knob { cursor: grabbing; }

/* Punch-in zoom lane: drag to mark a stretch, right-click for how far/how it
   eases. Gold, not the accent blue -- reads as "an effect", not a cut or a
   mute, at a glance. */
.zoom-lane {
  position: relative; height: 22px; margin-top: var(--sp-1);
  background: #0d0f13; border: 1px solid var(--line); border-radius: var(--r-sm);
  overflow: hidden; cursor: crosshair; touch-action: none;
}
.zoom-regions { position: absolute; inset: 0; }
.zoom-lane-tag {
  position: absolute; left: 6px; top: 4px; z-index: 3;
  font: 10px/1 var(--mono); letter-spacing: .1em;
  color: var(--text-faint); pointer-events: none;
}
.zoom-region {
  position: absolute; top: 0; bottom: 0; cursor: pointer;
  background: var(--accent-glow);
  border-left: 1px solid rgba(245,197,24,.55);
  border-right: 1px solid rgba(245,197,24,.55);
  display: flex; align-items: center; justify-content: center;
  font: 10px/1 var(--mono); color: var(--accent); letter-spacing: .04em;
  overflow: hidden;
}
/* text-overflow is a no-op on a FLEX container's own text node -- that is
   what let a narrow region clip mid-word ("nch · smoo") with no ellipsis at
   all. A block-level child does truncate; min-width:0 lets it actually
   shrink below its content size inside the flex row. */
.zoom-region .region-label, .speed-region .region-label {
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  min-width: 0; max-width: 100%;
}
.zoom-region.is-selected { box-shadow: inset 0 0 0 1px var(--accent); background: rgba(245,197,24,.34); }
.zoom-region.is-dragging { background: rgba(245,197,24,.30); border-style: dashed; pointer-events: none; }

/* Speed lane: slow a moment down, speed dead air up, without an export to
   CapCut. Silver, not gold -- it reads as "timing", not the zoom effect
   above it, at a glance. Slow vs fast is told apart by TONE and hatching
   (and the rate label, already on every region), not by hue -- a foreign
   blue/orange pair here fought the black+chrome+gold palette everywhere
   else. */
.speed-lane {
  position: relative; height: 22px; margin-top: var(--sp-1);
  background: #0d0f13; border: 1px solid var(--line); border-radius: var(--r-sm);
  overflow: hidden; cursor: crosshair; touch-action: none;
}
.speed-regions { position: absolute; inset: 0; }
.speed-lane-tag {
  position: absolute; left: 6px; top: 4px; z-index: 3;
  font: 10px/1 var(--mono); letter-spacing: .1em;
  color: var(--text-faint); pointer-events: none;
}
.speed-region {
  position: absolute; top: 0; bottom: 0; cursor: pointer;
  background: rgba(231,233,236,.14);
  border-left: 1px solid rgba(231,233,236,.45);
  border-right: 1px solid rgba(231,233,236,.45);
  display: flex; align-items: center; justify-content: center;
  font: 10px/1 var(--mono); color: var(--text-dim); letter-spacing: .04em;
  overflow: hidden;
}
.speed-region.is-selected { box-shadow: inset 0 0 0 1px var(--text); background: rgba(231,233,236,.28); }
.speed-region.is-dragging { background: rgba(231,233,236,.24); border-style: dashed; pointer-events: none; }
/* Slow regions stay a flat silver wash; fast regions get a fine diagonal
   hatch on the same silver -- denser texture reads as "sped up" the way a
   blurred filmstrip does, no colour required. */
.speed-region[data-rate-below-1] { background: rgba(231,233,236,.14); border-color: rgba(231,233,236,.45); color: var(--text-dim); }
.speed-region[data-rate-above-1] {
  background: repeating-linear-gradient(135deg, rgba(231,233,236,.10) 0 4px, rgba(231,233,236,.24) 4px 8px);
  border-color: rgba(231,233,236,.6); color: var(--text);
}
.speed-region[data-rate-below-1].is-selected { background: rgba(231,233,236,.30); }
.speed-region[data-rate-above-1].is-selected {
  background: repeating-linear-gradient(135deg, rgba(231,233,236,.16) 0 4px, rgba(231,233,236,.34) 4px 8px);
}

.ov-lane {
  position: relative; height: 22px; margin-top: var(--sp-1);
  background: #0d0f13; border: 1px solid var(--line); border-radius: var(--r-sm);
  overflow: hidden;
}
.ov-regions { position: absolute; inset: 0; }
.ov-lane-tag {
  position: absolute; left: 6px; top: 4px; z-index: 3;
  font: 10px/1 var(--mono); letter-spacing: .1em;
  color: var(--text-faint); pointer-events: none;
}

/* ── An empty lane is the cheapest lesson in the app ──────────────────────
   The punch-in and media lanes used to be `hidden` until the effect already
   existed, so the only way to find out punch-in exists at all was to
   right-click a piece and read the menu. All four lanes are now always drawn:
   an empty one keeps its NAME and nothing else, shrinks to a hairline row so
   it costs the picture almost nothing, and dims to the point where it reads
   as a place a thing could go rather than a thing that is there. No
   instructional copy -- the name is the whole label. */
.audio-lane.is-empty, .zoom-lane.is-empty, .speed-lane.is-empty, .ov-lane.is-empty {
  height: 16px;
  background: #0b0d10;
  border-color: rgba(255,255,255,.045);
}
.audio-lane.is-empty .audio-lane-tag,
.zoom-lane.is-empty .zoom-lane-tag,
.speed-lane.is-empty .speed-lane-tag,
.ov-lane.is-empty .ov-lane-tag {
  top: 3px; color: rgba(101,108,119,.72);
}
/* Hovering an empty effect lane says it is live without a word of copy. */
.zoom-lane.is-empty:hover, .speed-lane.is-empty:hover {
  border-color: var(--line-strong);
}
.zoom-lane.is-empty:hover .zoom-lane-tag,
.speed-lane.is-empty:hover .speed-lane-tag { color: var(--text-dim); }
/* ...and says WHAT it is live for. Round 13, defect 4: a plain click on the
   empty SPEED lane created a 1.2s 2x region (measured: {start:21, end:22.2,
   rate:2}) with nothing on screen having offered, and the created region's
   only affordance was a `title`. The gesture is right -- it is the gesture
   Opus and CapCut use -- so what was missing was the offer. Drawn on HOVER
   only, so no instructional copy is ever permanently on screen, and only on
   an EMPTY lane, so it never covers a region. The text lives in the markup
   (data-hint) rather than here, next to the lane it describes. */
.zoom-lane.is-empty:hover::after,
.speed-lane.is-empty:hover::after {
  content: attr(data-hint);
  position: absolute; right: 7px; top: 3px; z-index: 3;
  font: 10px/1 var(--mono); letter-spacing: .1em;
  color: var(--text-dim); pointer-events: none; white-space: nowrap;
}
.ov-bar {
  position: absolute; top: 0; height: 100%;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  display: flex; align-items: center; padding: 0 var(--sp-2);
  font: 10px/1 var(--mono); color: var(--text-dim);
  overflow: hidden; white-space: nowrap;
  cursor: grab; touch-action: none;
}
.ov-bar.is-dragging { cursor: grabbing; }
/* Sound reads differently from picture at a glance -- the ♪ prefix on its
   label (above) carries that now, so the bar itself stays in the same
   silver/charcoal family as everything else instead of a foreign cyan. */
.ov-bar.is-audio {
  background: rgba(231,233,236,.08); border-color: rgba(231,233,236,.30); color: var(--text-dim);
}
.ov-bar.is-audio.is-selected {
  border-color: var(--text); background: rgba(231,233,236,.18); color: var(--text);
}
/* N4a: the MUSIC lane. A song on its own lane reads as a song: a taller bar
   than a sticker's, a faint wave texture, gold when it is the thing you are
   editing. Only when everything on the lane is sound (is-music). */
.ov-lane.is-music { height: 26px; }
.ov-lane.is-music .ov-bar.is-audio {
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,.10) 0 1px, transparent 1px 5px),
    linear-gradient(180deg, rgba(231,233,236,.16), rgba(231,233,236,.06));
  border-color: rgba(231,233,236,.38); color: var(--text);
  font-weight: 600;
}
.ov-lane.is-music .ov-bar.is-audio.is-selected {
  border-color: var(--accent); color: var(--accent);
  background:
    repeating-linear-gradient(90deg, rgba(245,197,24,.18) 0 1px, transparent 1px 5px),
    linear-gradient(180deg, rgba(245,197,24,.22), rgba(245,197,24,.08));
  box-shadow: 0 0 0 1px rgba(245,197,24,.25);
}
.ov-lane.is-music .ov-bar-edge { width: 8px; }
.ov-lane.is-music .ov-bar-edge::after {
  content: ''; position: absolute; top: 5px; bottom: 5px; left: 3px; width: 2px;
  background: currentColor; opacity: .55; border-radius: 1px;
}
/* The Music button lives on the editor's transport line. In the More tools
   dock the same toolbar sits in a 319px column that clips past ~340px, and
   that view has the Media tab for sound, so the button only shows in focus. */
.timeline-dock #musicAddBtn { display: none; }
.music-tools .piece-tools-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.music-tools-hint { color: var(--text-faint); }
.music-slider input[type=range] { width: 100%; accent-color: var(--accent); cursor: pointer; }
.music-slider .fade-slider-head b { margin-left: auto; font: 600 12px/1 var(--mono); color: var(--text); font-variant-numeric: tabular-nums; }
.music-remove { align-self: flex-start; color: var(--text-dim); }
.music-remove:hover { color: #ff7b7b; }
.ov-bar.is-selected { border-color: var(--accent); background: rgba(245,197,24,.14); color: var(--text); }
.ov-bar-label { overflow: hidden; text-overflow: ellipsis; pointer-events: none; }
.ov-bar-edge { position: absolute; top: 0; bottom: 0; width: 6px; cursor: ew-resize; }
.ov-bar-edge-l { left: 0; }
.ov-bar-edge-r { right: 0; }

/* There is no segment lane. A second row of bars restating the hatching on the
   track above was duplicate furniture -- Opus has nothing like it, and it got
   asked about three times before it came out. What tells you a cut happened is
   the cut itself, on the one timeline that already exists. */

/* One bordered card per KEPT piece -- "the editor is still not like capcut".
   Sits directly on the filmstrip (index.html puts #trackBlocks between the
   <img> and #trackGaps), its own background-image the SAME filmstrip url,
   sized/positioned in real pixels so it shows exactly the slice of frames
   under its own span -- a clipped, rounded window onto the one continuous
   strip, not a second copy of it. Restraint: no new colour, silver border at
   rest, gold only while grabbed (the handle language, everywhere else). */
.track-blocks { position: absolute; inset: 0; pointer-events: none; }
.piece-block {
  position: absolute; top: 2px; bottom: 2px;
  background-color: var(--surface-2);
  background-repeat: no-repeat;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  box-shadow: 0 1px 2px rgba(0,0,0,.45);
  cursor: grab;
  pointer-events: auto;
  touch-action: none;
}
.piece-block:hover { border-color: rgba(231,233,236,.4); }
/* Round 8, the owner: "why does the timeline allow breaks and then not connect
   them together". A block that abuts another squares that corner and drops its
   own border there, so the two read as ONE strip cut at a seam -- rounded
   corners facing each other, or two borders stacked into a 2px rule, both draw
   a divider where the export has none. The seam's own blade (.join-marker) is
   what marks the cut. Outer corners keep the radius. */
.piece-block.is-joined-l {
  border-top-left-radius: 0; border-bottom-left-radius: 0; border-left-color: transparent;
}
.piece-block.is-joined-r {
  border-top-right-radius: 0; border-bottom-right-radius: 0; border-right-color: transparent;
}
/* SELECTED. The frame drawn over the piece (.piece-select, further down) is a
   separate overlay node, so the piece itself used to be byte-for-byte
   identical selected or not -- and Delete and Mute act on that piece. The
   block now carries its own state: a silver rim two hairlines thick plus a
   lift off the strip, the same metal language the grabbed state uses, with no
   new colour and no tint. Silver, not gold: gold on this timeline means the
   trim selection and the playhead. Drawn INSET rather than as an outer ring,
   because an outer ring is painted over by whichever neighbour comes later in
   the DOM, and raising the block with z-index would put it over the seam
   blade that marks the cut. Declared BEFORE .is-dragging so a drag of an
   already-selected piece still shows the gold grabbed state. */
.piece-block.is-selected {
  border-color: var(--text);
  box-shadow: inset 0 0 0 2px var(--text), 0 2px 10px rgba(0,0,0,.55);
}
/* Grabbed: a real lift, not a tint -- shadow + 1px up, gold edge (the same
   metal language the trim handles use), above every neighbour while held. */
.piece-block.is-dragging {
  cursor: grabbing;
  transform: translateY(-1px);
  border-color: var(--accent);
  box-shadow: 0 8px 20px rgba(0,0,0,.6), 0 0 0 1px rgba(245,197,24,.45);
}
/* The neighbour a drag has crossed past the midpoint of -- where it would
   land if reordering were real. */
.piece-block.is-drop-target { box-shadow: inset 0 0 0 1px var(--accent); }
/* A piece too short to draw to scale. A trim could crush a neighbour to a 2px
   sliver -- still clickable, effectively invisible -- so it is drawn at the
   minimum usable width and SAYS it is not to scale (dashed edges), rather than
   quietly lying about its length. */
.piece-block.is-min { border-style: dashed; }

/* Same deleted spans, hatched directly over the filmstrip frames they came
   from -- before this the filmstrip kept showing the removed footage as if
   nothing happened, contradicting the segment lane right below it. */
.track-gaps { position: absolute; inset: 0; pointer-events: none; }
/* ...but the CUTS inside it are grabbable. The container stays transparent so
   clicking the picture still scrubs and drags still pan; only the cut itself
   takes the pointer. Without this the track's own pointerdown won every grab
   and panned the whole view -- "it grabs everything and moves it around". */
.join-marker, .track-gap-edge { pointer-events: auto; touch-action: none; }
.join-grab {
  position: absolute; top: 0; bottom: 0; left: 50%;
  width: 14px; margin-left: -7px;          /* hit area, not a visible thing */
  cursor: ew-resize; pointer-events: auto;
}
/* The two sides of a seam that removed content: 11px each, straddling the
   blade so together they are one contiguous 22px target centred on the joint
   (round 8 -- the removed span has no width of its own to sit inside any
   more). Their own hairlines stay hidden at rest: two silver lines flanking a
   5px blade is three lines for one cut. They light gold under the pointer,
   which is also the only moment you need to know which side you have. */
.track-gap-edge {
  position: absolute; top: 0; bottom: 0; width: 11px;
  cursor: ew-resize; background: transparent; z-index: 5;
}
/* Round 10: each side stays on ITS OWN side of the blade. They used to
   straddle it (-9px and +2px against an 11px width), so aiming at a piece's
   own right edge landed on the grip that moves the NEXT piece's start --
   measured: a trim aimed at piece 2 trimmed piece 3. The blade sits at x=0:
   everything left of it belongs to the piece before, everything right of it to
   the piece after, 11px each, still one contiguous 22px target. */
.track-gap-edge--left  { left: -11px; }
.track-gap-edge--right { left: 0; }
.track-gap-edge::before {
  content: ''; position: absolute; top: 3px; bottom: 3px; left: 4px;
  width: 2px; border-radius: 1px; background: rgba(226,232,240,.5);
  opacity: 0;
}
.track-gap-edge:hover::before,
.track-gap-edge.is-dragging::before { opacity: 1; }
.track-gap-edge:hover::before,
.track-gap-edge.is-dragging::before,
.join-marker.is-dragging {
  background: var(--accent);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 0 8px rgba(245,197,24,.55);
}
.track-gap {
  /* Only the LEAD-IN and TAIL now: track outside the first and last kept piece,
     where there genuinely is no content. Removed spans BETWEEN pieces are no
     longer drawn as boxes at all -- the pieces touch and the cut is a seam
     (.join-marker--removed). Hatched, unaccented: empty track is not an event. */
  position: absolute; top: 0; bottom: 0; z-index: 4;
  background: repeating-linear-gradient(135deg,
    rgba(8,9,12,.88) 0 6px, rgba(38,42,50,.88) 6px 12px);
}
/* Round 15, D7. z-index 4 against .playhead-grab's 7 -- and a split PARKS the
   playhead exactly on the seam it just made, so the 18px scrub strip covered
   the whole 14px seam target. Measured after a split: all 15 integer x
   positions across .join-grab hit-tested to #playheadGrab, and a real click on
   the transition timed out at 5000ms because the browser refused to aim at a
   fully covered element.
   The seam wins now (8 > 7). The cost is that the playhead cannot be GRABBED
   in the ~9px either side of a cut -- which is the smaller loss by a long way:
   a plain click anywhere on the track still seeks it, the ruler still scrubs,
   and the arrow keys still step it, while the seam had no other door at all.
   editor.js keeps its #playheadGrab delegation as well, so a seam that ends up
   under the scrub strip for any other reason is still reachable. */
.join-marker {
  position: absolute; top: 0; bottom: 0; width: 3px; margin-left: -1.5px; z-index: 8;
  background: rgba(245,197,24,.9);
}
.join-marker--xfade { background: rgba(231,233,236,.75); }
/* Small always-on tag naming the transition on a join, the same idiom the
   MUTED tag already uses on an audio region -- a join used to be a bare
   colour with no text, so its type was a guess until you right-clicked it. */
.join-badge {
  position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%);
  z-index: 5; padding: var(--sp-1) var(--sp-2); border-radius: 3px;
  font: 10px/1.3 var(--mono); letter-spacing: .06em; white-space: nowrap;
  background: rgba(10,11,13,.85); color: var(--text-dim);
  /* Clickable now (round 7): the chip opens the transition picker, and shows
     its duration on hover. */
  border: 1px solid var(--line-strong); pointer-events: auto; cursor: pointer;
}
.join-badge:hover { border-color: rgba(245,197,24,.5); color: var(--text); }
.join-badge-dur { display: none; color: var(--text-faint); }
.join-badge:hover .join-badge-dur { display: inline; }
/* Under the finger the length is the point, so it stays printed and goes gold
   -- the same live-readout move the speed track makes. */
.join-badge.is-dragging { border-color: rgba(245,197,24,.55); color: var(--text); }
.join-badge.is-dragging .join-badge-dur { display: inline; color: var(--accent); }

/* ── The transition's own body, and its two length handles ─────────────────
   "the transitions need time handles so we can make them longer or shorter."
   The band is as wide as the transition is long and centred on the join --
   which is where the renderer puts the xfade -- so widening it from either
   edge stays symmetric about the cut. */
.join-span {
  position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%);
  z-index: 4; pointer-events: none; border-radius: 2px;
  background: linear-gradient(90deg,
    rgba(226,232,240,0) 0%, rgba(226,232,240,.20) 50%, rgba(226,232,240,0) 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.10);
}
.join-span--fade {
  background: linear-gradient(90deg,
    rgba(245,197,24,0) 0%, rgba(245,197,24,.22) 50%, rgba(245,197,24,0) 100%);
}
.join-span-edge {
  position: absolute; top: 0; bottom: 0; width: 12px;
  pointer-events: auto; cursor: ew-resize; touch-action: none;
}
.join-span-edge--left { left: -6px; }
.join-span-edge--right { right: -6px; }
.join-span-edge::before {
  content: ''; position: absolute; top: 3px; bottom: 3px; left: 5px;
  width: 2px; border-radius: 1px; background: rgba(226,232,240,.55);
}
.join-span-edge:hover::before,
.join-span-edge.dragging::before {
  background: var(--accent); width: 3px; top: 0; bottom: 0;
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 0 8px rgba(245,197,24,.5);
}
.join-badge--xfade { color: var(--text); }
/* P1: on a phone four labelled badges collide on a 7 s clip; the glyph
   alone marks the join, the name lives in the picker it opens. */
@media (max-width: 600px) {
  .join-badge { font-size: 0; gap: 0; padding: 3px 4px; }
  .join-badge .join-badge-glyph { width: 14px; height: 9px; }
}
.join-badge--fade { color: var(--accent); }
/* A set transition has to read at a GLANCE, not just on hover-for-tooltip or
   read-the-badge-text -- a small overlapping-corners glyph sits at the top of
   the seam (the badge already owns the bottom), same silver/gold split as the
   badge itself. */
.join-xfade-glyph {
  position: absolute; top: 2px; left: 50%; transform: translateX(-50%);
  z-index: 5; font: 10px/1 var(--mono); color: var(--text-dim);
  pointer-events: none; text-shadow: 0 1px 2px rgba(0,0,0,.8);
}
.join-xfade-glyph--fade { color: var(--accent); }

/* A plain cut -- a split with nothing removed and no transition on it. It has
   to survive being drawn over ANY frame, bright or dark, so it is a white
   blade inside its own dark casing rather than a tinted line, with a notch top
   and bottom the way every NLE marks a razor cut. Deliberately NOT gold: gold
   is the playhead and the trim handles, and a cut that looked like the
   playhead would be worse than no marker at all. */
.join-marker--cut {
  width: 5px; margin-left: -2.5px;
  background: linear-gradient(to right,
    rgba(5,6,8,.92) 0 1px, #f2f5f9 1px 4px, rgba(5,6,8,.92) 4px 5px);
}
.join-marker--cut::before,
.join-marker--cut::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
}
.join-marker--cut::before { top: 0; border-top: 6px solid #f2f5f9; }
.join-marker--cut::after { bottom: 0; border-bottom: 6px solid #f2f5f9; }

/* Round 8 -- a seam where content WAS removed. The pieces touch (the axis
   gives the removed span zero width), so this is a JOINT between two blocks,
   never a hole: the same razor blade and notches a plain split gets, split
   down the middle by a single gold hairline for the seconds that came out.
   One gold, and it is the only mark on the track that means "content missing
   here"; how much is in the tooltip and, for anyone who wants it back, in the
   two grips either side. Fixed 5px/-2.5px whatever transition sits on it, so
   those grips land in the same place every time. */
.join-marker--removed {
  width: 5px; margin-left: -2.5px;
  background: linear-gradient(to right,
    rgba(5,6,8,.92) 0 1px, #f2f5f9 1px 2px,
    var(--accent) 2px 3px, #f2f5f9 3px 4px, rgba(5,6,8,.92) 4px 5px);
}
.join-marker--removed::before,
.join-marker--removed::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
}
.join-marker--removed::before { top: 0; border-top: 6px solid #f2f5f9; }
.join-marker--removed::after { bottom: 0; border-bottom: 6px solid #f2f5f9; }
.join-cut-glyph {
  position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  z-index: 5; font: 10px/1 var(--mono); color: var(--accent);
  pointer-events: none; text-shadow: 0 1px 2px rgba(0,0,0,.9);
}

/* Right-click menu, per SEGMENT-EDITING-SPEC.md "one action, three doors":
   door 2 (discovery) prints the shortcut beside each item so it teaches
   door 3 (keyboard) without a tutorial. */
.ctx-menu {
  position: fixed; z-index: 80;
  min-width: 180px; padding: 4px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  box-shadow: 0 12px 28px rgba(0,0,0,.5);
}
.ctx-menu-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  width: 100%; padding: var(--sp-2) var(--sp-3); border: 0; border-radius: 4px;
  background: none; color: var(--text); font: 12px/1.2 var(--sans, inherit);
  text-align: left; cursor: pointer;
}
.ctx-menu-item:hover, .ctx-menu-item:focus-visible { background: rgba(245,197,24,.14); outline: none; }
.ctx-menu-shortcut { color: var(--text-faint); font: 10px/1 var(--mono); }
/* A disabled row (a menu's "Piece 1 of 3 · 0:07" header) reads as a header,
   not a dead button: no hover wash, no pointer cursor, dimmer text. */
.ctx-menu-item.is-header {
  color: var(--text-faint); cursor: default; font-weight: 600;
  border-bottom: 1px solid var(--line); border-radius: 4px 4px 0 0;
  margin-bottom: 2px; padding-bottom: var(--sp-1);
}
.ctx-menu-item.is-header:hover, .ctx-menu-item.is-header:focus-visible { background: none; }

/* ── Export ──────────────────────────────────────────────────────────────── */
/* Round 22, THE BLOCKER: THE EXPORT BUTTON WAS NOT CLICKABLE.
 *
 * Measured with real `page.mouse.click` at 1280x800, once a render existed so
 * the Download link was on the row: the button's rect was x 865.6-1025.9 while
 * the dock's own right edge was x 855. Seven clicks across the full width of
 * "Export for TikTok" all landed on `DIV.editor-foot`; a capture listener on
 * the button fired ZERO times. At 1366 also 0/7, at 1600 5/7, at 2000 7/7.
 *
 * The mechanism is NOT z-order or DOM order, and a z-index would not have
 * touched it. `.timeline-dock` declares `overflow-y: auto` and never mentions
 * overflow-x, and per CSS once one axis is not `visible` the other computes to
 * `auto` too -- measured, `getComputedStyle(dock).overflowX === "auto"` with
 * scrollWidth 703 against clientWidth 319. So the dock CLIPS horizontally. The
 * export row was wider than the dock (scrollWidth 490 in a 319px box), the
 * button sat in the clipped-away part, `getBoundingClientRect()` still reported
 * its position in scrolled-content space, and CLIPPED PIXELS ARE NOT
 * HIT-TESTABLE -- the click went straight through to whatever was painted
 * behind, which is the transcript panel's hint bar.
 *
 * The fix is therefore to stop the row overflowing at all, at any width:
 *  - the row wraps, so the status line takes one line and the actions another
 *    as soon as they cannot share one;
 *  - the actions wrap in turn, so Export drops onto its own full-width line
 *    rather than being pushed past the edge;
 *  - `min-width: 0` everywhere a flex item could otherwise refuse to shrink;
 *  - `min-width: max-content` on the button itself, so it never wins the
 *    argument by being clipped and never loses it by having its label cut --
 *    it wraps to its own line instead.
 * The wide layout is unchanged: at 2000px the row still measures one line. */
.export-row {
  margin-top: auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  flex-wrap: wrap;
  min-width: 0;
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--line);
  background: var(--surface);
  position: sticky; bottom: 0;
}
/* Round 4, defect #4: the post-export line ("Ready — 1080x1920 · captions
   burned · mediapipe · ...") only ellipsized in simple-mode, via a hardcoded
   180px max-width cutoff (since removed) that had nothing to do with the panel's real
   width -- at 1000px it cut the text mid-word ("captio…") well short of
   where the row actually ran out of room. `.export-row` is a flex row with
   `.export-actions` fixed-width (flex: none); giving THIS item `flex: 1 1
   auto` + `min-width: 0` lets it take exactly the space actually left over
   and ellipsize there, in every mode, at every width -- no magic number. */
/* `flex: 1 1 auto` + `min-width: 0` is round 4's own answer and it is kept
   exactly: the status still takes the room actually left over and ellipsizes
   there. What round 22 added is the WRAP on the row above, which is what the
   basis feeds -- with a short status the row still fits one line, and with a
   long one the actions drop to a line of their own instead of being pushed
   past the dock's clipped edge. */
.export-status {
  font-size: 12px; color: var(--text-dim); min-height: 17px; font-variant-numeric: tabular-nums;
  flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.export-status.is-err { color: var(--err); }
.export-status.is-ok  { color: var(--ok); }
/* "The file you can download is older than the clip on screen." Amber, the
   same warning tone the health chip uses -- not an error (nothing failed),
   not silence (which is what it used to be). */
.export-status.is-stale { color: var(--warn); }
/* Round 14, D1: the elapsed clock beside the stage. Same treatment as
   `.proj-elapsed` on a project card and for the same reason -- the label is
   the sentence, the clock is the proof the render has not hung. Tabular
   figures so the seconds tick in place instead of shuffling the row. */
.export-elapsed {
  margin-left: 8px;
  font: 600 10px/1 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}
.export-status.is-wait { color: var(--text-dim); }

/* Download is an <a>: it has no disabled attribute, so a stale file stayed a
   live link no matter what the status line said. This is the visible half of
   downloadDisabledReason() -- the title attribute carries the words. */
.btn.is-disabled,
.btn.is-disabled:hover {
  opacity: .45; cursor: not-allowed; pointer-events: auto;
  filter: grayscale(1);
}
.export-actions {
  display: flex; align-items: center; gap: var(--sp-2);
  /* was `flex: none`, which is what made this box wider than the dock and put
     the button in clipped space. It may now shrink and it may wrap. */
  flex: 1 1 auto; min-width: 0; flex-wrap: wrap; justify-content: flex-end;
}
.export-actions .caption-style { min-width: 0; }
/* PA3: the three shape pills. Same .seg as the board's Virality/Timeline
   switch, tightened to sit beside the Export button on a 390 phone. */
.export-actions .format-seg { flex: 0 0 auto; padding: 3px; }
.export-actions .format-seg .seg-btn { padding: 5px 9px; font-size: 11px; font-family: var(--mono); letter-spacing: .02em; }
.export-actions #aspectRatio { min-width: 0; max-width: 100%; }
/* The one control the whole app exists to reach. It may grow into spare room,
   it may take a whole line of its own -- it may never be narrower than its own
   label, because a clipped Export button is a dead Export button. */
.export-actions #exportBtn { flex: 1 1 auto; min-width: max-content; }
.export-actions #downloadBtn { flex: 0 1 auto; min-width: max-content; }

.cleanup-summary {
  padding: 6px var(--sp-4) 10px;
  font-size: 12px; color: var(--text-dim);
  background: var(--surface);
  font-variant-numeric: tabular-nums;
}
.cleanup-summary.is-err { color: var(--err); }

.caption-style {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; color: var(--text-dim); user-select: none;
}
#captionStyle,
#aspectRatio,
#musicSelect,
#musicDuckAmount {
  height: 30px; min-width: 104px;
  padding: 0 24px 0 9px;
  background: var(--surface-2)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%239aa1ab' d='M1 3.5 5 7.5 9 3.5'/%3E%3C/svg%3E")
    no-repeat right 8px center;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 12px;
  appearance: none; cursor: pointer;
}
#aspectRatio { min-width: 72px; }
#musicDuckAmount { min-width: 84px; }
#musicSelect { max-width: 190px; text-overflow: ellipsis; }
#captionStyle:focus-visible,
#aspectRatio:focus-visible,
#musicSelect:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── Caption preset picker: a visual grid of tiles instead of a text <select> ─
   Each tile is a real rendered sample from the server (the same renderer that
   burns the export), so you can actually tell the presets apart at a glance. */
.caption-style-picker {
  display: flex; flex-direction: column; gap: 8px;
  flex: 1 1 100%;
}
.caption-style-picker-label {
  font-size: 10px; font-weight: 600; letter-spacing: .02em;
  color: var(--text-dim);
}
.preset-grid {
  display: grid;
  /* Two columns made each sample 657x456px, so eight presets stood 2031px tall
     in an 814px pane and you had to scroll to see half of them. The tile is a
     rendered sample, not a photo -- it only has to be big enough to read
     "THIS IS INSANE" and tell the styles apart. The clamp keeps four across
     over every realistic pane width (260px cap when wide, 150px floor when
     narrow) and drops to three rather than crushing them. */
  grid-template-columns: repeat(auto-fit, minmax(clamp(150px, 22%, 260px), 1fr));
  gap: 10px;
}
.preset-tile {
  display: flex; flex-direction: column; gap: 6px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r);
  cursor: pointer;
  text-align: left;
  transition: border-color .12s ease, transform .12s ease, background .12s ease;
}
.preset-tile:hover {
  border-color: var(--line-strong);
  transform: translateY(-1px);
  background: var(--surface-2);
}
.preset-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.preset-tile.is-selected {
  border-color: var(--accent);
  background: rgba(245, 197, 24, .06);
  box-shadow: 0 0 0 1px var(--accent);
}
.preset-tile-thumb {
  display: block;
  aspect-ratio: 320 / 210;
  border-radius: calc(var(--r) - 3px);
  background: #0a0b0d;
  overflow: hidden;
}
.preset-tile-thumb img {
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
}
.preset-tile-label {
  font-size: 12px; color: var(--text-dim); font-weight: 600;
  text-align: center;
}
.preset-tile.is-selected .preset-tile-label { color: var(--accent); }

/* ── Audio row: sound switch + music picker + volume + mix mode ─────────── */
.audio-row {
  margin-top: auto;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--line);
  background: var(--surface);
}
/* The export row normally claims the flex column's free space; when the audio
   row sits above it, the audio row claims it instead so the two stay glued. */
.audio-row + .export-row { margin-top: 0; }
/* One quiet line under the Volume control saying what the export does to the
   sound. Not a banner, not a modal, not a colour: it is a fact about the
   finished file, so it reads at the weight of a caption. Its words come from
   the API, so this rule must survive a sentence of any length. */
.audio-note {
  margin: var(--sp-2) 0 0;
  color: var(--text-faint); font-size: 12px; line-height: 1.45;
  max-width: 62ch;
}
#musicUploadBtn { font-size: 12px; letter-spacing: .03em; }
.music-volume {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12px; color: var(--text-dim); user-select: none;
}
.music-volume input[type="range"] {
  width: 90px; accent-color: var(--accent); cursor: pointer;
}
.music-volume b {
  font: 600 10px/1 var(--mono); color: var(--text);
  min-width: 32px; text-align: right; font-variant-numeric: tabular-nums;
}
.audio-row .seg-btn { padding: 4px 10px; font-size: 12px; flex-shrink: 0; }

/* ── Word editor ─────────────────────────────────────────────────────────── */
.editor { display: flex; flex-direction: column; background: var(--surface); min-width: 0; min-height: 0; }
.editor-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line);
}
.editor-tools { display: flex; gap: var(--sp-3); }
.editor-meta {
  padding: 8px var(--sp-4); border-bottom: 1px solid var(--line);
  font: 10px/1.4 var(--mono); color: var(--text-faint); min-height: 29px;
}

/* The transcript read as scattered words rather than speech: every word is its
   own clickable box with 4px of padding and 1px of margin, so on top of the
   6.45px space between them each word sat ~16px from the next, at 14px type on
   a 2.05 line. Pull the air out -- the click target keeps its height, it just
   stops holding its neighbours at arm's length. */
.words { flex: 1; overflow-y: auto; padding: var(--sp-4); font-size: 14px; line-height: 1.85; }
.tr-para {
  position: relative;
  margin: 0 0 10px;
  padding-left: 58px;
}
.tr-para:last-child { margin-bottom: 0; }
/* The time this block starts at, in the gutter. 50 screens of transcript with
   nothing to navigate by is the actual complaint; this is the anchor. */
.tr-time {
  position: absolute; left: 0; top: 2px;
  font: 10px/1.6 var(--mono);
  color: var(--text-faint);
  letter-spacing: .02em;
  user-select: none;
}
.editor-empty { color: var(--text-faint); font-size: 12px; line-height: 1.6; }

.w {
  display: inline-block;
  padding: 1px 2px; margin: 0 -1px;
  border-radius: 4px;
  cursor: pointer;
  color: var(--text-dim);
  transition: background .1s ease, color .1s ease;
  border: 1px solid transparent;
}
.w:hover { background: var(--surface-3); color: var(--text); }
.w.in-clip { color: var(--text); }
.w.out-clip { opacity: .38; }
.w.is-active { background: var(--accent); color: #1a1400; font-weight: 600; }
.w.is-edited { border-bottom: 1px solid var(--accent-dim); }
.w.is-low { text-decoration: underline dotted rgba(240,136,62,.55); text-underline-offset: 3px; }
.w:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Hidden from the captions: still here, struck through, one click restores. */
.w.is-hidden { text-decoration: line-through; opacity: .45; color: var(--text-dim); }
.w.is-hidden:hover { opacity: .8; text-decoration-color: var(--accent); }
.w-hide {
  display: inline-block; vertical-align: middle;
  width: 20px; height: 20px; margin: 0 2px 0 0; padding: 0;
  font: inherit; font-size: 15px; line-height: 18px;
  color: var(--text-dim); background: var(--surface-3);
  border: 1px solid var(--line-strong); border-radius: 4px; cursor: pointer;
}
.w-hide:hover, .w-hide:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }

.w-input {
  display: inline-block;
  font: inherit; font-size: 14px;
  padding: 0 4px; margin: 0 1px;
  min-width: 46px;
  background: var(--surface-3);
  border: 1px solid var(--accent);
  border-radius: 4px;
  color: var(--text);
  outline: none;
}

.editor-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--line);
}
.save-state { font-size: 10px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.save-state.is-saving { color: var(--live); }
.save-state.is-saved  { color: var(--ok); }
.save-state.is-err    { color: var(--err); }

/* ── Toasts ──────────────────────────────────────────────────────────────── */
.toasts {
  position: fixed; right: var(--sp-4); bottom: var(--sp-4);
  display: flex; flex-direction: column; gap: 7px; z-index: 90;
  max-width: 380px;
  /* N4b: a toast is a notice, not a control. On a phone it lands over the
     open sheet, and the tap meant for the tile under it hit the toast
     instead -- which counted as a press OUTSIDE the card and closed it. Taps
     now go straight through to whatever the toast is covering. */
  pointer-events: none;
}
/* And while a sheet is up on a phone the notices move to the top of the
   screen, so they never sit on the card's tiles at all. */
body:has(.piece-tools.is-sheet:not([hidden])) .toasts { top: var(--sp-4); bottom: auto; }
.toast {
  padding: 10px 12px;
  background: var(--surface-3);
  border: 1px solid var(--line-strong);
  border-left: 2px solid var(--text-faint);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
  font-size: 12px; line-height: 1.45;
  animation: toast-in .18s ease;
}
.toast.ok   { border-left-color: var(--ok); }
.toast.err  { border-left-color: var(--err); }
.toast.warn { border-left-color: var(--warn); }
.toast b { display: block; font-weight: 600; margin-bottom: 2px; }
.toast span { color: var(--text-dim); }

@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.spin { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

@media (max-width: 1280px) {
  .layout { --rail-w: 210px; }
}

/* The transcript is useful context, but the picture and export controls win on
   compact laptop/tablet widths. Clips remains one tap away in the top switch. */
@media (max-width: 1100px) {
  .topbar { gap: var(--sp-3); }
  .build-stamp { display: none; }
  /* The properties panel used to be hidden here. It cannot be: it now holds
     the caption presets, the audio controls and the framing controls. The
     clips list goes instead — it is a whole view of its own, one tap away. */
}

@media (max-width: 860px) {
  .topbar { gap: var(--sp-2); padding-inline: 10px; }
  #projectSelect { min-width: 130px; max-width: 220px; }
  .view-switch { margin-left: 0; }
  .topbar-actions .btn,
  .topbar-actions > label {
    width: 38px; height: 38px; padding-inline: 0; justify-content: center;
    font-size: 0;
  }
  /* S1 step 2: with the credit readout in the row, the project header no
     longer fits a 390px phone with both chips spelled out. The health chip
     keeps its dot (the colour is the message); the minutes keep their number. */
  .topbar-actions #healthChip { padding: 0 4px; }
  .topbar-actions #healthText { display: none; }
  .topbar-actions .chip-credits { padding: 0 7px; gap: 6px; }
  .topbar-actions .credit-left { display: none; }
  /* P1 (2026-09-08): the row was 397px on a 390px phone -- the page scrolled
     sideways. The crumb gives way first (min-width 0), the actions pack
     tighter, Sign out is its icon. Nothing is allowed past the edge. */
  .topbar { overflow: hidden; }
  /* P1: the phone gets the simple editor + its tools; the two-column full
     editor does not fit a 390px screen and is not offered there. */
  .layout.focus:not(.view-board) #focusToggleBtn { display: none; }
  .topbar .crumbs { min-width: 0; flex: 1 1 auto; }
  .topbar-actions { gap: 6px; flex: none; }
  .topbar-actions .btn { width: 34px; height: 34px; }
  .view-tab { padding-inline: 10px; }
  .layout:not(.view-board) .stage { border-right: 0; }
  .layout:not(.view-board) .stage-head { flex-direction: column; }
  .layout:not(.view-board) .stage-toggles { width: 100%; flex-wrap: wrap; }
}

/* ── Board — clip grid + find panel ─────────────────────────────────────── */

.view-switch {
  display: flex; gap: 2px; padding: 4px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 99px; margin-left: var(--sp-3);
}
.view-tab {
  appearance: none; border: 0; cursor: pointer;
  /* Round 13, defect #3: measured 66x20 / 74x20 -- padding: 4px 16px gives a
     12px line only 8px of vertical padding, 4px under the 24px touch-target
     floor. The button itself is the target (its 30px parent doesn't help),
     so the button's own padding grows, not the .view-switch wrapper. */
  padding: 8px 16px; border-radius: 99px;
  font: 600 12px/1 var(--font); letter-spacing: .02em;
  color: var(--text-dim); background: transparent;
  transition: color .15s, background .15s;
}
.view-tab:hover { color: var(--text); }
.view-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Round 26b/26c. On the Clips page, gold is load-bearing in exactly three
   places: the brand mark, .btn-gold (the export/import call-to-action), and
   a focus ring (:focus-visible outlines, plus #boardPrompt/.board-search
   focus glow) -- every other surface, including this one, an active filter
   toggle, and the find-panel container, uses the same flat
   surface-2/line-strong treatment as any other "on" state. An active view
   tab is not a key moment -- it was its own second gold gradient recipe
   (f9e07a/f5c518/cf9f0e vs btn-gold's fae584/f5c518/c69a0c), five distinct
   gold RGBs doing the job of one. */
.view-tab.is-active {
  background: var(--surface-2); color: var(--text);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}

/* View switching: board hides the editor trio; editor hides the board. */
.layout.view-board .rail,
.layout.view-board .stage,
.layout.view-board .editor,
.layout.view-board .timeline-dock,
.layout.view-board .splitter { display: none; }


.layout:not(.view-board) .board { display: none; }

.layout.view-board {
  display: block;
  overflow-y: auto;
  padding: var(--sp-5);
}

.board { max-width: 1240px; margin: 0 auto; }

/* Find panel — the "what do you want me to find" window */
/* Round 26c, defect #3: this container carried a permanent gold radial wash
   PLUS a perpetually-animating gold "travelling edge light" ring around its
   whole border. A container is not a key moment (rule 1) -- gold now stays
   on the brand mark, .btn-gold, and focus rings only. Flat surface + line,
   same as any other panel; the travelling-light rule and its keyframes are
   removed outright rather than left dead in the sheet. */
.find-panel {
  position: relative;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.05);
}
.find-panel > * { position: relative; z-index: 2; }
.find-panel-row { display: flex; gap: var(--sp-3); align-items: stretch; }
#boardPrompt {
  flex: 1; min-width: 0;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--r);
  padding: 12px 14px; font: 400 14px/1.3 var(--font);
}
#boardPrompt:focus {
  outline: none; border-color: rgba(245,197,24,.55);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
#boardPrompt::placeholder { color: var(--text-faint); }
.find-count {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: 4px 8px; background: var(--bg);
  border: 1px solid var(--line-strong); border-radius: var(--r);
}
.find-count label { font: 600 10px/1 var(--font); letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); }
.find-count input {
  /* Round 12, defect #8: measured 52x19 -- under the 24px touch-target
     floor. min-height clears it without changing the number's own type
     scale. */
  width: 52px; min-height: 24px; background: none; border: 0; color: var(--text);
  font: 600 14px/1.2 var(--mono); text-align: center;
}
.find-count input:focus { outline: none; }
/* Round 11, defect #6: outline: none above left #boardCount with NOTHING
   substituted -- no ring at all, while its sibling #boardPrompt gets a
   3px accent-glow box-shadow on focus. The ring has to land on the WRAPPER
   (the input itself has no border of its own to draw a box-shadow against),
   the same way :focus-within already has to work for a labelled control. */
.find-count:focus-within { box-shadow: 0 0 0 3px var(--accent-glow); border-color: rgba(245,197,24,.55); }
/* Chrome paints its native spin-button on this number input -- measured a
   white ~rgb(242,242,242) widget sitting directly on the #0a0b0d surface on
   both hover and focus. Same appearance:none treatment already shipped for
   .trash-filter's search-cancel-button, just for the other native widget. */
.find-count input::-webkit-inner-spin-button,
.find-count input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.find-count input { -moz-appearance: textfield; }
.btn-gold {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 0 24px; border: 0; border-radius: var(--r); cursor: pointer;
  font: 700 14px/1 var(--font); letter-spacing: .01em; color: #141002;
  background: linear-gradient(180deg, #fae584 0%, #f5c518 48%, #c69a0c 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.6), inset 0 -1px 0 rgba(0,0,0,.25),
              0 2px 8px -2px rgba(245,197,24,.45);
  transition: filter .15s, transform .05s;
}
.btn-gold:hover:not(:disabled) { filter: brightness(1.07); }
.btn-gold:active:not(:disabled) { transform: translateY(1px); }
.btn-gold:disabled { opacity: .45; cursor: not-allowed; }
.find-status { margin-top: var(--sp-3); font: 500 12px/1.4 var(--font); color: var(--accent); }

/* The seam between the one primary action (prompt + Find clips, above) and
   the two secondary entry paths (transcript search + import, below). Quiet
   on purpose: this is a demotion, not a new section. */
.find-panel-divider { height: 1px; background: var(--line); margin: var(--sp-4) 0 var(--sp-3); }
.find-panel-utility { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* Board head */
.board-search {
  display: flex; align-items: center; gap: 8px; flex: 1 1 260px; min-width: 0;
  /* Round 13, defect #3: vertical padding used to live here, on the flex
     wrapper -- but a click 4px above #momentSearch's own box (still inside
     this wrapper's padding) does not focus a plain <input> with no <label>
     wrapping it, so the wrapper's padding bought no clickable/focusable
     area at all; only the input's own box does. Any vertical padding left
     here re-creates the same dead zone one size smaller, so it moves onto
     the input entirely (below) -- the wrapper keeps only the horizontal
     padding that frames the icon/count against its own border.
     Measured: with 0 vertical padding here, the input's enlarged box (see
     below) reaches all the way to the wrapper's top/bottom border, so a
     click 4px above the input's OLD position now lands inside the new one. */
  padding: 0 var(--sp-3);
  background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--r-sm); color: var(--text-dim);
}
/* Round 26c, defect #2: #momentSearch had outline:0 with no replacement --
   no border change, no shadow, nothing. Give it the same focus treatment
   #boardPrompt already has. Focus lands on the <input>, but the visible
   border lives on this wrapper, so the ring goes on the wrapper via
   :focus-within. */
.board-search:focus-within {
  border-color: rgba(245,197,24,.55);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.board-search input {
  flex: 1; min-width: 0; background: transparent; border: 0; outline: 0;
  color: var(--text); font-size: 12px;
  /* Round 13, defect #3: measured 902x16 -- the input had no padding of its
     own, so its real hit height was just the 12px text line. 8px top/bottom
     puts the input's own box at 32px, comfortably clearing the 24px floor,
     and moves its top edge up far enough that a click 4px above the old
     16px box now lands inside the input's own padding, not the wrapper. */
  padding: 8px 0;
}
.board-search input::placeholder { color: var(--text-faint); }
.board-search-count { font: 10px/1 var(--mono); color: var(--text-faint); white-space: nowrap; }
/* Round 12, defect #5: measured live -- with "music" typed and the mouse
   parked, the max-chroma pixel at the native search-cancel glyph's position
   was rgb(63,113,183), byte-identical to the blue .trash-filter already
   suppresses just above (its own comment: "the only non-gold saturated
   colour anywhere in this app's chrome"). Same fix, same reused affordance. */
.board-search input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.board-search-clear {
  appearance: none; flex: none; width: 24px; height: 24px; padding: 0;
  display: grid; place-items: center;
  background: var(--surface-3); border: 1px solid var(--line-strong);
  border-radius: 999px; color: var(--text-dim);
  font: 600 14px/1 var(--font); cursor: pointer;
}
.board-search-clear:hover { color: var(--text); border-color: var(--line-strong); }
.board-search-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.moment-hits {
  margin: 0 var(--sp-4) var(--sp-3);
  max-height: 260px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-1);
}
.moment-hit {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  padding: 8px var(--sp-3); text-align: left;
  background: transparent; border: 0; border-bottom: 1px solid var(--line);
  color: var(--text-dim); font-size: 12px; cursor: pointer;
}
.moment-hit:last-child { border-bottom: 0; }
.moment-hit:hover { background: rgba(255,255,255,.04); color: var(--text); }
.moment-hit-time {
  font: 10px/1 var(--mono); color: var(--accent); flex: none;
  font-variant-numeric: tabular-nums;
}
.moment-hit-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.moment-hit-text b { color: var(--text); background: rgba(245,197,24,.22); border-radius: 2px; }
.moment-hit-clip { margin-left: auto; flex: none; font-size: 10px; color: var(--text-faint); }
/* Round 12, defect #7: distinguishes the row that WRITES (makes a clip) from
   the one that only opens what already exists -- same faint weight as
   before would say both are equally inert. */
.moment-hit-clip.moment-hit-make { color: var(--text-dim); }

.board-head {
  display: flex; align-items: center; justify-content: space-between;
  margin: var(--sp-5) 0 var(--sp-4);
}
.board-head h2 { margin: 0; font-size: 14px; font-weight: 600; color: var(--text-dim); letter-spacing: .02em; }
/* This had NO rule, so it was a plain block: the segmented control and the
   Select button stacked, leaving Select dangling under "Virality" off-axis.
   One row, centre-aligned, Select sitting to the right of Favorites. */
.board-tools { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: nowrap; }
.board-head h2 { white-space: nowrap; }
@media (max-width: 600px) {
  .board-head { flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
  .board-tools { width: 100%; overflow-x: auto; scrollbar-width: none; }
  .board-tools::-webkit-scrollbar { display: none; }
}
/* Match the pill height so the two controls read as one row of equal weight. */
.board-tools #selectModeBtn { height: 28px; border-radius: 99px; }
/* Round 4, defect #4: a pill with a longer label (Speed's own "2x ·
   partial", markPartialPill above) wrapped to two lines and bled the whole
   .seg row past .panel-group's own border, right up to the properties
   panel's true edge, at 1000px -- .seg is a flex row with no width of its
   own (a flex ITEM inside .panel-group, another flex row), so with no
   min-width:0 it never shrank; it just grew past its container.
   First attempt gave every .seg-btn min-width:0 + ellipsis, but flexbox
   shrinks ALL shrinkable siblings roughly proportionally -- with 5 pills in
   the row it turned "None / 0.5x / 0.75x / 1.5x / 2x · partial" into
   unreadable "N… / 0… / 0… / 1… / 2x · p…" soup, which is worse than the
   wrap it replaced. The excess width here is caused by exactly ONE pill:
   markPartialPill only appends " · partial" to the CURRENT/active option,
   the same one pillGroup marks .is-active -- every other label already
   fits. So only .is-active gets to shrink; every plain pill keeps
   flex-shrink:0 and is never touched, same "chip" ellipsis idiom as
   .zoom-region/.speed-region's own .region-label further up this file,
   applied to the one pill that can actually be long. */
.seg {
  display: flex; gap: 2px; padding: 4px; min-width: 0; flex: 1 1 auto;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 99px;
}
.seg-btn {
  appearance: none; border: 0; cursor: pointer;
  /* Round 12, defect #8: 4px 12px against a 12px/1 label measured 73x20 on
     #sortViral -- under the 24px touch-target floor. 6px keeps the pill's
     proportions but clears it (12 + 6 + 6 = 24). */
  padding: 6px 12px; border-radius: 99px;
  font: 600 12px/1 var(--font); color: var(--text-dim); background: transparent;
  white-space: nowrap; flex-shrink: 0;
}
.seg-btn.is-active {
  /* Round 7, #6: a --surface-3 tint plus a 6%-alpha hairline measured
     30.68% pixels changed / max channel delta 77 on its own bbox between
     active tabs -- under the 15%-or-Δ80 readability bar this app holds
     every other on/off state to. #favFilter and #selectModeBtn already use
     an accent inset frame for exactly this "which state am I in" job; this
     is that same single-gold accent role, not a new hue. */
  color: var(--accent); background: var(--surface-3);
  box-shadow: inset 0 0 0 1px rgba(245,197,24,.45);
  flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
/* Round 26c, defect #2: none of these four had their own focus-visible rule,
   so a keyboard user tabbing the clip grid got Chromium's default
   outline-color rgb(16,16,16) on a #0a0b0d page -- invisible. */
.seg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The grid itself */
.clip-grid {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
}
.board-empty {
  grid-column: 1 / -1; text-align: center; padding: var(--sp-6);
  color: var(--text-faint); font-size: 14px;
  border: 1px dashed var(--line-strong); border-radius: var(--r-lg);
}
.clip-card {
  appearance: none; border: 0; background: none; cursor: pointer;
  /* D3: padding-bottom was 0 (from the shorthand `padding: 0` below), so a
     two-line title's descenders sat flush on the card's own border -- and
     on the 2px gold selection outline, where it read worst. 8px on the 4/8
     scale gives the clamp box room to breathe under the card edge. */
  padding: 0 0 var(--sp-2);
  display: flex; flex-direction: column; gap: 8px; text-align: left;
  border-radius: var(--r-lg);
}
.clip-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-thumb {
  position: relative; aspect-ratio: 9 / 16; width: 100%;
  border-radius: var(--r-lg); overflow: hidden;
  background: linear-gradient(180deg, var(--surface-3), var(--surface));
  border: 1px solid var(--line-strong);
  box-shadow: 0 2px 10px -4px rgba(0,0,0,.7);
  transition: transform .16s ease, border-color .16s, box-shadow .16s;
}
.clip-card:hover .card-thumb {
  transform: translateY(-3px);
  border-color: rgba(245,197,24,.55);
  box-shadow: 0 10px 26px -10px rgba(0,0,0,.85), 0 0 0 1px rgba(245,197,24,.25);
}
.card-thumb img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; display: block;
}
.card-thumb.thumb-missing img { display: none; }
/* Virality reads first, so it sits top-LEFT where the eye lands; the star and
   the overflow menu take the opposite corner.
   Round 26 made this chip neutral on the rule "metal marks what you can DO,
   never a number you only read". Quincy overruled that on 2026-09-16: white
   digits on a dark pill sitting over a bright video frame were hard to pick
   out at a glance, and the score is the one number the grid exists to
   compare. So the digits are gold now -- the same gold the rail card and the
   inspector pill already give the score, which makes every place a score
   appears finally agree. The dark glass stays: it is what keeps the number
   legible over any frame underneath. The chip is still not a button (no
   gold FILL, no glow) so it does not read as something to press; rank still
   comes from the number and from sort order, not from a hue. */
.card-score {
  position: absolute; top: 8px; left: 8px;
  min-width: 30px; text-align: center;
  padding: 4px 8px; border-radius: 7px;
  font: 700 12px/1 var(--mono); color: var(--accent);
  background: rgba(6,7,9,.82); backdrop-filter: blur(4px);
  border: 1px solid rgba(245,197,24,.32);
  text-shadow: 0 1px 2px rgba(0,0,0,.55);
}
.card-dur {
  /* Round 10: up beside the score. The bottom-right corner now holds the play
     affordance that moved off the middle of the card, and two chips fighting
     for one corner is how you get a mis-click.
     Round 7, #9: top was 9px against .card-score's 8px -- one clip card
     carried four different corner insets (8/9/7/7/6px). Normalised to the
     same 8px every other badge on this card now uses; left stays at 46px,
     that offset is layout (beside the score chip), not a corner inset. */
  position: absolute; top: 8px; left: 50%; transform:translateX(-50%);
  padding: 4px 8px; border-radius: 6px;
  font: 600 10px/1 var(--mono); color: var(--text);
  background: rgba(6,7,9,.72); backdrop-filter: blur(4px);
  border: 1px solid rgba(255,255,255,.12);
}
.card-state {
  /* Round 26c, defect #1: this pill and .card-fav both sat in the
     bottom-LEFT corner, and DOM order painted the pill on top of the heart
     -- every heart click on every card landed on this label instead, which
     then bubbled to the card and navigated to the Editor. The pill and
     .card-play are mutually exclusive by clip status (this only renders when
     status !== 'ready'; play only renders when status === 'ready'), so the
     bottom-RIGHT corner is always free when this shows. Moved there, and
     pointer-events:none besides -- a status label must never be able to
     steal a click from anything underneath it. */
  position: absolute; bottom: 8px; right: 8px;
  padding: 4px 8px; border-radius: 6px;
  font: 700 10px/1 var(--font); letter-spacing: .07em;
  color: var(--text-dim); background: rgba(6,7,9,.72);
  border: 1px solid rgba(255,255,255,.14);
  pointer-events: none;
}
.card-state.is-err { color: var(--err); border-color: rgba(242,85,90,.4); }
/* Slice 1 (audit 39): a clip rendering itself after Find clips. Gold, because
   it is the one thing on the board that is happening right now. */
.card-state.is-busy { color: var(--accent); border-color: rgba(245,197,24,.38); }
.card-progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: rgba(255,255,255,.12); pointer-events: none;
}
.card-progress i {
  display: block; height: 100%; background: var(--accent);
  transition: width .6s linear;
}
/* Round 15, D6 DOES NOT REPRODUCE, and this rule is deliberately unchanged.
   The referee's numbers are all real -- computed `display: flow-root`,
   `text-overflow: clip`, height 35.09px against scrollHeight 53 -- but they do
   not mean what they were read to mean. In current Chrome a `-webkit-box` that
   carries `-webkit-line-clamp` REPORTS its display as `flow-root`: a bare
   probe with the clamp computes flow-root, the same probe without it computes
   -webkit-box, and neither is inside a flex container. `text-overflow` has no
   say in a line clamp (the clamp draws its own ellipsis), and scrollHeight
   exceeding height is what a working clamp looks like, not a broken one.
   Screenshotted both ways at the card's real 163.4px width, with the title
   forced to six lines: this rule renders "A very long clip title / that will
   definitely…" -- two lines, ellipsis present, pixel-identical to a rebuilt
   version with the clamp moved off the flex item. Nothing to fix here.
   The full string is on the element's title attribute (editor.js) so a clamped
   name is still readable, which is the one part of the complaint that stood. */
.card-title {
  font: 600 12px/1.35 var(--font); color: var(--text);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  /* D3: two lines at 12px/1.35 need 16.2px x 2 = 32.4px, but the clamp box's
     computed clientHeight rounded down to 32px, shaving descenders off the
     second line. Explicit min-height gives it the fraction back. */
  min-height: 33px;
}
.card-subs { font: 500 10px/1 var(--mono); color: var(--text-faint); letter-spacing: .02em; }
/* PA2: the hook under the title, one line, and the whole post one tap away.
   Same 26px copy square as the Post card so the glyph means the same thing
   everywhere; it only appears on cards that have post copy. */
.card-post { display: grid; grid-template-columns: minmax(0, 1fr) 26px; gap: 6px; align-items: center; margin-top: -2px; }
.card-hook { font: 500 11px/1.4 var(--font); color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-copy { margin-top: 0; }

/* Narrow preview dock */
@media (max-width: 860px) {
  .layout.view-board { padding: var(--sp-4); }
  .find-panel-row { flex-wrap: wrap; }
  #boardPrompt { flex-basis: 100%; }
  .btn-gold { padding: 10px 18px; }
  /* D4: this used to drop the track minimum to 132px at the same time the
     container's own padding shrank (var(--sp-5) -> var(--sp-4) above), so
     narrowing the window from 900 to 860 gained a column instead of losing
     one -- a 22% jump down in poster size for LESS width. Keeping the track
     minimum at the same 158px as the unshrunk grid means the extra room
     from the smaller padding goes to the existing columns, not a new one;
     column count now only ever decreases as the viewport narrows. */
  .clip-grid { grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: var(--sp-3); }
  .view-switch { margin-left: var(--sp-2); }
  .view-tab { padding: 8px 11px; }
}

/* ── Simple clip view — finished video + actions, editor behind Edit ─────── */

/* Simple mode (no clip open / cut-down view) is a TWO-column layout, and it
   needs to place every child itself. It used to declare 2 columns while the
   main rules placed the stage in column 3 and the panel in column 5 -- columns
   that do not exist here, so the grid created implicit tracks and the player
   ended up in a void on the right. This state was never in any review brief,
   which is why it shipped. */
/* Round 22, defect 4: THE CAPTION WAS HIDDEN ON THE ONE SCREEN THAT NEEDS IT.
 *
 * Measured: after an export the app switches to this finished view, and the
 * whole Post card measured 0x0 with `offsetParent === null` -- because it
 * lives inside `.editor` and the rule below said `.editor { display: none }`.
 * So the screen where a person is about to paste a caption into TikTok was the
 * one screen with no caption on it. Round 19 built the card believing `.editor`
 * was "the one home that survives every layout mode"; it survives three of
 * four, and this is the fourth.
 *
 * The finished view gets a third column carrying the Post card and nothing
 * else: the finished video, the caption and hashtags with their Copy button,
 * and Download/Export along the bottom. The column is clamped rather than
 * fixed so it cannot eat a narrow window, and the picture is height-bound in
 * this view anyway (measured 471.4x838 in a 1048px-tall row), so it does not
 * cost the picture anything. */
.layout.simple-mode {
  grid-template-columns:
    var(--rail-w, 230px) minmax(0, 1fr) clamp(240px, 24vw, 380px);
  grid-template-rows: minmax(0, 1fr) auto;
}
.layout.simple-mode .rail  { grid-row: 1; grid-column: 1; }
.layout.simple-mode .stage { grid-row: 1; grid-column: 2; border-right: 0; }
.layout.simple-mode .timeline-dock { grid-row: 2; grid-column: 1 / -1; }
.layout.simple-mode .splitter { display: none; }
/* Not `display: none` any more -- only the parts of the editor that ARE the
   editor. The Post card is not one of them: it is what you POST. */
.layout.simple-mode .editor {
  display: flex; flex-direction: column;
  grid-row: 1; grid-column: 3;
  overflow-y: auto;
}
.layout.simple-mode .editor > *:not(.post-card) { display: none; }
.layout.simple-mode .panel-reveal { display: none; }
.layout.simple-mode .trimmer { display: none; }
.layout.simple-mode .player-side .readout { display: none; }
.layout.simple-mode .player-side .switch-block { display: none; }
.layout.simple-mode .stage-toggles .switch { display: none; }
.layout.simple-mode .player-frame { cursor: pointer; }

/* Hover-play preview videos on board cards */
.card-thumb video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; display: block;
}
/* Simple view: lead with the video — hide the analyst's rationale line.
   The export status itself now ellipsizes to its own container's real
   width in every mode (see .export-status, above) -- no mode-specific
   override needed. */
.layout.simple-mode #clipRationale { display: none; }

/* ClipBait brand: "Bait" is the gold hook. */
.brand-gold {
  background: linear-gradient(180deg, #fae584 10%, #f5c518 55%, #c69a0c 95%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

.brand { white-space: nowrap; }
/* "Qlip" was invisible until you hovered it. .brand is a <button>, and a button
   does NOT inherit `color` from the page -- it starts at the UA's buttontext
   (near-black), which on this black bar is nothing at all. Only the :hover rule
   set a colour, so the word appeared on mouseover and vanished again. Set it
   explicitly; chrome-white against the gold "Bait". */
.brand-name { white-space: nowrap; color: #e8ecf3; }

/* Card controls: explicit play + edit */
/* Round 10: OFF the middle of the card. A 46px play button parked dead centre
   owned the one place everybody clicks, so the first instinct -- click the clip
   you want to work on -- played it instead of opening it. The card face is now
   the editor; play is a corner affordance beside the duration, and hovering the
   thumbnail still previews the clip silently. */
.card-play {
  position: absolute; left:50%; top:50%; right:auto; bottom:auto; transform:translate(-50%,-50%);
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; line-height: 1; color: #fff; cursor: pointer;
  background: rgba(6,7,9,.72); border: 1px solid rgba(255,255,255,.22);
  backdrop-filter: blur(4px);
  transition: background .15s, border-color .15s, transform .15s;
  padding: 0 0 0 2px; margin: 0; font: inherit; -webkit-appearance: none; appearance: none;
}
.card-play:hover {
  background: linear-gradient(180deg, #fae584, #f5c518 55%, #c69a0c);
  color: #141002; border-color: transparent;
  transform: translate(-50%,-50%) scale(1.08);
}
.card-play:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-edit {
  position: absolute; top: 8px; left: 8px;
  padding: 4px 9px; border-radius: 7px; cursor: pointer;
  font: 700 10px/1 var(--font); letter-spacing: .03em; color: var(--text);
  background: rgba(6,7,9,.72); border: 1px solid rgba(255,255,255,.18);
  backdrop-filter: blur(4px);
  transition: background .15s, color .15s, border-color .15s;
}
.card-edit:hover {
  background: linear-gradient(180deg, #fae584, #f5c518 55%, #c69a0c);
  color: #141002; border-color: transparent;
}
/* draft pill moves down a hair so it never kisses the edit button */
.card-state { bottom: 8px; }

/* ── Stage fit: the export row must never overlap the player ────────────── */
/* The caption block sits at the bottom of the player. When the stage is short,
   .export-row was painting over it and swallowing every pointerdown, so caption
   drag/resize silently did nothing. Keep the row out of the player's space and
   give the caption layer the higher stacking order. (Do NOT constrain the frame
   height here — a percentage max-height collapses it and the captions shrink to
   nothing.) */
.player-wrap { position: relative; z-index: 2; }
.stage .export-row { position: relative; z-index: 1; flex: none; }
.cap-layer { z-index: 3; }
.cap-block { z-index: 4; }

/* The caption layer spans the whole frame. With pointer-events:auto it blanket-
   blocks the crop guide underneath (drag/resize silently dead everywhere).
   Only the caption block itself should be interactive. */
.cap-layer { pointer-events: none; }
.cap-block { pointer-events: auto; }

/* ── Imported media (overlays) ────────────────────────────────────────────
   The layer spans the crop guide, so an overlay's position and size are
   measured against the OUTPUT canvas — the same coordinate space the burn
   uses. pointer-events: none on the layer keeps it from blanket-blocking the
   crop guide underneath; only the overlays themselves are grabbable. */
.ov-layer { position: absolute; inset: 0; pointer-events: none; z-index: 2; }

.ov-item {
  position: absolute;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  cursor: grab;
  line-height: 0;
  outline: 1px dashed rgba(245,197,24,.55);
  outline-offset: 2px;
}
.ov-item.is-dragging { cursor: grabbing; outline-color: rgba(245,197,24,.95); }
.ov-item.is-selected { outline: 2px solid var(--accent); outline-offset: 2px; }
.ov-item img, .ov-item video { width: 100%; height: auto; display: block; }
/* Outside its own time window an overlay is dimmed rather than hidden, so it
   stays grabbable while the user positions it. */
.ov-item { opacity: var(--ov-opacity, 1); }
.ov-item.is-outside { opacity: calc(var(--ov-opacity, 1) * .28); filter: grayscale(.6); }

/* ── Cover-ups (B4) ──────────────────────────────────────────────────────
   The layer is the video's exact box (same inset, same framing transform);
   each patch is placed inside the contain-fit picture rect by editor.js. A
   patch blurs what is under it and, while the Picture tools are open, can be
   dragged and resized like an overlay. */
.player-frame .hide-layer {
  position: absolute; inset: var(--stage-pad, 0);
  pointer-events: none; overflow: hidden;
  /* Above the seek mask and the two transition holds (z 3): those are
     frozen frames painted from the decoded picture, sticker and all. */
  z-index: 3;
}
.hide-patch {
  position: absolute; box-sizing: border-box;
  backdrop-filter: blur(22px) saturate(.6); -webkit-backdrop-filter: blur(22px) saturate(.6);
  background: rgba(20,20,22,.28);
  border-radius: 6px;
}
.hide-patch[hidden] { display: none; }
.hide-patch .hide-patch-fill { position: absolute; inset: 0; width: 100%; height: 100%; display: block; border-radius: inherit; }
.player-frame.is-tools-open .hide-layer { pointer-events: auto; }
.player-frame.is-tools-open .hide-patch {
  outline: 1.5px dashed rgba(245,197,24,.9); outline-offset: 1px; cursor: grab;
}
.player-frame.is-tools-open .hide-patch.is-dragging { cursor: grabbing; }
.hide-patch .ov-handle { display: none; }
.player-frame.is-tools-open .hide-patch .ov-handle { display: block; }
.hide-patch .hide-patch-tag {
  position: absolute; left: 0; top: -20px; font: 600 10px/16px var(--font-mono, ui-monospace, monospace);
  letter-spacing: .04em; color: #0b0b0c; background: var(--accent, #f5c518); padding: 0 6px; border-radius: 3px;
  display: none; white-space: nowrap;
}
.player-frame.is-tools-open .hide-patch .hide-patch-tag { display: block; }
.hide-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.hide-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.hide-row .muted { flex: 1; }

.ov-handle {
  position: absolute; width: 11px; height: 11px;
  background: var(--accent, #f5c518);
  border: 1px solid rgba(0,0,0,.6); border-radius: 2px;
  pointer-events: auto;
}
.ov-handle.tl { left: -6px; top: -6px; cursor: nwse-resize; }
.ov-handle.tr { right: -6px; top: -6px; cursor: nesw-resize; }
.ov-handle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; }
.ov-handle.br { right: -6px; bottom: -6px; cursor: nwse-resize; }

.overlay-row {
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--line);
}
.overlay-list { display: flex; flex-direction: column; gap: 5px; }
.ov-entry {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 12px; color: var(--text-dim);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r); padding: 5px 8px;
}
.ov-entry.is-selected { border-color: rgba(245,197,24,.55); }
.ov-entry .ov-name {
  max-width: 20ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text);
}
.ov-entry input[type="number"] { width: 62px; }
.ov-entry input[type="range"] { width: 76px; }

/* ── Properties panel (tabs) ──────────────────────────────────────────────
   Framing, audio, caption style and caption colours each used to be their own
   full-width strip in the dock — a label, one control, and dead space across
   the rest of the window. Tabbing them into the right-hand column removes four
   strips and gives each group room to breathe. */
.panel-tabs {
  display: flex; gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3) 0;
  border-bottom: 1px solid var(--line);
  flex: none;
  /* Clip and Audio make five. On a narrow panel they do not all fit, and the
     answer is not to hide one -- "i cant find it" is the bug being fixed.
     The strip scrolls sideways instead, and showPanelTab scrolls whichever
     tab selection just opened back into view. No visible scrollbar: it is a
     tab strip, not a list. */
  overflow-x: auto; scrollbar-width: none;
}
.panel-tabs::-webkit-scrollbar { display: none; }
.panel-tab {
  appearance: none; border: 0; background: transparent;
  color: var(--text-dim); font: inherit; font-size: 12px; font-weight: 500;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm) var(--r-sm) 0 0;
  border-bottom: 2px solid transparent;
  cursor: pointer; white-space: nowrap;
}
.panel-tab:hover { color: var(--text); background: var(--surface-2); }
/* Stays reachable however far the strip has scrolled. */
.panel-collapse { position: sticky; right: 0; margin-left: auto; background: var(--surface); }
.panel-tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }

.panel-body { display: flex; flex-direction: column; min-height: 0; flex: 1; overflow-y: auto; }

/* ── Round 23: the text menu moved ABOVE the timeline ─────────────────────
   Quincy's two markups, in his own hand: the Clip/Audio/Transcript/Captions
   strip ringed in the right-hand column with "TEXT MENU FOR" and an arrow
   pointing at the timeline, and the tall black rectangle beside the picture
   ringed with "DONT WASTE SPACE" / "HARD TO SEE".
   Both are one move. The strip now sits on top of the dock, the category it
   selects fills the dock's own height (which is where the empty rectangle
   was), and TIMELINE is the first item in the strip so the way back never
   moves. What is left on the right is the Post card alone -- "no two stacked
   windows on the right, one pane". */
.timeline-dock > .panel-tabs { padding: var(--sp-2) var(--sp-4) 0; }
.timeline-dock > .panel-body { flex: 1 1 auto; min-height: 0; }
/* The wrapper is layout-transparent: the trimmer keeps every measurement it
   had as a direct child of the dock. */
.panel-body--timeline {
  display: flex; flex-direction: column;
  overflow: visible; padding: 0; gap: 0;
}
.panel-body--timeline > .trimmer { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* The slack the empty rectangle used to hold now goes to the two rows that
   read better bigger. The film strip only grows a little: its frames are laid
   out in SOURCE TIME across the width, so it is object-fit:fill and extra
   height stretches the faces in it. The waveform has no such constraint --
   it is drawn, not photographed -- so it takes the rest, and a tall waveform
   is the one thing in this pane that gets genuinely easier to read
   ("HARD TO SEE", his other note on the same screenshot). */
/* Round 25: the lanes are SLIM, and the slack is the transcript's.
   Quincy ringed the timeline at 1920x1080 and wrote "I HATE HOW THIS LOOKS
   VERY BAD". Measured there: #track 260px, #audioLane 180px -- 440px of the
   dock spent on a 9:16 source drawn object-fit:fill across 1338px, so every
   frame was a 22px-wide vertical smear of a 146px-wide frame, sixty of them,
   reading as wallpaper. Round 24's own comment already said the height "buys
   vertical smear, not information" and then kept 260 anyway, because a
   shorter strip on a bigger window felt indefensible. It isn't: opus.pro's
   video track is slim at EVERY window size, and consistency is the point.
   72px is two rows of a 9:16 frame at honest proportion (see renderFilmstrip,
   which now derives its frame COUNT from this height so the frames stop being
   squashed). 84px of waveform is enough to read a drop. The height that stops
   being spent here goes to the transcript, which round 24b already routes. */
.panel-body--timeline #track { flex: 3 1 56px; min-height: 56px; max-height: 72px; }
.panel-body--timeline #audioLane { flex: 2 1 40px; min-height: 40px; max-height: 84px; }
/* Both lanes have a ceiling -- a 600px waveform is not "using the space", it is
   a different kind of silly -- so on a very tall column there is slack left
   over. It is split above and below rather than left as a hole under content
   stuck to the top, which is what read as "cut off". */
.panel-body--timeline > .trimmer { justify-content: center; }
/* A category the timeline is hiding must still say it has something for you.
   Selecting a piece no longer STEALS the pane (that would hide the very thing
   you just clicked); it marks its tab instead. Silent is not invisible. */
.panel-tab.is-hinted { color: var(--accent); }
.panel-tab.is-hinted::after {
  content: ""; display: inline-block; width: 5px; height: 5px; margin-left: 5px;
  border-radius: 50%; background: var(--accent); vertical-align: middle;
}

.panel-body[data-panel="transcript"] { overflow: hidden; }   /* .words scrolls itself */
.panel-body[data-panel="captions"],
.panel-body[data-panel="audio"],
.panel-body[data-panel="frame"],
.panel-body[data-panel="clip"] { gap: var(--sp-3); padding: var(--sp-3); }

/* Inside the panel the old strips lose their full-width chrome and stack. */
.panel-body .frame-row,
.panel-body .audio-row,
.panel-body .caption-colors,
.panel-body .overlay-row {
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface-2);
  padding: var(--sp-3);
  flex-wrap: wrap; row-gap: var(--sp-2);
}
.panel-group {
  display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface-2); padding: var(--sp-3);
  /* Hard backstop for round 4, defect #4: whatever a child's own sizing
     does, nothing renders past this group's own border -- the group's own
     padding (--sp-3, 12px) is what gives the panel edge its clearance. */
  overflow: hidden;
}
.panel-group .caption-style { flex: 1 1 140px; }
.panel-group .caption-style select { width: 100%; }
/* Fade in/out/shape, transition, split -- these ghost buttons used to be
   indistinguishable from plain text labels (like the pill-group titles right
   next to them) until you hovered them. A faint border says "this is a
   button" without adding the visual weight a filled button would. */
.panel-group .btn-ghost { border-color: var(--line-strong); }

/* ── Piece panel: CapCut-style "this menu belongs to the selected piece" ──── */
/* The clip's source window, relocated here from the dock (round 9). Quiet:
   it is context for the piece numbers below it, not a headline. */
.piece-clip-window {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-2); padding: 0 var(--sp-1) var(--sp-2);
  border-bottom: 1px solid var(--line); flex: none;
}
.piece-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-2); padding: 0 var(--sp-1);
}
.piece-head h2 { font-size: 12px; }
.piece-field {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-dim);
}
.piece-field input {
  height: 28px; width: 84px;
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 12px; padding: 0 8px;
}
.piece-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.piece-actions { display: flex; gap: var(--sp-2); padding: 0 var(--sp-1); }

/* The clip's own In / Out / Length -- the answer to "why cant i adjust the
   clip size". Sits at the top of the Clip panel whether or not a piece is
   selected, because the clip always has a length. The Length label carries
   its own rule ("moves the end") so nothing has to be guessed and no
   paragraph of instructions has to live permanently on screen. */
.clip-window-fields { flex-wrap: wrap; align-items: flex-end; }
.clip-window-fields .piece-head { flex: 1 0 100%; }
/* Length is the widest label of the three, so it gets the room to say what it
   does instead of being clipped to "Length (s) - moves th...". */
.clip-window-fields .piece-field:last-child { flex: 1 0 100%; }
.clip-window-fields .piece-field:last-child input { width: 100%; }

/* The way from a selected piece to that piece's sound. A door, not a duplicate
   control -- everything behind it lives once, on the Audio tab. */
.piece-audio-jump { align-self: flex-start; margin: 0 var(--sp-1); }

/* -- Volume ---------------------------------------------------------------
   The fade slider's chassis on a different number, so the Audio section reads
   as one instrument rather than three. 100% gets a mark you can aim for. */
.gain-slider-detent {
  position: absolute; top: 50%; width: 1px; height: 10px; margin-top: -5px;
  background: var(--line-strong); pointer-events: none;
}
.gain-slider.is-set .fade-slider-fill { background: rgba(231,233,236,.42); }

/* ── The speed value track ────────────────────────────────────────────────
   One draggable number, full panel width. The rail is 4px; the HIT AREA is
   28px, because what you grab must be bigger than what you see. Silver
   everywhere; the single gold is the readout while fine-drag is engaged --
   the only state this control has. */
.speed-group { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
.speed-head { display: flex; align-items: baseline; gap: var(--sp-2); }
.speed-head .panel-group-title { margin-right: 0; }
.speed-lengths {
  flex: 1 1 auto; min-width: 0;
  font: 10px/1 var(--mono); color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-variant-numeric: tabular-nums;
}
/* The readout is the ENTRY. It reads as a number, not as a form field, until
   you put the cursor in it -- then it is plainly a box you can type in. */
.speed-readout {
  font: 600 14px/1 var(--mono); color: var(--text);
  font-variant-numeric: tabular-nums; letter-spacing: .01em;
  width: 5ch; padding: 4px; text-align: right;
  background: transparent; border: 1px solid transparent;
  border-radius: var(--r-sm);
}
.speed-readout:hover { border-color: rgba(201,209,220,.28); }
.speed-readout:focus {
  outline: none; background: rgba(255,255,255,.05);
  border-color: rgba(245,197,24,.55); color: var(--text);
}
.speed-readout.is-fine { color: var(--accent); }
.speed-readout.is-partial { color: var(--text-dim); }
.speed-times {
  font: 600 14px/1 var(--mono); color: var(--text-dim);
  margin-left: 1px; white-space: nowrap;
}
.speed-reset { height: 24px; padding: 0 10px; font-size: 10px; }
.speed-reset:disabled { opacity: .35; cursor: default; }

.speed-track {
  position: relative; height: 28px; width: 100%;
  cursor: ew-resize; touch-action: none;
  border-radius: var(--r-sm);
}
.speed-track:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.speed-rail {
  position: absolute; left: 0; right: 0; top: 50%; height: 4px; margin-top: -2px;
  background: #0d0f13; border: 1px solid var(--line-strong); border-radius: 3px;
}
.speed-fill {
  position: absolute; top: 50%; height: 4px; margin-top: -2px;
  background: rgba(231,233,236,.28); border-radius: 3px; pointer-events: none;
}
.speed-tick {
  position: absolute; top: 50%; width: 1px; height: 8px; margin: -4px 0 0 0;
  background: rgba(231,233,236,.22); pointer-events: none;
}
/* Returning to normal must be the easiest thing on the track, so 1x is the
   only tick you can see from across the room. */
.speed-tick.is-unity { height: 14px; margin-top: -7px; background: rgba(231,233,236,.7); }
.speed-knob {
  position: absolute; top: 50%; left: 50%; width: 14px; height: 14px;
  margin: -7px 0 0 -7px; border-radius: 50%;
  background: linear-gradient(160deg, #ffffff 0%, #c7ccd3 42%, #7d838c 100%);
  box-shadow: 0 1px 2px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.85);
  pointer-events: auto; cursor: grab;
}
.speed-track:active .speed-knob { cursor: grabbing; }
.speed-knob.is-pulse { animation: speed-detent 60ms ease-out; }
@keyframes speed-detent {
  from { transform: scale(1); }
  50%  { transform: scale(1.45); }
  to   { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .speed-knob.is-pulse { animation: none; }
}

/* The selected piece, hatched over on the track exactly like a cut is, but
   silver rather than gold -- gold on this timeline already means "the trim
   selection / playhead", so the piece needs its own colour to read as a
   different kind of selected. A full frame (not just side ticks) plus small
   corner grips is what makes the selection read over busy video thumbnails,
   CapCut-selection weight -- still pointer-events:none, it's a paint layer. */
.piece-select {
  position: absolute; top: 0; bottom: 0;
  border: 1.5px solid var(--text);
  background: rgba(231,233,236,.07);
  pointer-events: none; z-index: 4;
}
.piece-select::before, .piece-select::after,
.piece-select .grip-l, .piece-select .grip-r {
  content: ''; position: absolute; width: 6px; height: 6px;
  border: 1.5px solid var(--text); background: var(--text);
}
.piece-select::before { top: -1px; left: -1px; }
.piece-select::after { top: -1px; right: -1px; }
.piece-select .grip-l { bottom: -1px; left: -1px; }
.piece-select .grip-r { bottom: -1px; right: -1px; }

/* The second, VISIBLE door into the per-piece menu. Everything this menu
   offers -- split, delete, mute, punch-in, reorder, the transition after this
   piece -- was reachable only by right-clicking, which is a gesture the app
   never advertises anywhere. One control, one glyph, no copy; it opens the
   exact same menu the right-click does. Its parent is a paint layer
   (pointer-events:none), so it takes the pointer back for itself. */
.piece-more {
  position: absolute; top: 2px; right: 7px; z-index: 7;
  width: 22px; height: 16px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(10,11,13,.55); border-radius: 4px;
  background: linear-gradient(180deg, #fbfcfd 0%, #e7e9ec 45%, #b7bcc4 100%);
  color: #0a0b0d; font: 700 10px/1 var(--font); letter-spacing: .06em;
  cursor: pointer; pointer-events: auto;
  box-shadow: 0 1px 2px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.7);
}
.piece-more:hover, .piece-more:focus-visible {
  background: linear-gradient(180deg, #fceca3 0%, #ffd42e 55%, #d4a70f 100%);
  outline: none;
}

/* ── Drag-to-resize splitters ─────────────────────────────────────────────
   Every boundary between panels is grabbable, so the user decides how much
   room the list, the picture, the properties and the timeline each get. */
.splitter { background: transparent; position: relative; z-index: 5; }
.splitter::after {
  content: ''; position: absolute; inset: 0;
  background: var(--line); transition: background .12s ease;
}
.splitter:hover::after, .splitter.is-dragging::after { background: var(--accent); }
.splitter-v { grid-row: 1; width: 5px; cursor: col-resize; margin: 0 -2px; }
.splitter-v::after { left: 2px; right: 2px; }
.splitter-h { grid-column: 1 / -1; height: 5px; cursor: row-resize; margin: -2px 0; }
.splitter-h::after { top: 2px; bottom: 2px; }
/* Round 13, defect 5: the only thing that said this 5px strip was draggable
   was its tooltip. A short chrome grip, centred, says it silently -- the same
   idiom every NLE uses, and no copy. */
.splitter-h::before {
  content: ''; position: absolute; left: 50%; top: 1px;
  width: 34px; height: 3px; margin-left: -17px; border-radius: 2px;
  background: linear-gradient(180deg, rgba(231,233,236,.34), rgba(231,233,236,.12));
  pointer-events: none;
}
.splitter-h:hover::before, .splitter-h.is-dragging::before {
  background: linear-gradient(180deg, rgba(231,233,236,.62), rgba(231,233,236,.28));
}
body.is-resizing { cursor: inherit; user-select: none; }
body.is-resizing iframe, body.is-resizing video { pointer-events: none; }


/* ── Clip card: heart + overflow menu ─────────────────────────────────────
   Score top-left, menu top-right, heart bottom-left. The heart is always on
   the poster — barely-there white when off, solid red when on — so the state
   is readable at a glance across a whole grid without hovering anything. */
.card-more {
  /* Round 7, #9: was 6px, off the 8px every other corner badge now uses. */
  position: absolute; top: 8px; right: 8px;
  width: 26px; height: 26px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,.16); border-radius: 7px;
  background: rgba(10,11,13,.66); backdrop-filter: blur(6px);
  color: var(--text); line-height: 1;
  cursor: pointer; opacity: .55;
  transition: opacity .12s ease, background .12s ease;
}
.clip-card:hover .card-more, .clip-card:focus-within .card-more { opacity: 1; }
.card-more:hover { background: rgba(10,11,13,.92); border-color: var(--line-strong); }
.card-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; opacity: 1; }

/* Round 26e, defect #7: heart/dots/check were bare text glyphs (17px/14px),
   outside the 10/12/14/17 scale. Now an inline <path fill="currentColor">
   sized by its own svg width/height (see svgIconPath in editor.js), so the
   glyph never competes with the app's real type sizes -- color/hover/focus
   below still drive it exactly as they drove the old text glyph. */
.card-fav {
  /* Round 7, #9: was 7px, off the 8px every other corner badge now uses. */
  position: absolute; right: 8px; left:auto; bottom: 8px;
  width: 28px; height: 28px; padding: 0;
  display: grid; place-items: center;
  border: 0; background: transparent;
  line-height: 1; cursor: pointer;
  color: rgba(255,255,255,.28);
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.8));
  transition: color .14s ease, transform .12s ease;
}
.card-fav svg { width:17.6px; height:17.6px; stroke:#fff; stroke-width:1.5; stroke-linejoin:round; }
.card-fav:hover { color: rgba(255,255,255,.6); transform: scale(1.12); }
.card-fav:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-fav.is-on { color: #ff2d3f; }
.card-fav.is-on:hover { color: #ff5060; }

.card-menu {
  position: fixed; z-index: 80; min-width: 190px;
  display: flex; flex-direction: column; padding: 5px;
  background: var(--surface-3);
  border: 1px solid var(--line-strong); border-radius: var(--r);
  box-shadow: var(--shadow);
  animation: toast-in .12s ease;
}
.card-menu-item {
  appearance: none; border: 0; background: transparent;
  color: var(--text); font: inherit; font-size: 12px; text-align: left;
  padding: 8px 9px; border-radius: var(--r-sm); cursor: pointer;
  white-space: nowrap;
}
.card-menu-item:hover { background: var(--surface-2); }
.card-menu-item.is-danger { color: var(--err); margin-top: 3px; border-top: 1px solid var(--line); }
.card-menu-item.is-danger:hover { background: rgba(242,85,90,.12); }

/* ── Multi-select (batch actions) ─────────────────────────────────────────
   Opus Clip's model: a Select button flips the grid into selection mode and
   the actions apply to everything ticked. A per-card menu is the wrong shape
   for "delete twelve of these". */
.select-bar {
  position: sticky; top: 0; z-index: 12;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  margin-bottom: var(--sp-3); padding: 9px var(--sp-4);
  background: var(--surface-3);
  border: 1px solid var(--line-strong); border-radius: var(--r);
  box-shadow: var(--shadow);
  font-size: 12px; font-weight: 500;
}
.select-bar-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-danger { background: rgba(242,85,90,.14); border-color: rgba(242,85,90,.45); color: #ff8a8e; }
.btn-danger:hover:not(:disabled) { background: rgba(242,85,90,.24); border-color: rgba(242,85,90,.7); }
/* Round 26e, defect #2: the round-26c fix above was still a no-op --
   .btn-secondary is ALREADY --surface-2, so only a 0.07-alpha hairline
   (--line-strong) was left to say "on". Measured mouse-away: 5.60% pixels
   changed, max channel delta 30/255, against .seg-btn.is-active's proven
   17.28% / 79 and card selection's 4.05% / 235. This borrows the accent
   frame the app already proves reads as state -- the focus/state gold role,
   not a new gold surface. */
#selectModeBtn.is-active,
#favFilter.is-active {
  background: rgba(245,197,24,.1); color: var(--accent);
  box-shadow: inset 0 0 0 1px rgba(245,197,24,.45);
}

/* In selection mode the whole card is a checkbox target, so play/heart/menu
   step aside — a mis-tap during a batch delete is expensive. */
.clip-grid.is-selecting .card-play,
.clip-grid.is-selecting .card-fav,
.clip-grid.is-selecting .card-more { display: none; }
.card-check {
  /* Round 7, #9: was 7px, off the 8px every other corner badge now uses. */
  position: absolute; top: 8px; right: 8px;
  /* Round 12, defect #8: measured 22x22 -- under the 24px touch-target
     floor. */
  width: 24px; height: 24px; border-radius: 6px;
  border: 2px solid rgba(255,255,255,.7);
  background: rgba(10,11,13,.6); backdrop-filter: blur(6px);
  display: none; place-items: center;
  color: #141002; line-height: 1;
}
.card-check svg { width: 12px; height: 12px; }
.clip-grid.is-selecting .card-check { display: grid; }
.clip-card.is-checked .card-check { background: var(--accent); border-color: var(--accent); }
.clip-card.is-checked { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── Card stacking: the hover preview must sit UNDER the controls ─────────
   The hover <video> is absolutely positioned and inserted into the thumb at
   hover time, AFTER the badges in DOM order. Among positioned elements with
   z-index:auto, paint order follows DOM order — so the video painted straight
   over the score, heart, ⋯ and duration, and they vanished (and stopped being
   clickable) the moment the mouse touched a card. Explicit layers fix it. */
.card-thumb img,
.card-thumb video { z-index: 1; }
.card-score,
.card-dur,
.card-state,
.card-fav,
.card-more,
.card-check,
.card-play { z-index: 3; }

/* ── Imported-media drop zone ─────────────────────────────────────────────
   Two critic findings solved by one element: 13.3% of the Captions panel was
   trailing dead space, and "+ Add media" was a lone button in its own box.
   The drop zone GROWS into that space and doubles as the discoverability cue
   for drag-and-drop, which otherwise has no visible affordance at all. */
.media-drop {
  flex: 1 1 auto; min-height: 92px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: var(--sp-3); text-align: center;
  border: 1px dashed var(--line-strong); border-radius: var(--r);
  background: var(--surface-2);
  color: var(--text-dim); font-size: 12px; cursor: pointer;
  transition: border-color .14s ease, background .14s ease, color .14s ease;
}
.media-drop:hover { border-color: rgba(245,197,24,.5); color: var(--text); }
.media-drop.is-drop-target {
  border-color: var(--accent); border-style: solid;
  background: rgba(245,197,24,.07); color: var(--text);
}
.media-drop b { color: var(--text); font-size: 12px; font-weight: 600; }
.media-drop-icon { font-size: 17px; line-height: 1; color: var(--text-faint); }
.media-drop-hint { line-height: 1.5; max-width: 30ch; }

/* Was: an odd last tile spanned the full row so a 7-preset grid left no
   dangling cell. That assumed a two-column grid; at four across a
   double-width final tile is the thing that looks broken. */

/* Framing controls only exist while the user is in manual mode — they had a
   whole properties tab to themselves for a status line the crop guide already
   prints and a zoom slider the corner handles already do. Round 10 moved them
   from a 54px row UNDER the picture onto the strip ABOVE it: one line, no
   wrapping (wrapping is what made it 54px tall), no border of its own. */
.frame-row {
  display: flex; align-items: center; gap: var(--sp-3);
  min-width: 0; padding: 0 var(--sp-2);
  border: 0; background: none;
  /* WRAPS ITS OWN CONTENTS. Crop size and Reset-to-auto cannot shrink, so in a
     narrow stage column they overflowed the line and drew ON TOP of Full frame
     -- measured at 1366x768 in the source view. Folding here rather than on
     .stage-strip means only the SOURCE view (where these two exist at all) ever
     grows a second line; the finished view, which is what he looks at, stays
     one 22px line. */
  flex-wrap: wrap;
}
/* The status line — and, during a corner drag pinned at full size, the reason
   the corner has stopped moving. See frameStatusText() in editor.js. */
#frameStatus {
  flex: 1 1 auto; min-width: 0;
  font-size: 10px; color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#frameStatus.is-at-limit { color: var(--accent); }
.frame-row .frame-zoom, .frame-row #frameResetBtn { flex: none; }

/* ── Find-clips progress ──────────────────────────────────────────────────
   The wait that most needed feedback had none: the analyse job reported
   stage "starting" / progress 0.0 for its entire run, so the UI could only
   show a spinner. The windowed scan knows how many sections are done, so the
   bar fills for real rather than faking motion. */
.find-status { display: flex; flex-direction: column; gap: 7px; margin-top: var(--sp-3); }
.find-progress {
  height: 6px; border-radius: 99px; overflow: hidden;
  background: var(--surface-3); border: 1px solid var(--line);
}
.find-progress > i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--accent-dim), var(--accent));
  box-shadow: 0 0 12px var(--accent-glow);
  transition: width .45s cubic-bezier(.22,.61,.36,1);
}
.find-status-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  font-size: 12px; color: var(--text-dim);
}
.find-status-row b { color: var(--accent); font: 600 12px/1 var(--mono); font-variant-numeric: tabular-nums; }

/* ── Properties panel: blocks must never be squeezed below their content ──
   These are flex items in a column, so flex-shrink let the panel crush them
   when space was tight: the caption-colour row was clamped to 42px while its
   swatches wrapped to 71px, and the overflow rendered straight underneath the
   Add-media zone. The panel already scrolls, so the right behaviour is to keep
   natural height and let it scroll. `flex: none` on every block; the drop zone
   alone may GROW into leftover space, but never shrink. */
.panel-body > .panel-group,
.panel-body > .caption-colors,
.panel-body > .overlay-row,
.panel-body > .frame-row,
.panel-body > .audio-row,
.panel-body > .piece-head,
.panel-body > .piece-actions { flex: none; min-height: 0; }
#pieceBody { display: flex; flex-direction: column; gap: var(--sp-3); min-height: 0; }
#pieceBody > .panel-group,
#pieceBody > .piece-head,
#pieceBody > .piece-actions { flex: none; min-height: 0; }
.media-drop { flex: 1 0 auto; }

/* Brand is now a button (click = refresh); keep it looking like the old div. */
.brand { appearance: none; border: 0; background: transparent; padding: 0; cursor: pointer; }
.brand:hover .brand-name { color: #fff; }
.brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-sm); }

/* Import lives inside the find panel now, in the quiet utility row alongside
   transcript search (round 26) rather than its own centred block. */
/* P1 (2026-09-08): importing lives on the Projects board (drop zone + link
   field) and in the editor's Import tab. A second pair of import buttons on
   the clip grid was clutter -- two ways in on a screen that is not the way in. */
.board-import { display: none; }

/* Outside its time window during PLAYBACK an overlay is gone, exactly as the
   export renders it. Paused, it stays faintly visible (.is-outside) so it can
   still be grabbed and repositioned. */
.ov-item.is-hidden { opacity: 0 !important; pointer-events: none; }

/* ── Round 7 ──────────────────────────────────────────────────────────────── */

/* The playhead's continuation through the lanes: one line across the whole
   edit (Opus/CapCut's reference). Positioned in px by setPlayheadAt against
   the trimmer.
   It used to be "painted only -- the grab stays on the track's own strip",
   which meant 44 of the playhead's 99 visible pixels were a line you could
   see and could not take hold of. Measured on clip 443: pressing the line in
   the middle of the audio lane hit #audioLane and dragged the playhead
   0.00s. The child below is its grab, so the whole line is one handle. */
.trimmer { position: relative; }
.playhead-span {
  position: absolute; width: 2px; margin-left: -1px;
  background: var(--text); opacity: .5;
  pointer-events: none; z-index: 6;
}
/* 18px, the same target the track's own strip uses -- a 2px line is a line
   you miss. The parent stays pointer-events:none so the lanes underneath keep
   every pixel this does not claim; a child may re-enable them for itself. */
.playhead-span-grab {
  position: absolute; top: 0; bottom: 0; left: 50%;
  width: 18px; margin-left: -9px;
  cursor: ew-resize; touch-action: none; pointer-events: auto;
}

/* The ruler scrubs (pointerdown seeks + drags), so it has to invite it -- and
   it has to be hittable: 11px was the height of its own 10px type, which is
   an 11px target for the surface every editor scrubs from first. */
.track-scale { cursor: ew-resize; touch-action: none; }

/* ── The scrub bar under the player (item 2) ──────────────────────────────
   One slim silver track, ONE gold fill and dot -- the playhead/trim-handle
   metal language, at the one place a key moment earns it. Linear over kept
   content only; mm:ss at both ends. */
.scrub-row {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; flex: none;
  padding: 0 2px;
}
.scrub-time {
  font: 10px/1 var(--mono); color: var(--text-dim);
  font-variant-numeric: tabular-nums; flex: none; min-width: 34px;
}
.scrub-time:last-child { text-align: right; }
.scrub-bar {
  position: relative; flex: 1; height: 18px;
  cursor: pointer; touch-action: none;
}
.scrub-track {
  position: absolute; left: 0; right: 0; top: 50%; height: 4px;
  transform: translateY(-50%);
  background: rgba(231,233,236,.16);
  border-radius: 99px;
  box-shadow: inset 0 1px 1px rgba(0,0,0,.5);
}
.scrub-fill {
  position: absolute; left: 0; top: 50%; height: 4px; width: 0;
  transform: translateY(-50%);
  background: linear-gradient(180deg, #fae584 0%, #f5c518 60%, #c69a0c 100%);
  border-radius: 99px;
}
.scrub-dot {
  position: absolute; top: 50%; left: 0;
  width: 12px; height: 12px; margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: linear-gradient(180deg, #fae584 0%, #f5c518 55%, #c69a0c 100%);
  box-shadow: 0 1px 3px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.45);
  pointer-events: none;
}
.scrub-row.is-scrubbing .scrub-dot,
.scrub-bar:hover .scrub-dot { transform: scale(1.15); }
.layout.view-board .scrub-row { display: none; }

/* ── Per-piece edge grips on the selected block (item 3) ──────────────────
   Chunky || handles ON the selected piece's own edges, Opus-style: dragging
   one trims THAT piece only. Gold on grab -- the handle language. */
.piece-edge {
  position: absolute; top: 0; bottom: 0; width: 12px;
  cursor: ew-resize; pointer-events: auto; touch-action: none; z-index: 6;
}
/* Round 10: inside the piece, not centred on its edge. A grip that hangs 6px
   over the boundary is 6px of the NEIGHBOUR that trims this piece instead --
   the same seam ambiguity as the cut grips above. 12px each, fully owned. */
.piece-edge--l { left: 0; }
.piece-edge--r { right: 0; }
.piece-edge::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 6px; height: 26px; border-radius: 3px;
  background: linear-gradient(180deg, #fbfcfd 0%, #e7e9ec 45%, #b7bcc4 100%);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.7);
}
.piece-edge::after {
  /* the || tell: two hairline slots in the bar */
  content: ''; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 3px; height: 12px;
  border-left: 1px solid rgba(10,11,13,.55);
  border-right: 1px solid rgba(10,11,13,.55);
}
.piece-edge:hover::before, .piece-edge.is-dragging::before {
  background: linear-gradient(180deg, #fceca3 0%, #ffd42e 55%, #d4a70f 100%);
  height: 32px;
}

/* ── Held-frame transition preview (item 4a) ──────────────────────────────
   The outgoing piece's last frame, held over the incoming video and animated
   out across the join. Same geometry as the seek mask; its own element so
   the two can be live at once. */
.transition-hold {
  position: absolute;
  top: var(--stage-pad); left: var(--stage-pad);
  width: calc(100% - var(--stage-pad) * 2);
  height: calc(100% - var(--stage-pad) * 2);
  object-fit: contain;
  pointer-events: none;
  visibility: hidden;
  z-index: 3;            /* over the video + seek mask, under captions/overlays */
}
.transition-hold.is-on { visibility: visible; }

/* Round 10, item 2: the INCOMING side of a join, so the transition can start
   BEFORE the cut instead of only after it. One frame of the next piece,
   grabbed once per join from a hidden peek video and cached, drawn over the
   live outgoing picture across the first half of the window. Identical
   geometry to .transition-hold; the two are never on at the same instant (one
   owns [join-d/2, join), the other [join, join+d/2)). */
.transition-incoming {
  position: absolute;
  top: var(--stage-pad); left: var(--stage-pad);
  width: calc(100% - var(--stage-pad) * 2);
  height: calc(100% - var(--stage-pad) * 2);
  object-fit: contain;
  pointer-events: none;
  visibility: hidden;
  z-index: 3;
}
.transition-incoming.is-on { visibility: visible; }
/* Never rendered, never laid out -- it exists only to be seeked and grabbed. */
.transition-peek {
  position: absolute; width: 2px; height: 2px;
  opacity: 0; pointer-events: none; left: -9999px; top: 0;
}

/* ── Corner-drag at the source boundary (item 1b) ─────────────────────────
   At zoom 1 the crop already spans the whole source; an outward corner drag
   has nothing to grow into. The frame edge pulses once so the limit reads as
   "the wall", not "the control died". */
@keyframes frame-limit-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(245,197,24,.0); }
  35%  { box-shadow: 0 0 0 3px rgba(245,197,24,.45); }
  100% { box-shadow: 0 0 0 0 rgba(245,197,24,.0); }
}
.crop-guide.is-at-limit {
  border-color: rgba(245,197,24,.95) !important;
  animation: frame-limit-pulse .7s ease-out infinite;
}

/* ── Transition picker (item 4b) ──────────────────────────────────────────
   Family tabs, one-click tiles with a live-motion glyph each, duration
   presets in the header. Same surface language as every ctx panel. */
.transition-picker { width: 292px; padding: var(--sp-2); }
.tp-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); padding: 2px 4px var(--sp-2);
  border-bottom: 1px solid var(--line); margin-bottom: var(--sp-2);
}
.tp-title { font: 600 10px/1 var(--font); letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.tp-durs { flex: none; padding: 2px; }
.tp-durs .seg-btn { padding: 4px 9px; font-size: 10px; }
.tp-tabs { display: flex; flex-wrap: wrap; gap: 2px; margin: var(--sp-2) 0; }
.tp-tab {
  appearance: none; border: 0; cursor: pointer;
  padding: 4px 8px; border-radius: 99px;
  font: 600 10px/1 var(--font); color: var(--text-dim); background: transparent;
}
.tp-tab:hover { color: var(--text); background: var(--surface-3); }
.tp-tab.is-active { color: var(--accent); background: rgba(245,197,24,.1); }
.tp-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px;
  max-height: 240px; overflow-y: auto;
}
.tp-grid--basics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.tp-tile {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 6px 4px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); cursor: pointer;
}
.tp-tile:hover { border-color: var(--line-strong); background: var(--surface-3); }
.tp-tile.is-selected { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
/* One line + ellipsis truncated "Fade Through Black/White/Gray" to the
   identical string "Fade Through…" on three neighbouring tiles -- three
   different transitions reading as one. Two lines fit every name in the
   list; anything longer still clamps, but never on the word that tells
   them apart. The full name is on the tile's own title attribute too. */
.tp-tile-label {
  font: 500 10px/1.2 var(--font); color: var(--text-dim);
  max-width: 100%; overflow: hidden; text-align: center;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow-wrap: anywhere;
}
.tp-tile.is-selected .tp-tile-label { color: var(--accent); }
.tp-approx {
  position: absolute; top: 2px; right: 4px;
  font: 10px/1 var(--mono); font-style: normal; color: var(--text-faint);
}
.tp-foot { margin-top: var(--sp-2); border-top: 1px solid var(--line); border-radius: 0 0 4px 4px; }

/* The glyph: a 30x18 stage; the <i> is the OUTGOING pane (silver), animating
   out over the gold incoming ground -- each family moves the way its export
   does. Pure CSS, ~2s loop, paused under reduced motion. */
.tglyph {
  position: relative; width: 30px; height: 18px; border-radius: 3px;
  overflow: hidden; flex: none;
  background: linear-gradient(180deg, rgba(245,197,24,.6), rgba(198,154,12,.6));
  display: flex; align-items: center; justify-content: center;
  font: 12px/1 var(--mono); color: var(--text-dim);
}
.tglyph i {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, #3a4048, #23272e);
  animation-duration: 1.8s; animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}
.tglyph--none { background: var(--surface-3); }
.tglyph--none i { display: none; }
.tglyph--black i { animation-name: tg-black; }
.tglyph--white i { animation-name: tg-white; }
.tglyph--cross i { animation-name: tg-cross; }
.tglyph--dim i { animation-name: tg-dim; }
.tglyph--slide-l i { animation-name: tg-slide-l; }
.tglyph--slide-r i { animation-name: tg-slide-r; }
.tglyph--slide-u i { animation-name: tg-slide-u; }
.tglyph--slide-d i { animation-name: tg-slide-d; }
.tglyph--wipe-l i { animation-name: tg-wipe-l; }
.tglyph--wipe-r i { animation-name: tg-wipe-r; }
.tglyph--wipe-u i { animation-name: tg-wipe-u; }
.tglyph--wipe-d i { animation-name: tg-wipe-d; }
.tglyph--split-h i { animation-name: tg-split-h; }
.tglyph--split-v i { animation-name: tg-split-v; }
.tglyph--circle i { animation-name: tg-circle; }
.tglyph--zoom i { animation-name: tg-zoom; }
.tglyph--squeeze-h i { animation-name: tg-squeeze-h; }
.tglyph--squeeze-v i { animation-name: tg-squeeze-v; }
@keyframes tg-black { 0%, 20% { opacity: 1; filter: none; } 50% { opacity: 1; filter: brightness(0); } 80%, 100% { opacity: 0; filter: brightness(0); } }
@keyframes tg-white { 0%, 20% { opacity: 1; filter: none; } 50% { opacity: 1; filter: brightness(4); } 80%, 100% { opacity: 0; filter: brightness(4); } }
@keyframes tg-cross { 0%, 25% { opacity: 1; } 75%, 100% { opacity: 0; } }
@keyframes tg-dim { 0%, 25% { opacity: 1; } 50% { opacity: .45; } 75%, 100% { opacity: 1; } }
@keyframes tg-slide-l { 0%, 25% { transform: none; } 75%, 100% { transform: translateX(-100%); } }
@keyframes tg-slide-r { 0%, 25% { transform: none; } 75%, 100% { transform: translateX(100%); } }
@keyframes tg-slide-u { 0%, 25% { transform: none; } 75%, 100% { transform: translateY(-100%); } }
@keyframes tg-slide-d { 0%, 25% { transform: none; } 75%, 100% { transform: translateY(100%); } }
@keyframes tg-wipe-l { 0%, 25% { clip-path: inset(0); } 75%, 100% { clip-path: inset(0 100% 0 0); } }
@keyframes tg-wipe-r { 0%, 25% { clip-path: inset(0); } 75%, 100% { clip-path: inset(0 0 0 100%); } }
@keyframes tg-wipe-u { 0%, 25% { clip-path: inset(0); } 75%, 100% { clip-path: inset(0 0 100% 0); } }
@keyframes tg-wipe-d { 0%, 25% { clip-path: inset(0); } 75%, 100% { clip-path: inset(100% 0 0 0); } }
@keyframes tg-split-h { 0%, 25% { clip-path: inset(0); } 75%, 100% { clip-path: inset(0 50%); } }
@keyframes tg-split-v { 0%, 25% { clip-path: inset(0); } 75%, 100% { clip-path: inset(50% 0); } }
@keyframes tg-circle { 0%, 25% { clip-path: circle(75% at 50% 50%); } 75%, 100% { clip-path: circle(0% at 50% 50%); } }
@keyframes tg-zoom { 0%, 25% { transform: scale(1); opacity: 1; } 75%, 100% { transform: scale(1.7); opacity: 0; } }
@keyframes tg-squeeze-h { 0%, 25% { transform: scaleX(1); } 75%, 100% { transform: scaleX(0); } }
@keyframes tg-squeeze-v { 0%, 25% { transform: scaleY(1); } 75%, 100% { transform: scaleY(0); } }
/* N4b: whip -- a hard pan left with a smear at the moment of the cut. */
.tglyph--whip i { animation-name: tg-whip; }
@keyframes tg-whip {
  0%, 25% { transform: none; filter: none; }
  50% { transform: translateX(-50%); filter: blur(2px); }
  75%, 100% { transform: translateX(-100%); filter: none; }
}

/* ── N4b: the join card ────────────────────────────────────────────────────
   Eight tiles and one length slider, nothing else. Wider tiles than the
   library grid so the glyph reads at a glance; the glyph runs its loop so
   each tile IS its own preview. */
.transition-picker--inline { width: auto; padding: 0; }
.tp-grid--card {
  grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px;
  max-height: none; overflow: visible;
}
.tp-grid--card .tp-tile { padding: 8px 4px 7px; gap: 6px; min-height: 64px; }
.tp-grid--card .tglyph { width: 40px; height: 24px; }
.tp-grid--card .tglyph--none {
  background: var(--surface-3);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
.tp-grid--card .tglyph--none i {
  display: block; inset: 0 auto 0 50%; width: 2px; margin-left: -1px;
  background: var(--text-dim); animation: none;
}
.tp-grid--card .tp-tile.is-selected .tglyph { box-shadow: 0 0 0 1px var(--accent); }
/* A still frame of the loop shows eight identical gold blocks, so each glyph
   also carries a fixed hint of what it does -- readable in a screenshot and
   the instant the card opens, before the loop has moved. */
.tp-grid--card .tglyph::after {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font: 600 12px/1 var(--font); color: rgba(255,255,255,.85);
  text-shadow: 0 1px 2px rgba(0,0,0,.6); pointer-events: none;
}
.tp-grid--card .tglyph--cross::after { content: 'A/B'; font-size: 10px; letter-spacing: .04em; }
.tp-grid--card .tglyph--black::after { content: '●'; color: #0a0b0d; text-shadow: none; }
.tp-grid--card .tglyph--white::after { content: '○'; }
.tp-grid--card .tglyph--slide-l::after { content: '←'; }
.tp-grid--card .tglyph--slide-r::after { content: '→'; }
.tp-grid--card .tglyph--zoom::after { content: '⤢'; }
.tp-grid--card .tglyph--whip::after { content: '»'; font-size: 14px; }
.tp-length {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--line);
}
.tp-length.is-off { opacity: .45; }
.tp-length-slider { flex: 1; min-width: 0; accent-color: var(--accent); cursor: pointer; }
.tp-length-slider:disabled { cursor: default; }
.tp-length-val {
  flex: none; min-width: 44px; text-align: right;
  font: 12px/1 var(--mono); color: var(--text);
}
.is-sheet .tp-grid--card .tp-tile { min-height: 58px; }
/* The seam's own marker: the same moving glyph, at badge size, in front of
   the name -- readable from the timeline without opening the card. */
.join-badge { display: inline-flex; align-items: center; gap: 5px; }
.join-badge-glyph { width: 18px; height: 11px; border-radius: 2px; }
@media (prefers-reduced-motion: reduce) {
  .tglyph i { animation: none; opacity: .5; }
  .crop-guide.is-at-limit { animation: none; box-shadow: 0 0 0 3px rgba(245,197,24,.45); }
}

/* ── The one help sheet ───────────────────────────────────────────────────
   Opened by the ? in the trim toolbar (which for eleven rounds looked like a
   button and had no click handler at all), and once, automatically, the first
   time a clip is opened in the editor. It carries the mental model in three
   sentences and every key the app actually binds -- and it carries them HERE,
   behind one control, rather than as permanent instructional copy in the
   chrome, which the design bar forbids. Escape, a second ?, or a click on the
   scrim closes it. */
.help-scrim {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(6, 7, 9, .62);
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-5);
}
.help-sheet {
  width: min(560px, 100%); max-height: 82vh; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  padding: var(--sp-5);
  /* Four type steps on this surface and no more -- 15 / 13 / 12 / 10.
     Without this the containers inherited the body's 13.5px and made a
     fifth, near-identical step. */
  font-size: 12px;
}
/* The once-ever first-run sheet only: it dims, but it does not intercept, so
   the gesture that dismisses it still lands on what the user aimed at. The
   sheet itself keeps its own clicks. See makeHelpPassthrough in editor.js.
   Declared AFTER .help-sheet on purpose: the round-11 design test reads the
   FIRST `.help-sheet {` block it finds, and that must be the real panel. */
.help-scrim.is-passthrough { pointer-events: none; background: rgba(6, 7, 9, .5); }
.help-scrim.is-passthrough .help-sheet { pointer-events: auto; }
/* The one gold moment on this surface: a hairline at the head, nothing else. */
.help-sheet-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--accent-dim);
}
.help-sheet-head h2 { font-size: 14px; margin: 0; }
.help-sheet-title-wrap { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
.sheet-close {
  flex: none; width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--line); border-radius: 50%;
  background: none; color: var(--text-dim); font-size: 14px; line-height: 1;
  cursor: pointer;
}
.sheet-close:hover { color: var(--text); border-color: var(--line-strong); }
/* Round 26e, defect #9: this carried the class name of a different sheet
   (help-close) and, lacking .btn, fell through to Chromium's default
   outline instead of the app's gold ring every other focusable control
   gets. Renamed to what it actually is; the ring is now explicit here
   instead of depending on a class it never had. */
.sheet-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.help-model { margin: var(--sp-4) 0 0; padding: 0; list-style: none; }
.help-model li {
  margin: 0 0 var(--sp-3); padding-left: var(--sp-4);
  border-left: 2px solid var(--line-strong);
  color: var(--text-dim); font-size: 12px; line-height: 1.55;
}
.help-model b { color: var(--text); font-weight: 600; }
.help-keys-title {
  margin: var(--sp-5) 0 var(--sp-2);
  font: 10px/1 var(--mono); letter-spacing: .1em; color: var(--text-faint);
}
.help-keys { display: grid; grid-template-columns: 130px 1fr; gap: var(--sp-2) var(--sp-4); }
.help-keys dt { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.help-keys dd { margin: 0; color: var(--text-dim); font-size: 12px; align-self: center; }
.help-keys kbd {
  display: inline-block; min-width: 20px; padding: 2px 6px;
  border: 1px solid var(--line-strong); border-radius: 4px;
  background: var(--surface-2); color: var(--text);
  font: 12px/1.2 var(--mono); text-align: center;
}
.help-keys-or { color: var(--text-faint); font-size: 12px; align-self: center; }
.help-gestures { margin: 0; padding-left: var(--sp-4); }
.help-gestures li { color: var(--text-dim); font-size: 12px; margin-bottom: var(--sp-1); }

/* ── Trash (round 14, D10) ───────────────────────────────────────────────
   Shares the help sheet's chrome so the app has one dialog, not two. What is
   different here is that one of the buttons destroys video, so it is the only
   thing on the surface allowed to carry the error colour, and it earns it only
   on hover -- a red row of buttons would read as an alarm on a screen whose
   normal state is "your work is safe". */
/* Round 7, #2: `.help-sheet` is itself the scroller (overflow-y:auto,
   max-height:82vh) and `.trash-body` was overflow:visible -- so with 124
   rows the WHOLE sheet grew to scrollHeight ~7371px and scrolled as one
   block. Scrolled to the bottom, the summary/Empty-trash row measured
   y=-6548, the Empty button y=-6554 and the close button y=-6649 -- the
   abort control for an irreversible purge could sit thousands of pixels
   off-screen. `emptyTrash()`'s progress row (and its Cancel) is inserted
   right after `.trash-summary-row`, so it inherited the same trap.
   Fixed by making `.trash-sheet` a fixed-height flex column that does NOT
   scroll itself -- only `.trash-body` (the 124 rows) scrolls, inside it.
   The head, the filter, the summary/progress row and the close button are
   now structurally outside the scrolling region, so they are on screen at
   every scroll position without needing position:sticky tricks at all. */
.trash-sheet {
  display: flex; flex-direction: column; overflow: hidden;
}
.trash-sheet .help-sheet-head,
.trash-sheet .trash-filter-row,
.trash-sheet .trash-summary-row,
.trash-sheet .trash-progress { flex: none; }
.trash-summary-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); margin-top: var(--sp-4);
}
/* Round 26c, defect #7: this is a sentence ("124 items · 14.8 GB still on
   this PC"), not an engine string -- ui-monospace is for those, not prose. */
.trash-summary {
  margin: 0;
  font: 10px/1.4 var(--font); letter-spacing: .04em; color: var(--text-faint);
}
.trash-body {
  margin-top: var(--sp-3);
  /* Round 7, #2: this is now the ONLY scrolling region in the trash sheet
     -- see the .trash-sheet flex-column rule above. flex + min-height:0 is
     what lets a flex child actually shrink and scroll instead of growing
     to fit its content and pushing the sheet's own max-height. */
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
}
.trash-empty {
  margin: var(--sp-5) 0; text-align: center;
  color: var(--text-faint); font-size: 12px;
}
.trash-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--line);
}
.trash-row:last-child { border-bottom: 0; }
.trash-row-text { min-width: 0; }
.trash-row-name {
  color: var(--text); font-size: 12px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Round 26c, defect #5: 106/124 rows clipped mid-timestamp at 1366px --
   "deleted Aug 26, 6:0..." -- because the one field that tells otherwise-
   identical rows apart sat last in a single ellipsized line. It now gets
   its own right-aligned column that is never allowed to truncate; only the
   name/count/size part on the left shrinks. Round 26c, defect #7: also off
   ui-monospace -- this is a sentence, not an engine string. */
.trash-row-meta {
  margin-top: 2px; max-width: 100%;
  display: flex; align-items: baseline; gap: var(--sp-3);
  font: 10px/1.3 var(--font); color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.trash-row-meta-main {
  min-width: 0; flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trash-row-meta-when { flex: none; white-space: nowrap; }
.trash-row-actions { flex: none; display: flex; gap: var(--sp-2); }
/* Round 26b made "Delete for good" loud (danger fill + border at rest) after
   the original bug: it sat at the exact tone of disabled text. Round 26c
   overseer arbitration -- a critic then measured the loud version as the
   ONLY bordered, filled, saturated control in the row, x124 rows, which is
   its own failure mode. Neither extreme wins:
     - "Put back" (safe, reversible) is now the VISIBLE one at rest --
       filled, bordered, full-strength text.
     - "Delete for good" (irreversible) is quiet at rest -- transparent fill
       and border -- but its TEXT is an unmistakable danger red, never the
       disabled grey (--text-faint, #656c77) and never --text-dim (the grey
       "Put back" itself used before this pass, #9aa1ab). It only gains the
       danger fill + border on hover/focus, where a deliberate action is
       already in progress.
   Net: the reversible action carries the weight; the irreversible one is
   quiet but never mistakable for "disabled". */
.trash-row-actions .btn-secondary {
  background: var(--surface-2); border: 1px solid rgba(255,255,255,.13); color: var(--text);
}
.trash-row-actions .btn-secondary:hover:not(:disabled) { background: var(--surface-3); border-color: var(--line-strong); }
.trash-purge {
  background: transparent; border-color: transparent; color: #ff8a8e;
}
.select-delete {
  background: transparent; border-color: transparent; color: #ff8a8e;
}
/* :not(:disabled) matches .btn-ghost:hover:not(:disabled)'s specificity
   (0,3,0 each) so this, being LATER in the sheet, actually wins the
   cascade -- without it .btn-ghost's plain :hover:not(:disabled) rule (also
   0,3,0 as of adding :not) silently overrode this and the button hovered
   to plain surface-3/text instead of the danger colour. */
.trash-purge:hover:not(:disabled), .trash-purge:focus-visible {
  background: rgba(242,85,90,.2); border-color: rgba(242,85,90,.65); color: #ff8a8e;
}
.select-delete:hover:not(:disabled), .select-delete:focus-visible {
  background: rgba(242,85,90,.2); border-color: rgba(242,85,90,.65); color: #ff8a8e;
}
/* #deleteProjectBtn is the same failure as the original trash-purge bug: a
   destructive control (permanently deletes a project) styled identically to
   a safe one via plain .btn-ghost (color: var(--text-dim), the same grey
   "Put back" used to wear). Same at-rest treatment as .trash-purge. */
#deleteProjectBtn { color: #ff8a8e; }
#deleteProjectBtn:hover:not(:disabled), #deleteProjectBtn:focus-visible {
  background: rgba(242,85,90,.2); color: #ff8a8e;
}

/* ── Trash filter (round 4) ────────────────────────────────────────────────
   124 rows, nine repeated names -- a search box in the sheet header, styled
   like every other text input in the app (#promptInput). Flat surface at
   rest; the gold ring only shows up on focus, same as everywhere else it's
   used -- this is not a second gold surface, it's the one systemic ring. */
.trash-filter-row { margin-top: var(--sp-3); position: relative; }
.trash-filter {
  width: 100%; height: 28px; padding: 0 var(--sp-6) 0 var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 12px;
}
.trash-filter::placeholder { color: var(--text-faint); }
.trash-filter:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* D6: the native search-cancel glyph is a saturated blue with no CSS hook --
   hidden outright, replaced by .trash-filter-clear below in the app's own
   neutral chip language. */
.trash-filter::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.trash-filter-clear {
  appearance: none; position: absolute; top: 50%; right: 6px;
  transform: translateY(-50%);
  /* Round 12, defect #8: measured 20x20 -- under the 24px touch-target
     floor. */
  width: 24px; height: 24px; padding: 0;
  display: grid; place-items: center;
  background: var(--surface-3); border: 1px solid var(--line-strong);
  border-radius: 999px; color: var(--text-dim);
  font: 600 14px/1 var(--font); cursor: pointer;
}
.trash-filter-clear:hover { color: var(--text); border-color: var(--line-strong); }
.trash-filter-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── emptyTrash() progress (round 4) ──────────────────────────────────────
   Swapped in for the summary row while the 124-request loop runs. Same
   type scale as the summary line it replaces (10px meta text). */
.trash-progress {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); margin-top: var(--sp-3);
}
.trash-progress-text {
  margin: 0; font: 10px/1.4 var(--font); letter-spacing: .04em;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}
.trash-progress-cancel { flex: none; }

/* ── Typed-confirm sheet (round 4) ────────────────────────────────────────
   Replaces window.prompt() for the two destructive confirms in the Trash
   sheet (single purge, empty all). Same help-scrim/help-sheet chrome as
   every other dialog; stacks on top of the trash sheet it was opened from.
   The gate is unchanged -- required text, refusal, cancel -- only the
   chrome moved off a browser-native prompt(). */
.confirm-sheet { width: min(420px, 100%); }
.confirm-message {
  margin: var(--sp-4) 0 0;
  font-size: 12px; line-height: 1.55; color: var(--text-dim);
  /* Round 11, defect #7: the default `normal` collapses batchDelete()'s own
     "\n\n• …\n" bullet list into one prose run -- "…and 3 more This removes"
     reads as one sentence. pre-line keeps the newlines the code writes
     without breaking the auto-wrap of long lines the way `pre` would. */
  white-space: pre-line;
}
.confirm-label {
  display: block; margin-top: var(--sp-4);
}
.confirm-label span {
  display: block; margin-bottom: var(--sp-2);
  font-size: 10px; letter-spacing: .04em; color: var(--text-faint);
}
.confirm-input {
  width: 100%; height: 32px; padding: 0 var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--text); font: inherit; font-size: 14px;
}
.confirm-input::placeholder { color: var(--text-faint); opacity: .6; }
.confirm-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.confirm-actions {
  display: flex; justify-content: flex-end; gap: var(--sp-3);
  margin-top: var(--sp-5);
}

/* ── Round 16: the laptop dock ───────────────────────────────────────────────
   Measured at 1366x768: the dock showed 170px of 337px, so .zoom-lane,
   .speed-lane, .ov-lane and .track-scale all sat below its fold -- and the
   SPEED lane is the only way to change speed, so the feature was behind a
   fold. The dock DOES scroll, but half the editing surface behind a 170px box
   is a laptop failure, not a scrolling problem.

   Compact keeps every lane, draws it shorter, and lets the dock take a little
   more of the window. The base ceiling (round 13's budget, 45vh - 175px) is
   untouched -- this is a second ceiling that only applies while compact is on,
   and --dock-h still wins in both, so a splitter drag is never overridden.
   Measured after: dock clientHeight 246 vs scrollHeight 235 -- nothing below
   the fold, and the stage keeps 464px of a 768px window. */
.timeline-dock.is-compact {
  max-height: max(var(--dock-h, 0px), clamp(180px, calc(62vh - 230px), 520px));
}
.timeline-dock.is-compact .player-side { padding: 2px var(--sp-3); row-gap: 2px; }
.timeline-dock.is-compact .trimmer-head { margin-bottom: 2px; }
.timeline-dock.is-compact .track { height: 40px; }
.timeline-dock.is-compact .audio-lane { height: 26px; margin-top: 2px; }
.timeline-dock.is-compact .zoom-lane,
.timeline-dock.is-compact .speed-lane,
.timeline-dock.is-compact .ov-lane { height: 16px; margin-top: 2px; }
.timeline-dock.is-compact .track-scale { margin-top: 2px; }
.timeline-dock.is-compact .export-row { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }

/* The collapse control is a word plus its chevron, not a bare glyph at the end
   of a tab strip (which reads as "more tabs"). */
.panel-collapse { display: inline-flex; align-items: center; gap: 4px; }
/* The word costs ~36px of a 420px strip, which the five tabs were already
   nearly filling (measured: 454px of tabs+control in a 420px strip, the Media
   tab overlapped by 30px). Tighter tab padding pays for it: 5 tabs x 8px = 40.
   Measured after: scrollWidth 414 in a 420px strip, nothing clipped. */
.panel-tabs .panel-tab { padding-left: var(--sp-2); padding-right: var(--sp-2); }
.panel-collapse-label {
  font: 500 10px/1 var(--font); letter-spacing: .06em; text-transform: uppercase;
}
.panel-collapse-chevron { font-size: 14px; line-height: 1; }

/* ── Round 24: on a TALL window the timeline stops being a hole ────────────
   Quincy, at 2000x1160, ringed the black above and below the timeline in red.
   Measured on project 124 / clip 414 before this block: the timeline column
   (.panel-body--timeline) was 968px tall holding 305px of content -- 321.5px
   of dead black above it and 329.5px below, 651px in all.

   The cause was mine, from round 23: `justify-content: center` on the trimmer
   plus ceilings of 260px on #track and 180px on #audioLane. At 1366x768 the
   slack is ~130px and centring reads as balance; at 2000x1160 the slack is
   ~650px and the same rule reads as two holes.

   Deleting the ceilings is not the fix. The filmstrip is fetched at the lane's
   own height (?h=260 on this clip) and laid out in SOURCE TIME across the
   width with object-fit:fill, so 60 frames share the column's width no matter
   how tall the lane is: extra height buys vertical smear, not information. A
   600px waveform is the same trade. The lanes have an honest size and it is
   not "whatever is left over".

   So the slack is not the timeline's to fill, and the answer is to stop
   handing it to the timeline. On a tall window the right-hand side becomes ONE
   column: the Post card on top, where POST already sits in his mental map, and
   the timeline across the bottom with its export bar under it -- the shape of
   every NLE, and still BESIDE the picture, which is what round 19 was for. The
   dock is sized by its own content, so nothing in it is stretched and nothing
   is centred against a void. What is left over lands in the Post pane, which
   really is thin on this clip (one line of caption, four chips, a rationale --
   166px of card). That is a MARKUP problem, not a CSS one; it needs something
   more to say, and I do not own editor.js.

   Gated on min-height:900px so 1366x768 -- where the round 23 numbers were
   right -- is not touched at all. */
@media (min-width: 1200px) and (min-height: 900px) {
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) {
    /* Round 24b: this was `minmax(0,1fr) auto` -- the Post card got the whole
       first row and 148px of caption sat in 702px of black. The hole had not
       closed, it had MOVED. Post is sized by what it says; the dock takes
       everything else and grows UP from the export bar. */
    grid-template-rows: auto minmax(0, 1fr);
  }
  /* The left of the window is unchanged and must span both rows. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .rail,
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) #splitRail,
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .stage {
    grid-row: 1 / -1;
  }
  /* Post on top, timeline underneath it, both across the whole right-hand
     side. The splitter between them had a column to drag and no longer does. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) #splitPanel {
    display: none;
  }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .editor {
    grid-row: 1; grid-column: 5 / -1;
    border-left: 1px solid var(--line-strong);
  }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .timeline-dock {
    grid-row: 2; grid-column: 5 / -1;
    border-top: 1px solid var(--line-strong);
    border-left: 1px solid var(--line-strong);
  }
  /* A card 166px tall in a pane 1400px wide should not set its lines 200
     characters long. Measure first, width second. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .post-card > * {
    max-width: 78ch;
  }
  /* Inside a content-sized dock there is no free space to distribute, so the
     lanes state their heights outright instead of flexing into a void. 72px
     of filmstrip and 84px of waveform are the SAME ceilings a 1366x768 laptop
     reaches through max-height -- every window gets the same slim track, which
     is what round 25 corrected: round 24 wrote 260/180 here and that is the
     smear described above, shipped at full size. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .panel-body--timeline > .trimmer {
    justify-content: flex-start;
  }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .panel-body--timeline #track {
    flex: 0 0 auto; height: 72px; max-height: none;
  }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.panel-collapsed):not(.panel-empty):has(.player-frame.ar-portrait) .panel-body--timeline #audioLane {
    flex: 0 0 auto; height: 84px; max-height: none;
  }
}

/* Same window, POST hidden (the HIDE control sets .panel-collapsed, and the
   min-1400 block above moves the dock into the column the panel vacated).
   There is no Post card to hand the slack to there, so the timeline simply
   stays where it is -- content-sized, along the bottom of its column, with its
   export bar under it -- and toggling HIDE no longer makes it jump. */
@media (min-width: 1400px) and (min-height: 900px) {
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) .timeline-dock {
    align-self: stretch;
  }
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) .panel-body--timeline > .trimmer {
    justify-content: flex-start;
  }
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) .panel-body--timeline #track {
    flex: 0 0 auto; height: 72px; max-height: none;
  }
  .layout.panel-collapsed:not(.simple-mode):not(.view-board):not(.stage-expanded):not(.focus) .panel-body--timeline #audioLane {
    flex: 0 0 auto; height: 84px; max-height: none;
  }
}

/* ── Round 24b: the leftover height gets the clip's own words ──────────────
   Round 24 moved the timeline to the bottom of the right-hand column and I
   reported the timeline's own dead space as 8px. Measured across the WHOLE
   column it was still 555px, in one gap between the Post card and the dock:
   the hole had moved, not closed. Quincy has called that out before, and
   "the emptiness moved" reads to him as not fixed.

   The row split above hands the leftover to the dock instead of the Post
   card. That alone does not fix it -- it only moves the same void inside the
   dock -- because the arithmetic is brutal: at 2000x1160 the column is 1108px
   and this clip's real content is a 148px Post card plus a ~406px dock.
   ~550px has to come from somewhere, and there are only three places it can
   come from:
     - stretch the lanes: a 60-frame filmstrip drawn with object-fit:fill gets
       taller, not more informative, and a 600px waveform is the same trade.
       Refused; that is the "absurd lane".
     - pad it out: padding a card around emptiness is the void with a border.
     - show something true that is already one click away.
   The third is the only honest one. On a window this tall the dock keeps the
   timeline at its honest height and fills the rest with the TRANSCRIPT panel,
   which is already in the DOM, already scrolls itself, and is the thing every
   editor puts beside a timeline. Nothing is stretched and nothing is invented.

   Scoped to the timeline tab being the visible one, so choosing Transcript
   (or Captions, or Media) still gives that panel the whole dock, and gated on
   min-height:900px so 1366x768 keeps the single-panel behaviour it had. */
@media (min-height: 900px) {
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded) .timeline-dock .panel-body--timeline,
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded) .timeline-dock .panel-body--timeline > .trimmer {
    flex: 0 0 auto;
  }
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded) .timeline-dock .panel-body--timeline:not([hidden]) ~ .panel-body[data-panel="transcript"] {
    /* `!important` because line 42 of this file is `[hidden]{display:none
       !important}` and the tab switcher hides panels with the attribute.
       Nothing weaker reaches it. */
    display: flex !important;
    flex: 1 1 0;
    min-height: 0;
    border-top: 1px solid var(--line);
  }
  /* Same rule round 24 applied to the Post card, for the same reason and with
     a second payoff. A 1370px-wide pane sets the transcript 200 characters to
     the line, which nobody can read and which flattens a 40-second clip into
     two lines with 300px of black under them. At a 78ch measure the same words
     are readable AND they occupy the pane they were given. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded) .timeline-dock .panel-body[data-panel="transcript"] #words {
    max-width: 78ch;
  }
}

/* ── Round 24c: the transcript only earns the slack when it HAS words ──────
   Round 24b handed the leftover height to the transcript panel. Measured on
   clip 2303 at 2000x1160, that made it worse, not better: this clip has no
   transcript yet, so the panel opened to 803px holding one grey sentence --
   the largest gap in the dock went 267px -> 669px, and #track/#audioLane,
   released from their flex, collapsed 260/180 -> 56/40. The timeline got
   SMALLER on the bigger window while a new hole opened under it. That is the
   same "the emptiness moved" failure 24b was written to fix, reintroduced one
   element to the right.

   The panel's own empty state is a real element (#words > p.editor-empty, set
   by setEditorPlaceholder), so the sheet can ask the question directly rather
   than assume. With words: 24b's behaviour, which is the right one -- the
   dock fills with the clip's own text. Without: the rule stands down entirely
   and the lanes keep the flex they had before 24b, so the worst case is the
   layout we already shipped rather than a bigger hole. */
@media (min-height: 900px) {
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded)
    .timeline-dock:has(.panel-body[data-panel="transcript"] .editor-empty) .panel-body--timeline,
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded)
    .timeline-dock:has(.panel-body[data-panel="transcript"] .editor-empty) .panel-body--timeline > .trimmer {
    flex: 1 1 auto;
  }
  /* `!important` to beat 24b's own `display:flex !important` above. */
  .layout:not(.simple-mode):not(.view-board):not(.stage-expanded)
    .timeline-dock:has(.panel-body[data-panel="transcript"] .editor-empty)
    .panel-body--timeline:not([hidden]) ~ .panel-body[data-panel="transcript"] {
    display: none !important;
  }
}

/* ── The ruler measures the lanes UNDER it, so it sits above them ──────────
   `.track-scale` was the last element in the trimmer, below the audio lane:
   numbers with nothing beneath them read as a footer, not as the axis the
   blocks are placed against. Opus, CapCut and Premiere all put it on top,
   attached to the tracks it labels. The element and its absolute-positioned
   spans are unchanged -- only its place in the column moved, so every x the
   JS computes still lands where it did.

   Its 4px of separation has to move with it: `margin-top` opened a gap above
   the ruler and left it welded to the lane below, which is exactly backwards
   now. */
.panel-body--timeline .track-scale,
.trimmer > .track-scale {
  margin-top: 0;
  margin-bottom: var(--sp-1);
}
.timeline-dock.is-compact .track-scale {
  margin-top: 0;
  margin-bottom: 2px;
}

/* ── Sound repair (app/audiofix.py) ─────────────────────────────────────────
   Numbers first, switch second. The measurement is the argument for the
   control existing, so it is shown as three plain readouts he can check
   rather than a grade or a meter -- and the one number that means "this is
   broken" (a loudest point above full scale) is the only one that takes the
   gold. Palette rule holds: black + silver, ONE accent, and it is spent here
   on the fact, not on the buttons. */
.repair-group { display: grid; gap: var(--sp-2); }
.repair-note { margin: 0; line-height: 1.45; max-width: 52ch; }

.repair-facts {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px; background: var(--line); border: 1px solid var(--line);
  border-radius: var(--r-sm); overflow: hidden;
  /* Three short numbers do not need a 1000px row. Capped so they read as one
     group instead of three things drifting apart on a wide dock. */
  max-width: 520px;
}
.repair-fact { display: grid; gap: 2px; padding: 8px 10px; background: var(--surface); }
.repair-fact-key {
  font-size: 10px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-faint);
}
.repair-fact-value { font-size: 17px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
/* Above full scale: the one thing on this panel that is actually wrong. */
.repair-fact--over .repair-fact-value { color: var(--accent); }
.repair-fact--over .repair-fact-value::after {
  content: ' over'; font-size: 10px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--accent-dim);
}

.repair-choices { display: flex; flex-wrap: wrap; gap: 4px; }
.repair-choice {
  appearance: none; cursor: pointer; padding: 6px 11px;
  font: inherit; font-size: 12px; font-weight: 500;
  color: var(--text-dim); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 999px;
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.repair-choice:hover { color: var(--text); border-color: var(--line-strong); }
.repair-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.repair-choice.is-active {
  color: var(--text); background: var(--surface-3);
  border-color: var(--line-strong); font-weight: 600;
}
/* The selected one gets a mark rather than a fill: on a row of pills a filled
   pill and a hovered pill are the same shape, and he has to be able to see
   which one is ON without moving the mouse away to check. */
.repair-choice.is-active::before {
  content: ''; display: inline-block; width: 5px; height: 5px;
  margin-right: 6px; vertical-align: middle; border-radius: 50%;
  background: var(--accent);
}
.repair-resolved { margin: 0; font-size: 10px; line-height: 1.45; max-width: 52ch; }

/* ── Screen 1 (REDESIGN-BRIEF, 2026-09-06): nothing selected = the clip, big,
   and Export. That is the whole screen. Display rules over the same DOM: every
   lane, panel and tab still exists with its state; "Edit" brings them back. */
.layout.focus:not(.view-board) { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
.layout.focus:not(.view-board) .rail,
.layout.focus:not(.view-board) .editor,
.layout.focus:not(.view-board) .splitter,
.layout.focus:not(.view-board) .timeline-dock,
.layout.focus:not(.view-board) .rail-reveal,
.layout.focus:not(.view-board) .stage-strip,
.layout.focus:not(.view-board) .stage-toggles > :not(#focusToggleBtn) { display: none; }
body:has(.layout.focus:not(.view-board)) .panel-reveal,
body:has(.layout.focus:not(.view-board)) .rail-reveal { display: none; }
.layout.focus:not(.view-board) .stage {
  grid-row: 1 / -1; grid-column: 1; border-right: 0;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.layout.focus:not(.view-board) .player-frame.ar-portrait,
.layout.focus:not(.view-board) .player-frame.vertical { max-width: min(100%, calc((100vh - var(--topbar-h) - 190px) * 9 / 16)); }
.layout.focus:not(.view-board) .player-frame.ar-square { width: min(100%, calc(100vh - var(--topbar-h) - 190px)); }
.layout.focus:not(.view-board) .player-frame.ar-landscape { width: min(100%, calc((100vh - var(--topbar-h) - 190px) * 16 / 9)); }
.focus-bar { display: flex; justify-content: center; padding: var(--sp-3) var(--sp-4) var(--sp-4); }
.focus-bar .export-row { width: min(100%, 520px); }
.focus-bar .export-actions .caption-style { display: none; }
.layout.view-board #focusToggleBtn { display: none; }
/* The scrubber and the Export button belong to the picture, not the window. */
.layout.focus:not(.view-board) .player-frame.ar-portrait ~ .scrub-row,
.layout.focus:not(.view-board) .player-frame.vertical ~ .scrub-row { width: min(100%, calc((100vh - var(--topbar-h) - 190px) * 9 / 16)); margin: 0 auto; }
.layout.focus:not(.view-board) .player-frame.ar-square ~ .scrub-row { width: min(100%, calc(100vh - var(--topbar-h) - 190px)); margin: 0 auto; }
.layout.focus:not(.view-board) .player-frame.ar-landscape ~ .scrub-row { width: min(100%, calc((100vh - var(--topbar-h) - 190px) * 16 / 9)); margin: 0 auto; }
.layout.focus .focus-bar .export-row { background: transparent; border: 0; box-shadow: none; padding: 0; width: auto; }
.layout.focus .focus-bar .export-actions { justify-content: center; }
.layout.focus .focus-bar #exportBtn { flex: 0 0 auto; min-width: 280px; }
.layout.focus .focus-bar .export-status:empty { display: none; }

/* ── Screen 2 (REDESIGN-BRIEF): the pieces live on the scrubber; tap one for its tools ── */
.scrub-pieces { position: absolute; inset: 0; pointer-events: none; }
.layout.focus:not(.view-board) .scrub-pieces { pointer-events: auto; }
.scrub-piece {
  position: absolute; top: 0; bottom: 0;
  border-left: 2px solid rgba(231,233,236,.55);
  box-sizing: border-box; cursor: pointer;
}
.scrub-piece.is-first { border-left-color: transparent; }
.scrub-piece::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 4px;
  transform: translateY(-50%); border-radius: 99px; pointer-events: none;
  box-shadow: 0 0 0 0 transparent; transition: box-shadow .12s ease;
}
.scrub-piece:hover::after { box-shadow: 0 0 0 2px rgba(231,233,236,.28); }
.scrub-piece.is-selected::after { box-shadow: 0 0 0 2px var(--accent), 0 0 10px rgba(245,197,24,.45); }
.scrub-piece.is-shaped::before {
  content: ""; position: absolute; left: 4px; right: 4px; bottom: -1px; height: 2px; border-radius: 2px;
  background: rgba(231,233,236,.45);
}
.scrub-piece-rate {
  position: absolute; left: 50%; top: -13px; transform: translateX(-50%);
  font: 10px/1 var(--mono); color: var(--accent); white-space: nowrap;
  font-variant-numeric: tabular-nums; pointer-events: none;
}
.piece-tools {
  position: absolute; left: 50%; transform: translateX(-50%); z-index: 8;
  box-sizing: border-box; width: min(100%, 520px);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: rgba(21,24,29,.96); border: 1px solid var(--line-strong); border-radius: 10px;
  box-shadow: 0 14px 40px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.06);
  backdrop-filter: blur(6px);
  display: flex; flex-direction: column; gap: var(--sp-3);
}
.piece-tools[hidden] { display: none; }
.piece-tools::after {
  content: ""; position: absolute; bottom: -7px; left: var(--caret-x, 50%); width: 12px; height: 12px;
  transform: translateX(-6px) rotate(45deg);
  background: rgb(21,24,29); border-right: 1px solid var(--line-strong); border-bottom: 1px solid var(--line-strong);
}
.piece-tools-head { display: flex; align-items: baseline; gap: var(--sp-2); }
.piece-tools-title { font-weight: 600; }
.piece-tools-close { margin-left: auto; }
.piece-tools .speed-group, .piece-tools .piece-tools-sound { margin: 0; }
.piece-tools-sound { flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: var(--sp-2); }
/* N3 / FIX-5: Mute and Fade shape joined this section. .panel-group wraps, and
   a wrapping COLUMN inside the phone sheet (whose height is capped) broke
   into three columns and pushed the last control off the right edge. One
   column, always; the sheet scrolls if it must. */
.piece-tools-sound > * { flex: 0 0 auto; width: 100%; }
.layout.focus:not(.view-board) .player-frame.ar-portrait ~ .piece-tools,
.layout.focus:not(.view-board) .player-frame.vertical ~ .piece-tools { width: min(100%, calc((100vh - var(--topbar-h) - 190px) * 9 / 16)); }
.layout.focus:not(.view-board) .player-frame.ar-square ~ .piece-tools { width: min(100%, calc(100vh - var(--topbar-h) - 190px)); }
.layout.focus:not(.view-board) .player-frame.ar-landscape ~ .piece-tools { width: min(100%, calc((100vh - var(--topbar-h) - 190px) * 16 / 9)); }
.layout:not(.focus) .piece-tools, .layout.view-board .piece-tools { display: none; }

/* ── Screen 3 (REDESIGN-BRIEF): joins on the scrubber + the join card ── */
.scrub-join {
  position: absolute; top: -4px; bottom: -4px; width: 16px; margin-left: -8px;
  cursor: pointer; z-index: 2;
}
.scrub-join::before {
  content: ""; position: absolute; left: 50%; top: 50%; width: 8px; height: 8px;
  margin: -4px 0 0 -4px; transform: rotate(45deg); border-radius: 1px;
  background: rgba(231,233,236,.6); box-shadow: 0 0 0 0 transparent;
  transition: background .12s ease, box-shadow .12s ease;
}
.scrub-join:hover::before { background: var(--text); box-shadow: 0 0 0 2px rgba(231,233,236,.28); }
.scrub-join.has-transition::before {
  background: linear-gradient(180deg, #fae584 0%, #f5c518 55%, #c69a0c 100%);
  box-shadow: 0 0 6px rgba(245,197,24,.5);
}
.scrub-join.is-selected::before { box-shadow: 0 0 0 2px var(--accent), 0 0 10px rgba(245,197,24,.45); }
.scrub-join-label {
  position: absolute; left: 50%; top: -11px; transform: translateX(-50%);
  font: 10px/1 var(--mono); color: var(--accent); white-space: nowrap; pointer-events: none;
}
.join-tools { width: min(100%, 400px); }
.join-tools-now { font-variant-numeric: tabular-nums; }
.join-tools .transition-picker--inline { width: auto; padding: 0; }
.join-tools .tp-grid { max-height: 200px; }
/* While its card is open the card head already names the transition; the label under the caret would only peek out. */
.scrub-join.is-selected .scrub-join-label { visibility: hidden; }

/* ── Screen 4 (REDESIGN-BRIEF): tap the words → their caption controls ───── */
.caption-tools { width: min(100%, 560px); }
/* Live words occupy their own stable slot, never move the controls below them. */
.caption-tools-selection {
  display: flex; flex-direction: column; gap: var(--sp-2);
  block-size: 208px; flex: 0 0 208px; min-block-size: 0;
  overflow-y: auto; overflow-anchor: none; scrollbar-gutter: stable;
}
.caption-tools-selection > * { flex-shrink: 0; }
.caption-tools { overflow-anchor: none; }

/* PA2: the Post card inside its tool card. The card keeps its own styles;
   only the panel dressing (border, background, the Hide control) comes off. */
.post-tools { width: min(100%, 560px); }
.post-tools .post-card { padding: 0; border: 0; background: transparent; }
.post-tools .post-card .panel-collapse { display: none; }
.post-tools .post-card-head h3 { display: none; }
#postToolsBtn.is-on { color: var(--accent); }
.caption-tools-now { font-variant-numeric: tabular-nums; }
.caption-tools-words { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2); }
.caption-tools-word {
  padding: 3px 9px; border: 1px solid var(--line-strong); border-radius: 6px;
  background: var(--surface); font-weight: 600; cursor: text;
  transition: border-color .12s ease, color .12s ease;
}
.caption-tools-word:hover { border-color: var(--text); }
.caption-tools-word.is-active { border-color: var(--accent); color: var(--accent); }
.caption-tools-word:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.caption-tools-word-hide {
  margin: 0 -4px 0 6px; padding: 0 4px; font: inherit; font-size: 14px; line-height: 1;
  color: var(--text-dim); background: transparent; border: 0; cursor: pointer; border-radius: 4px;
}
.caption-tools-word-hide:hover, .caption-tools-word-hide:focus-visible { color: var(--accent); outline: none; }
.caption-tools-remove-line { align-self: flex-start; }
.caption-tools-restore { margin-left: 4px; color: var(--accent); }
.caption-tools-line { display: flex; flex-direction: column; gap: var(--sp-1); }
.caption-tools-line .caption-line-input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 14px; padding: 8px 10px;
  color: var(--text); background: var(--bg-2, rgba(255,255,255,0.04)); border: 1px solid var(--line, rgba(255,255,255,0.14)); border-radius: 8px;
}
.caption-tools-line .caption-line-input:focus { outline: none; border-color: var(--accent); }
.caption-tools-line-hint { font-size: 11px; }
.caption-tools .w-input { font-weight: 600; }
.caption-tools-timing { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.caption-tools-timing .panel-group-title { margin-right: var(--sp-2); }
.caption-tools-styles { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.caption-tools-styles .preset-tile { padding: 4px; gap: 4px; }
.caption-tools-styles .preset-tile-label { font-size: 10px; }
.caption-tools .caption-colors, .caption-tools .caption-plain {
  padding: var(--sp-2) 0 0; border: 0; background: transparent; min-height: 0;
  border-top: 1px solid var(--line); flex-wrap: wrap;
}
/* The tapped words wear the same gold outline a hovered/dragged caption does,
   so "selected" reads on the clip itself while the card is open. */
.cap-layer.is-tools-open .cap-block { outline-color: rgba(245,197,24,.75); background: rgba(245,197,24,.05); }
/* Below the words (they sit high): the caret flips to the card's top edge. */
.caption-tools.is-below::after { bottom: auto; top: -7px; transform: translateX(-6px) rotate(225deg); }
.caption-tools-timing .seg-btn { padding: 6px 9px; min-width: 0; }   /* 24px floor kept, narrower */
.caption-tools .caption-colors { gap: var(--sp-2); }
.caption-tools .caption-colors .panel-group-title { margin-right: 0; }

/* Screen 5: the picture card. */
.picture-tools { width: min(100%, 560px); }
.picture-tools-now { font-variant-numeric: tabular-nums; }
.picture-tools .frame-readout { margin: 0; }
.picture-tools-framing { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.picture-tools-framing .panel-group-title { margin-right: var(--sp-1); }
.picture-tools-framing .frame-row { flex: 1 1 100%; flex-wrap: wrap; }
.picture-tools-hint { flex: 1 1 100%; margin: 0; }
.picture-tools-punch { display: flex; flex-direction: column; gap: var(--sp-2); }
.picture-tools-punch .panel-group { margin: 0; }
.scrub-piece.is-punched::after { background-image: linear-gradient(90deg, transparent, rgba(245,197,24,.35), transparent); }
.picture-tools.is-below::after { bottom: auto; top: -7px; transform: translateX(-6px) rotate(225deg); }
.player-frame.is-tools-open { box-shadow: 0 0 0 2px var(--accent), 0 0 14px rgba(245,197,24,.35); }
/* Screen 5: in focus mode the whole-source view (Full frame unpressed, the one
   view where you move the crop) takes the SOURCE's own shape and width, not the
   export's -- the portrait cap above turned a 16:9 source into a 240px strip
   with the gold frame 84px wide. Same height budget as the finished view. */
.layout.focus:not(.view-board) .player-frame.is-source { max-width: min(100%, calc((100vh - var(--topbar-h) - 190px) * var(--source-ar, 16 / 9))); }
.layout.focus:not(.view-board) .player-frame.is-source ~ .scrub-row { width: min(100%, calc((100vh - var(--topbar-h) - 190px) * var(--source-ar, 16 / 9))); margin: 0 auto; }

/* ── Screen 6: in the editor the clip list is a DRAWER over the picture ──
   Hidden by default (setView hides it on entering the editor); the CLIPS tab
   at the left edge brings it back to pick another clip, and picking one, the
   Hide button, or a click anywhere else puts it away. Before this it came back
   as a 230px grid column at wide widths -- the picture shrank -- and NOT AT
   ALL under 1180px (Quincy's deck dock): the narrow rule kept it display:none
   and the tab went away with it, a dead end. Hidden, not removed: it is the
   same #rail / #clipList, same list, same Import. */
.layout:not(.view-board):not(.rail-hidden) { --rail-w: 0px !important; }
.layout:not(.view-board):not(.rail-hidden) #splitRail { display: none; }
.layout:not(.view-board):not(.rail-hidden) .rail {
  display: flex !important;
  position: fixed; left: 0; top: var(--topbar-h); bottom: 0;
  width: min(300px, 86vw); z-index: 45;
  border-right: 1px solid var(--line-strong);
  box-shadow: 14px 0 44px rgba(0, 0, 0, .6);
  animation: rail-slide-in .18s ease-out;
}
@keyframes rail-slide-in { from { transform: translateX(-24px); opacity: 0; } }

/* ── Clean speech in the words card + its marks on the clip ─────────────── */
.caption-tools-clean { display: flex; flex-direction: column; gap: 8px; align-items: stretch; text-align: left; }
.caption-tools-clean .panel-group-title { text-align: left; align-self: flex-start; }
/* P1: switches read left-to-right like every other row; the count sits at the right edge. */
.caption-tools-clean .clean-line { display: flex; align-items: center; justify-content: flex-start; gap: 10px; }
.caption-tools-clean .clean-line .clean-note { margin-left: auto; }
.caption-tools-clean .clean-sub { padding-left: 14px; border-left: 2px solid var(--line); margin-top: 2px; }
.caption-tools-clean .clean-row { font-size: 12px; color: var(--text); }
.caption-tools-clean .clean-note { font: 10px/1 var(--mono); color: var(--text-faint); white-space: nowrap; }
.caption-tools-clean .clean-amount { display: flex; flex-direction: column; gap: 4px; padding: 2px 0 4px 30px; }
.caption-tools-clean .clean-amount-label { font-size: 12px; color: var(--text-dim); }
.caption-tools-clean .clean-amount input[type=range] { width: 100%; accent-color: var(--accent); cursor: pointer; }
.scrub-cut, .scrub-mute {
  position: absolute; top: 0; bottom: 0; pointer-events: none; z-index: 3;
  border-radius: 1px;
}
.scrub-cut { background: repeating-linear-gradient(135deg, rgba(0,0,0,.72) 0 2px, rgba(255,255,255,.14) 2px 3px); }
.scrub-mute { background: rgba(255, 96, 96, .9); box-shadow: 0 0 0 1px rgba(0,0,0,.5); }

/* Quincy, 2026-09-07: "the title of the clip jammed into the top left corner
   also looks cheap". In focus mode the stage head is not a page header: it is
   the picture's caption. Centred over the picture, quiet, no rule under it;
   "Edit" (the way into the deeper tools) fades back until pointed at. */
.layout.focus:not(.view-board) .stage-head {
  position: relative; justify-content: center; border-bottom: 0;
  padding: var(--sp-3) calc(var(--sp-4) + 64px) var(--sp-1);
}
.layout.focus:not(.view-board) .stage-head > div:first-child { min-width: 0; text-align: center; }
.layout.focus:not(.view-board) .stage-head h1 {
  font-size: 14px; font-weight: 500; letter-spacing: 0.02em; color: var(--text-dim);
  -webkit-line-clamp: 1; max-width: 60ch; margin: 0 auto;
}
.layout.focus:not(.view-board) .stage-toggles { position: absolute; right: var(--sp-4); top: 50%; transform: translateY(-50%); }
.layout.focus:not(.view-board) #focusToggleBtn { opacity: 0.5; transition: opacity 120ms ease; }
.layout.focus:not(.view-board) #focusToggleBtn:hover,
.layout.focus:not(.view-board) #focusToggleBtn:focus-visible { opacity: 1; }

/* ═══ EDITOR REVAMP (BUILD-PLAN priority #1) — the CapCut/Opus shell ═══════
   Focus = the editor. Big picture on top, one transport line, and the REAL
   timeline full width across the bottom, always visible. The knob playhead
   stands above the ruler and drops a line through every lane. The old scrub
   strip under the picture is retired here (kept for the non-focus layout). */
.layout.focus:not(.view-board) .stage { grid-template-rows: auto minmax(0, 1fr) auto; }
.layout.focus:not(.view-board) .scrub-row { display: none; }
.layout:not(.focus) .focus-deck, .layout.view-board .focus-deck { display: none; }
.focus-deck {
  display: flex; flex-direction: column;
  border-top: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(255,255,255,.02), transparent 40%), var(--surface);
}
.focus-deck[hidden] { display: none; }
/* Picture budget: the deck is ~230px tall (transport 56 + ruler 22 + track 72 + lanes/padding). */
.layout.focus:not(.view-board) { --focus-chrome: 262px; --pic-h: calc(100vh - var(--topbar-h) - var(--focus-chrome)); }
.layout.focus:not(.view-board) .player-frame.ar-portrait,
.layout.focus:not(.view-board) .player-frame.vertical { max-width: min(100%, calc(var(--pic-h) * 9 / 16)); }
.layout.focus:not(.view-board) .player-frame.ar-square { max-width: min(100%, var(--pic-h)); }
.layout.focus:not(.view-board) .player-frame.ar-landscape { max-width: min(100%, calc(var(--pic-h) * 16 / 9)); }
.layout.focus:not(.view-board) .player-frame.is-source { max-width: min(100%, calc(var(--pic-h) * var(--source-ar, 16 / 9))); }

/* Transport line: start over · play · time — tools in the middle — Export right. */
.transport {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 4px var(--sp-4);
  min-height: 48px;
}
.transport-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: 999px;
  border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--text);
  cursor: pointer; padding: 0; transition: background .12s ease, border-color .12s ease, transform .12s ease;
}
.transport-btn:hover { background: rgba(255,255,255,.08); border-color: rgba(255,255,255,.25); }
.transport-btn:active { transform: scale(.96); }
.transport-btn:disabled { opacity: .35; cursor: default; }
.transport-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.transport-play {
  width: 42px; height: 42px; color: #111;
  background: linear-gradient(180deg, #fbfcfd 0%, #dfe3e8 55%, #b7bcc4 100%);
  border-color: rgba(255,255,255,.35);
  box-shadow: 0 1px 6px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.8);
}
.transport-play:hover { background: linear-gradient(180deg, #ffffff 0%, #e9ecf0 55%, #c3c8d0 100%); }
.transport-play .ico-play { margin-left: 2px; }
.transport-play .ico-pause { display: none; }
.transport-play.is-playing .ico-play { display: none; }
.transport-play.is-playing .ico-pause { display: block; }
.transport-time {
  font: 12px/1 var(--mono); color: var(--text); font-variant-numeric: tabular-nums;
  display: inline-flex; align-items: baseline; gap: 6px; margin-left: var(--sp-1); white-space: nowrap;
}
.transport-time i { font-style: normal; color: var(--text-faint); }
.transport-time span { color: var(--text-muted, var(--text-faint)); }
.transport-tools { display: flex; align-items: center; margin-left: var(--sp-3); }
.transport-tools .trimmer-tools h3 { display: none; }
.transport-tools .trimmer-tools .btn-sm { height: 32px; min-width: 32px; }
.transport .focus-bar { margin-left: auto; padding: 0; display: flex; justify-content: flex-end; }
.transport .focus-bar[hidden] { display: none; }
.layout.focus .transport .focus-bar .export-row { width: auto; }
/* PA3: with the shape pills in the row, the row (flex: 0 0 auto) no longer fit
   the bar at 1440 and spilled LEFT over the Split/Music/Post tools -- the
   status text sat under "Post". The bar and the row may shrink now; the
   status line (min-width 0, ellipsis) is what gives, never the buttons. */
.layout.focus .transport .focus-bar { flex: 1 1 auto; min-width: 0; }
.layout.focus .transport .focus-bar .export-row { flex: 1 1 auto; min-width: 0; }
.layout.focus .transport .focus-bar .export-actions { justify-content: flex-end; }
.layout.focus .transport .focus-bar #exportBtn { min-width: 200px; }
.layout.focus .transport .focus-bar .export-status { max-width: 340px; }
/* PA3: 340px of status + three pills + Download + Export is wider than the bar
   at 1440 and forced a second line. The status gives way (it ellipsises). */
@media (max-width: 1600px) { .layout.focus .transport .focus-bar .export-status { max-width: 240px; } }

/* The timeline itself: full width, roomier than in the dock. */
.focus-timeline { position: relative; padding-top: 6px; }
.focus-timeline > .trimmer { padding: 0 var(--sp-4) var(--sp-2); }
.focus-timeline .trimmer-head { display: none; }
.focus-timeline .track-scale { height: 20px; margin-top: 0; }
.focus-timeline .track { height: 58px; border-radius: 8px; }
.focus-timeline .audio-lane { height: 30px; }
/* Measured, not guessed: --focus-chrome is set by the editor from the real
   heights of the title line and the deck (see focusChromeVar in editor.js).
   The static fallback is the measured 1440x900 total minus the trims. */
.layout.focus:not(.view-board) .stage-head { padding: 4px var(--sp-4); }
/* Knob: the playhead you grab. Sits on the ruler, above the track, silver metal. */
.playhead-knob {
  position: absolute; left: 0; top: 0; width: 22px; height: 22px;
  transform: translate(-50%, -100%);
  cursor: ew-resize; touch-action: none; z-index: 9;
}
.playhead-knob[hidden] { display: none; }
.playhead-knob::before {
  content: ''; position: absolute; left: 3px; right: 3px; top: 1px; height: 16px;
  border-radius: 5px 5px 3px 3px;
  background: linear-gradient(180deg, #fbfcfd 0%, #e7e9ec 45%, #b7bcc4 100%);
  box-shadow: 0 1px 4px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.8);
  clip-path: polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%);
}
.playhead-knob::after {
  content: ''; position: absolute; left: 50%; top: -3px; width: 30px; height: 30px;
  transform: translateX(-50%);
}
.playhead-knob:hover::before, .playhead-knob.is-dragging::before {
  background: linear-gradient(180deg, #ffffff 0%, #f1f3f5 45%, #c9ced6 100%);
}
.focus-timeline .playhead::before { display: none; }
.focus-timeline .playhead { box-shadow: 0 0 10px rgba(231,233,236,.55); }

/* Tap-to-reveal cards float over the bottom of the picture instead of pushing
   the timeline around: nothing shifts when a control appears. */
.layout.focus:not(.view-board) .player-wrap { position: relative; }
.layout.focus:not(.view-board) .player-wrap > .player-frame ~ .piece-tools {
  position: absolute; left: 50%; bottom: var(--sp-3); transform: translateX(-50%);
  width: min(94%, 560px); z-index: 20;
  background: rgba(14,15,18,.92); backdrop-filter: blur(10px);
  border: 1px solid var(--line-strong); border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0,0,0,.55);
}
/* Wide window: the card sits BESIDE the picture in the gutter (left/top/width/
   max-height are set from the picture's real box by placeFocusCard), scrolling
   inside itself; it never covers the picture or the words. No caret: it is
   next to the thing it belongs to, the gold outline on the clip says which. */
.layout.focus:not(.view-board) .player-wrap > .player-frame ~ .piece-tools.is-side {
  bottom: auto; transform: none; overflow: auto; overscroll-behavior: contain;
  scrollbar-width: thin;
}
.piece-tools.is-side::after { display: none; }
/* Narrow window: a sheet over the deck. The picture and the words stay clear;
   Done brings the timeline back. */
.layout.focus:not(.view-board) .player-wrap > .player-frame ~ .piece-tools.is-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; top: auto;
  width: 100%; max-width: none; transform: none;
  max-height: 62vh; overflow: auto; overscroll-behavior: contain;
  border-radius: 14px 14px 0 0; border-bottom: 0; z-index: 30;
}
.piece-tools.is-sheet::after { display: none; }
/* "The sheet scrolls if it must" was a lie on a phone: its panel groups are
   flex children that SHRINK (flex-shrink 1) and clip (overflow hidden), so a
   315px sheet held a 172px sound group whose Fade out and Fade shape rows
   were drawn 60px below the viewport, and scrollHeight === clientHeight --
   nothing to scroll. Inside a placed card every row keeps its own height and
   the card itself is the only thing that scrolls. */
.piece-tools.is-sheet > *, .piece-tools.is-side > * { flex: 0 0 auto; }
.piece-tools.is-sheet > .panel-group, .piece-tools.is-side > .panel-group { overflow: visible; }
/* The sheet is fixed, but it lives inside .player-wrap, which is its own
   stacking layer (z-index 2): the sheet's 30 only counts INSIDE that layer,
   so the deck's playhead knob and seam labels were painting straight through
   the card on phones. While a sheet is open the whole wrapper rises above
   the deck; the moment the card hides the wrapper drops back to 2. */
.layout.focus:not(.view-board) .player-wrap:has(> .piece-tools.is-sheet:not([hidden])) { z-index: 40; }

@media (max-width: 720px) {
  .layout.focus:not(.view-board) { --focus-chrome: 300px; }
  .transport { flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); }
  .transport-btn { width: 44px; height: 44px; }
  .transport-play { width: 50px; height: 50px; }
  .transport-tools { margin-left: auto; }
  .transport .focus-bar { flex: 1 1 100%; margin-left: 0; }
  .layout.focus .transport .focus-bar .export-row, .layout.focus .transport .focus-bar .export-actions { width: 100%; }
  .layout.focus .transport .focus-bar #exportBtn { flex: 1 1 auto; min-width: 0; }
  .layout.focus .transport .focus-bar .export-status { display: none; }
  .focus-timeline > .trimmer { padding: 0 var(--sp-3) var(--sp-3); }
  .focus-timeline .track { height: 64px; }
  .playhead-knob { width: 28px; height: 26px; }
  .layout.focus:not(.view-board) .player-wrap > .player-frame ~ .piece-tools { width: 96%; }
}
/* The quiet Edit/Done link keeps its corner on phones too (the 860px rule
   stretches .stage-toggles to 100%, which slid the absolute box off-screen). */
.layout.focus:not(.view-board) .stage-toggles { width: auto; left: auto; }

/* ── SaaS S1: the login gate ─────────────────────────────────────────────────
   Black ground, one white button, gold only on the wordmark and the consent
   box once it is ticked. Nothing else on screen: the app is not there until
   you are someone. */
.auth-gate {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-5);
  background:
    radial-gradient(60% 50% at 50% 0%, rgba(245,197,24,.07) 0%, rgba(245,197,24,0) 70%),
    var(--bg);
}
.auth-gate[hidden] { display: none; }
.auth-card {
  width: min(100%, 380px);
  padding: var(--sp-6) var(--sp-6) var(--sp-5);
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-lg);
  box-shadow: 0 1px 0 rgba(255,255,255,.05) inset, 0 24px 60px -24px rgba(0,0,0,.9);
}
.auth-brand { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-5); }
.auth-brand .brand-name { font-size: 17px; }
.auth-title { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
.auth-sub { margin: var(--sp-1) 0 var(--sp-5); font-size: 12px; color: var(--text-dim); }
.auth-form { display: flex; flex-direction: column; gap: var(--sp-4); }
.auth-field { display: flex; flex-direction: column; gap: var(--sp-1); font-size: 12px; color: var(--text-dim); }
.auth-field[hidden] { display: none; }
.auth-field input {
  height: 44px; padding: 0 var(--sp-3);
  font: 400 14px/1 var(--font); color: var(--text);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r);
  outline: none; transition: border-color .15s;
}
.auth-field input:focus { border-color: rgba(255,255,255,.35); }
.auth-consent {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  font-size: 12px; line-height: 1.45; color: var(--text-dim); cursor: pointer;
}
.auth-consent[hidden] { display: none; }
.auth-consent input {
  flex: 0 0 auto; width: 18px; height: 18px; margin: 1px 0 0;
  accent-color: var(--accent); cursor: pointer;
}
.auth-consent a { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
.auth-consent a:hover { color: var(--accent); }
.auth-error { margin: 0; font-size: 12px; color: var(--err); }
.auth-error[hidden] { display: none; }
.auth-submit { height: 44px; font-size: 14px; }
.auth-submit:disabled { opacity: .45; cursor: not-allowed; }

/* S1 step 2: the credit readout in the header and the out-of-minutes prompt. */
.chip-credits { border-color: rgba(245,197,24,.25); background: rgba(245,197,24,.06); color: var(--text); white-space: nowrap; flex: none; }
.chip-credits .credit-left { margin-left: -4px; color: var(--text-dim); }
.chip-credits[hidden] { display: none; }
a.chip-credits { text-decoration: none; cursor: pointer; }
a.chip-credits:hover { border-color: rgba(245,197,24,.55); }
.credits-later { margin-top: var(--sp-2); }
.chip-credits .credit-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: linear-gradient(180deg, #fae584 0%, #f5c518 55%, #c69a0c 100%);
  box-shadow: 0 0 6px rgba(245,197,24,.45);
}
.chip-credits.is-low { border-color: rgba(240,136,62,.35); color: var(--warn); }
.chip-credits.is-low .credit-dot { background: var(--warn); box-shadow: 0 0 6px rgba(240,136,62,.45); }
.chip-credits.is-empty { border-color: rgba(242,85,90,.35); color: var(--err); }
.chip-credits.is-empty .credit-dot { background: var(--err); box-shadow: none; }
.credits-prompt { z-index: 210; background: rgba(6,7,9,.78); backdrop-filter: blur(6px); }
.credits-numbers {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3);
  margin: 0 0 var(--sp-4);
}
.credits-numbers div {
  padding: var(--sp-3); border: 1px solid var(--line); border-radius: var(--r-md); background: var(--bg);
}
.credits-numbers dt { margin: 0; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.credits-numbers dd { margin: var(--sp-1) 0 0; font-size: 17px; font-weight: 600; color: var(--text); }
.credits-numbers div:first-child dd { color: var(--accent); }
.credits-note { margin: 0 0 var(--sp-4); font-size: 12px; color: var(--text-dim); }
.credits-prompt .auth-title { text-transform: none; letter-spacing: -.01em; }
.credits-prompt .auth-submit { width: 100%; }
.auth-switch { margin: var(--sp-5) 0 0; font-size: 12px; color: var(--text-dim); text-align: center; }
.auth-forgot { margin: calc(-1 * var(--sp-2)) 0 0; font-size: 12px; color: var(--text-dim); text-align: right; }
.auth-forgot[hidden] { display: none; }
.auth-note { margin: 0; font-size: 12px; line-height: 1.5; color: var(--text-dim); }
.auth-note[hidden] { display: none; }
.auth-note code { font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace; color: var(--text); }
.chip-credits.is-unlimited .credit-left { display: none; }
.chip-credits.is-unlimited .credit-dot { background: var(--accent); box-shadow: 0 0 6px rgba(245,197,24,.55); }
.auth-link {
  padding: 0; border: 0; background: none; font: inherit; color: var(--text);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.auth-link:hover { color: var(--accent); }
.auth-signout { height: 28px; }
.auth-signout[hidden] { display: none; }
@media (max-width: 480px) {
  .auth-gate { padding: var(--sp-4); align-items: flex-start; padding-top: 12vh; }
  .auth-card { padding: var(--sp-5) var(--sp-4) var(--sp-4); }
}


/* ── B5 (MASTER-SPEC 2026-09-08): the wide-window editor layout ──────────
   Quincy at 1440x900: "big dead black space left of the vertical preview;
   the editor doesn't use the window". Three columns like a real editor:
   the clip rail | the picture | an inspector column, timeline beneath.
   The inspector holds the clip card at rest and every tap-to-reveal card
   when one is open (.is-docked), so nothing floats and nothing measures. */
@media (min-width: 1200px) {
  /* Keep the clip list as an on-demand drawer, including wide desktops. */
  .layout.focus:not(.view-board) { --rail-w: 0px !important; grid-template-columns: 0 minmax(0, 1fr); }
  .layout.focus:not(.view-board).rail-hidden .rail { display: none !important; }
  .layout.focus:not(.view-board) #splitRail { display: none; }
  .layout.focus:not(.view-board) .stage { grid-column: 2; }
  .layout.focus:not(.view-board) .rail-hide { display: inline-flex; }

  .layout.focus:not(.view-board) .player-wrap {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 400px;
    grid-template-rows: minmax(0, 1fr) auto;
    column-gap: var(--sp-4); row-gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4) var(--sp-3);
  }
  .layout.focus:not(.view-board) .player-wrap > .player-frame { grid-column: 1; grid-row: 1; justify-self: center; }
  .layout.focus:not(.view-board) .player-wrap > .scrub-row { grid-column: 1; grid-row: 2; }
  .layout.focus:not(.view-board) .player-wrap > .player-frame ~ .piece-tools.is-docked,
  .layout.focus:not(.view-board) .player-wrap > .clip-inspector {
    grid-column: 2; grid-row: 1 / -1; align-self: stretch;
    position: static; transform: none; width: auto; max-width: none; max-height: none;
    min-height: 0; overflow: auto; overscroll-behavior: contain; scrollbar-width: thin;
    box-sizing: border-box; margin: 0;
  }
  .piece-tools.is-docked::after { display: none; }
  .piece-tools.is-docked > * { flex: 0 0 auto; }
  .piece-tools.is-docked > .panel-group { overflow: visible; }
  /* A card in the column takes the clip card's place. */
  .layout.focus:not(.view-board) .player-wrap:has(> .piece-tools.is-docked:not([hidden])) > .clip-inspector { display: none; }
}
.clip-inspector {
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-4);
  background: rgba(14,15,18,.92); border: 1px solid var(--line-strong); border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.05);
}
.clip-inspector[hidden] { display: none; }
.layout:not(.focus) .clip-inspector, .layout.view-board .clip-inspector { display: none; }
@media (max-width: 1199px) { .clip-inspector { display: none; } }
.insp-back { display: inline-flex; align-items: center; gap: 4px; align-self: flex-start; margin: 0 0 2px -4px; padding: 4px 8px 4px 6px; border: 0; border-radius: 6px; background: transparent; color: var(--text-dim); font: 600 12px/1 var(--sans, inherit); letter-spacing: .02em; text-transform: uppercase; cursor: pointer; }
.insp-back span { font-size: 18px; line-height: 0; position: relative; top: -1px; }
.insp-back:hover, .insp-back:focus-visible { color: var(--accent); background: rgba(255,255,255,.05); outline: none; }
.insp-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.insp-title { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; letter-spacing: -.01em; text-transform: none; }
.insp-score {
  flex: none; min-width: 40px; height: 28px; padding: 0 9px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 14px/1 var(--mono); color: #0b0c0f;
  background: linear-gradient(180deg, #fae584, #f5c518 55%, #c69a0c);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5), 0 2px 8px rgba(245,197,24,.25);
}
.insp-meta { font: 12px/1.3 var(--mono); color: var(--text-dim); }
.insp-doors { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.insp-door { display: flex; align-items: center; justify-content: center; gap: 8px; height: 40px; }
.insp-door-glyph { opacity: .85; }
.insp-post { display: flex; flex-direction: column; gap: var(--sp-2); padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.insp-post-head { display: flex; align-items: center; justify-content: space-between; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim); }
.insp-desc { margin: 0; font-size: 12px; line-height: 1.45; color: var(--text); }
.insp-post-row { display: grid; grid-template-columns: minmax(0, 1fr) 26px; gap: 6px; align-items: start; }
.insp-hook { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--text); }
.insp-post-row .clip-hashtags { margin-top: 2px; }
.post-copy.is-done { color: var(--accent); border-color: var(--accent); }
.insp-why { margin: 0; font-size: 12px; line-height: 1.45; }
.insp-why > summary, .why-clip > summary { cursor: pointer; color: var(--text-dim); list-style: none; display: flex; align-items: center; gap: 6px; }
.insp-why > summary::before, .why-clip > summary::before { content: ''; width: 0; height: 0; border: 4px solid transparent; border-left: 5px solid var(--text-faint); transition: transform 120ms ease; }
.insp-why[open] > summary::before, .why-clip[open] > summary::before { transform: rotate(90deg); }
.insp-why > summary::-webkit-details-marker, .why-clip > summary::-webkit-details-marker { display: none; }
.insp-why > p, .why-clip > p { margin: var(--sp-2) 0 0; font-size: 12px; line-height: 1.45; }
.why-clip { margin-top: var(--sp-2); font-size: 12px; }

/* Sound repair stays reachable in both editor layouts. */
#soundRepairDialog { width: min(560px, calc(100vw - 32px)); max-height: 85dvh; overflow: auto; box-sizing: border-box; background: var(--panel, #151619); color: var(--text, #eee); border: 1px solid var(--line, #444); border-radius: 12px; padding: 20px; }
#soundRepairDialog::backdrop { background: #0009; }
#soundRepairDialog > button { display: block; margin-left: auto; }

@media (max-width: 767px) {
  #focusToggleBtn { display: none !important; }
  .topbar .crumb, .topbar .brand-name { display: none; }
  #transportTools button, #soundRepairDialog button { min-height: 44px; }
}

.proj-heading { display:flex; align-items:center; gap:8px; }
.proj-heading .proj-name { flex:1; min-width:0; }
.proj-more { flex:none; min-width:44px; min-height:44px; font-size:24px; }
.project-rename-dialog { width:min(420px,calc(100vw - 32px)); box-sizing:border-box; padding:24px; border-radius:12px; border:1px solid #444; background:#151619; color:#eee; }
.project-rename-dialog::backdrop { background:#0009; }
.project-rename-dialog label { display:grid; gap:12px; }
.project-rename-dialog input { width:100%; box-sizing:border-box; padding:12px; background:#090a0c; color:#eee; border:1px solid #555; border-radius:6px; font:inherit; }
.project-rename-dialog button { min-height:44px; margin-right:8px; }

/* Tone: warm accents, restrained surfaces, and deliberate touch targets. */
#soundRepairDialog { width:min(760px,calc(100vw - 24px)); }
.tone-studio { padding:6px 0 24px; border-bottom:1px solid #303238; margin-bottom:24px; }
.tone-studio h2 { font-size:22px; letter-spacing:-.04em; margin:8px 0; }
.tone-subtitle { color:#a5a8b0; font-size:13px; margin:0 0 20px; }
.tone-lowcut { display:flex; align-items:center; justify-content:space-between; padding:16px; background:linear-gradient(110deg,#262319,#191a1d); border:1px solid #4b4128; border-radius:12px; margin-bottom:14px; }
.tone-lowcut span { display:block; font-size:12px; color:#b6b2a5; margin-top:5px; }
.tone-toggle { border:1px solid #484b52; border-radius:24px; padding:8px 17px; background:#212328; color:#aeb1ba; font:inherit; font-size:12px; cursor:pointer; min-height:44px; }
.tone-toggle[aria-checked=true] { background:#f5c518; border-color:#f5c518; color:#16130a; box-shadow:0 0 18px #f5c51818; }
.tone-bands { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }
.tone-band { background:#1b1d21; border:1px solid #33363d; border-radius:12px; padding:14px; transition:border-color .15s; }
.tone-band.is-on { border-color:#8b742d; }
.tone-band-head { display:flex; justify-content:space-between; align-items:center; gap:8px; }
.tone-band-head strong { font-size:16px; }
.tone-band > p { font-size:12px; color:#9297a1; margin:8px 0 18px; }
.tone-segments { display:flex; background:#101114; border:1px solid #2e3137; border-radius:8px; padding:3px; gap:2px; margin-top:9px; }
.tone-segments button { flex:1; min-width:0; min-height:40px; padding:8px 4px; background:transparent; border:0; border-radius:5px; color:#b7bac3; font:inherit; font-size:11px; cursor:pointer; }
.tone-segments button[aria-pressed=true] { background:#34312a; color:#ffe192; }
.tone-segments button:disabled { opacity:.35; cursor:default; }
.tone-studio button:focus-visible { outline:2px solid #f5c518; outline-offset:3px; }
@media(max-width:600px) { .tone-bands { grid-template-columns:1fr; } .tone-band { padding:16px; } .tone-band > p { margin-bottom:10px; } .tone-segments button { font-size:13px; min-height:44px; } }

.tone-details summary { cursor:pointer; color:#c9cbd1; padding:12px 2px 18px; font-size:13px; }
.tone-details[open] summary { color:#f6d86e; }

#soundRepairDialog > button { position:sticky; top:0; z-index:3; background:#24262b; }
.tone-studio h2 { text-transform:none; color:#f0ede6; }

.picture-color { padding:16px; margin:14px 0; border:1px solid #4a4028; border-radius:12px; background:linear-gradient(130deg,#24221b,#17191d); }
.picture-color summary { cursor:pointer; font-weight:650; color:#f3df99; font-size:14px; }
.picture-color > p { color:#aaa99f; font-size:12px; margin:12px 0; }
.look-presets { display:grid; grid-template-columns:repeat(4,1fr); gap:7px; margin:16px 0; }
.look-preset { min-height:48px; border:1px solid #46433b; border-radius:8px; color:#e9e4d5; background:#292a27; font:inherit; font-size:12px; cursor:pointer; }
.look-preset[data-look-preset=fresh] { background:linear-gradient(140deg,#303d39,#222b2e); }
.look-preset[data-look-preset=warm] { background:linear-gradient(140deg,#61442d,#312624); }
.look-preset[data-look-preset=mono] { background:linear-gradient(140deg,#5b5c5d,#252627); }
.look-preset:hover { border-color:#f5c518; }
.look-control { display:grid; gap:7px; margin:18px 0; }
.look-control strong { font-size:12px; font-weight:500; color:#d5d5d0; }
.look-control input { width:100%; margin:0; accent-color:#f5c518; min-height:28px; cursor:pointer; }
.look-ends { display:flex; justify-content:space-between; font-size:10px; color:#94978f; }
.look-compare { width:100%; min-height:44px; }
.look-compare[aria-pressed=true] { border-color:#f5c518; color:#ffe192; }

.sound-dialog-header { display:flex; justify-content:space-between; gap:12px; position:sticky; top:0; z-index:3; background:#151619; padding-bottom:12px; }

/* On-demand product walkthrough: gold entry point, readable feature cards. */
.view-tab.is-active, .view-tab[aria-selected="true"] { color: #f5c518; }
.walkthrough-launcher {
  flex: 0 0 28px; width: 28px; height: 28px; border-radius: 50%;
  border: 1px solid rgba(245,197,24,.35); background: #19170f;
  color: #f5c518; font-family: inherit; font-size: 16px; font-weight: 800; line-height: 1;
  cursor: pointer; display: grid; place-items: center;
}
.walkthrough-launcher:hover { background: #f5c518; color: #111; }
.walkthrough-launcher:focus-visible, .walkthrough button:focus-visible, .walkthrough input:focus-visible {
  outline: 2px solid #f5c518; outline-offset: 3px;
}
.walkthrough {
  box-sizing: border-box; width: min(960px, calc(100vw - 32px)); height: min(780px, calc(100dvh - 40px));
  max-width: none; max-height: none; padding: 0; margin: auto; color: #edf0f3;
  border: 1px solid #4a4328; border-radius: 20px; background: #101215;
  box-shadow: 0 24px 100px #000a; overflow: hidden;
}
.walkthrough[open] { display: flex; flex-direction: column; }
.walkthrough::backdrop { background: #030507c9; backdrop-filter: blur(5px); }
.walkthrough-head { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 24px 28px; border-bottom: 1px solid #303238; }
.walkthrough-eyebrow { color: #f5c518; font-size: 11px; letter-spacing: .1em; font-weight: 800; }
.walkthrough-head h2 { margin: 8px 0 0; font-size: 23px; line-height: 1.2; }
.walkthrough-close { width: 44px; height: 44px; flex-shrink: 0; color: #e9ebee; background: #202329; border: 1px solid #41444a; border-radius: 50%; font-size: 26px; cursor: pointer; }
.walkthrough-layout { display: grid; grid-template-columns: 245px minmax(0,1fr); min-height: 0; flex: 1; }
.walkthrough-index { padding: 22px 16px; border-right: 1px solid #303238; display: flex; flex-direction: column; min-height: 0; }
.walkthrough-index label { font-weight: 700; font-size: 12px; margin-bottom: 8px; }
.walkthrough-index input { box-sizing: border-box; width: 100%; min-height: 44px; padding: 10px; color: #fff; background: #080a0c; border: 1px solid #45484d; border-radius: 9px; }
.walkthrough-topics { overflow: auto; margin-top: 12px; }
.walkthrough-topics h3 { color: #a5abb5; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; margin: 18px 8px 7px; }
.walkthrough-topic { display: block; text-align: left; width: 100%; min-height: 44px; padding: 10px; background: transparent; color: #c4c9d1; border: 1px solid transparent; border-radius: 8px; cursor: pointer; line-height: 1.4; }
.walkthrough-topic:hover { background: #22252b; }
.walkthrough-topic[aria-current] { color: #ffe078; background: #282310; border-color: #665626; }
.walkthrough-main { overflow: auto; min-width: 0; }
.walkthrough-card { padding: 30px; outline: none; }
.walkthrough-card h3 { font-size: clamp(23px, 3vw, 32px); line-height: 1.15; margin: 12px 0 16px; letter-spacing: -.025em; }
.walkthrough-intro { color: #c8ced7; line-height: 1.65; font-size: 16px; }
.walkthrough-location { margin: 24px 0; background: #1c1e23; border-left: 3px solid #f5c518; padding: 14px 16px; border-radius: 0 10px 10px 0; }
.walkthrough-location strong { font-size: 10px; letter-spacing: .1em; color: #f5c518; }
.walkthrough-location p { margin: 7px 0 0; line-height: 1.6; font-size: 14px; }
.walkthrough-steps { padding-left: 24px; margin: 22px 0; }
.walkthrough-steps li { padding-left: 6px; margin: 0 0 16px; line-height: 1.65; font-size: 14px; }
.walkthrough-steps li::marker { color: #f5c518; font-weight: 800; }
.walkthrough-tip { background: #171b21; padding: 16px; border-radius: 10px; color: #b9c3d1; font-size: 13px; line-height: 1.65; }
.walkthrough-availability, .walkthrough-empty { color: #a5abb5; line-height: 1.5; font-size: 12px; }
.walkthrough-footer { display: flex; align-items: center; gap: 10px; padding: 16px 24px; border-top: 1px solid #303238; }
.walkthrough-progress { margin-right: auto; color: #bcc1cb; font-size: 13px; }
.walkthrough-footer button, .walkthrough-reveal { min-height: 44px; }
.walkthrough button:disabled { opacity: .45; cursor: default; }
.walkthrough-highlight { outline: 3px solid #f5c518 !important; outline-offset: 5px !important; box-shadow: 0 0 0 8px #f5c51825 !important; }
@media (max-width: 767px) {
  .walkthrough { width: calc(100vw - 16px); height: calc(100dvh - 16px); border-radius: 14px; }
  .walkthrough-head { padding: 16px; gap: 10px; }
  .walkthrough-head h2 { font-size: 19px; }
  .walkthrough-layout { display: flex; flex-direction: column; }
  .walkthrough-index { padding: 12px 16px; flex: 0 0 auto; border-right: 0; border-bottom: 1px solid #303238; }
  .walkthrough-index label { display: none; }
  .walkthrough-topics { display: flex; gap: 8px; overflow: auto; margin-top: 8px; max-height: 64px; }
  .walkthrough-topics h3 { display: none; }
  .walkthrough-topic { width: auto; flex: 0 0 175px; }
  .walkthrough-main { flex: 1; min-height: 0; }
  .walkthrough-card { padding: 20px; }
  .walkthrough-card h3 { font-size: 25px; }
  .walkthrough-footer { padding: 12px 16px; }
  .topbar-actions { gap: 4px; }
}

.walkthrough-head h2, .walkthrough-card h3 { text-transform: none; color: #f0f1f4; }
@media (max-width: 767px) {
  .walkthrough-launcher { flex-basis: 44px; width: 44px; height: 44px; border: 0; background: radial-gradient(circle, #25200f 0 13px, transparent 14px); }
  .walkthrough-launcher:hover { color: #ffe078; background: radial-gradient(circle, #332a10 0 13px, transparent 14px); }
}

.walkthrough-launcher { position: relative; }
.walkthrough-launcher::after {
  content: attr(data-tooltip); position: absolute; top: calc(100% + 10px); right: 0;
  width: 180px; padding: 10px 12px; border: 1px solid #665626; border-radius: 8px;
  background: #141510; color: #ffe078; font: 500 12px/1.5 system-ui, sans-serif;
  text-align: left; box-shadow: 0 8px 24px #0008; pointer-events: none;
  opacity: 0; visibility: hidden; z-index: 500;
}
.walkthrough-launcher:hover::after, .walkthrough-launcher:focus-visible::after { opacity: 1; visibility: visible; }

.walkthrough-visual { margin: 18px 0 22px; }
.walkthrough-image-button { position: relative; display: block; width: 100%; padding: 0; overflow: hidden; cursor: zoom-in; border: 1px solid #504527; border-radius: 10px; background: #090c0e; }
.walkthrough-image-button img { display: block; width: 100%; height: 170px; object-fit: contain; }
.walkthrough-image-hint { position: absolute; right: 7px; bottom: 7px; padding: 4px 7px; border-radius: 5px; color: #ffe078; background: #111e; font-size: 10px; }
.walkthrough-visual figcaption { margin-top: 8px; color: #b8bec7; font-size: 12px; line-height: 1.5; }
.walkthrough-image-dialog { padding: 16px; margin: auto; width: min(1000px,calc(100vw - 24px)); max-height: calc(100dvh - 24px); box-sizing: border-box; border: 1px solid #655429; border-radius: 14px; background: #101215; color: #e9edf2; }
.walkthrough-image-dialog::backdrop { background: #030507dd; }
.walkthrough-image-dialog img { display: block; max-width: 100%; max-height: calc(100dvh - 160px); margin: 16px auto; object-fit: contain; }
.walkthrough-image-dialog p { margin: 0; font-size: 13px; line-height: 1.5; color: #c3c9d2; }
.walkthrough-image-close { display: block; margin-left: auto; padding: 10px 16px; min-height: 44px; border-radius: 8px; border: 1px solid #625329; background: #211d11; color: #ffe078; cursor: pointer; }
.walkthrough-image-close:focus-visible { outline: 2px solid #f5c518; outline-offset: 2px; }
@media(max-width:767px) { .walkthrough-image-button img { height: 135px; } }

body:has(.layout.focus.rail-hidden:not(.view-board)) .rail-reveal { display: flex !important; }

#focusToggleBtn.editor-mode-toggle { width: 112px; min-height: 32px; flex: 0 0 112px; color: #d4c58a; }
body.library-open #focusToggleBtn, body:has(.layout.view-board) #focusToggleBtn { display: none; }
.clip-transcript-dialog { width: min(640px,calc(100vw - 24px)); max-height: calc(100dvh - 32px); box-sizing: border-box; padding: 24px; border: 1px solid #655429; border-radius: 14px; background: #101215; color: #e8edf3; }
.clip-transcript-dialog::backdrop { background: #030507bd; }
.clip-transcript-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.clip-transcript-head h2 { color: #f5c518; text-transform: none; margin: 0; }
.clip-transcript-text p { line-height: 1.85; font-size: 16px; margin: 18px 0; }
.clip-transcript-dialog .btn { min-height: 44px; }
@media(max-width:767px) { #focusToggleBtn.editor-mode-toggle { display: none !important; } }

#focusToggleBtn.editor-mode-toggle { display: inline-flex; width: 64px; flex-basis: 64px; opacity: 1; }
body.library-open #focusToggleBtn, body:has(.layout.view-board) #focusToggleBtn { display: none; }
#sideTools > .piece-tools-head { position: sticky; top: 0; background: #11151a; z-index: 2; padding-bottom: 10px; }
#sideTools > .piece-tools-head .btn { margin-left: auto; }
#sideTools > .panel-body { display: block !important; overflow: visible; height: auto; }
#sideTools .words { max-height: none; overflow: visible; }
@media(max-width:767px) { #focusToggleBtn.editor-mode-toggle { display: inline-flex !important; width: 52px; flex-basis: 52px; } body.library-open #focusToggleBtn, body:has(.layout.view-board) #focusToggleBtn { display: none !important; } }

.voiceover-card{padding:14px;border:1px solid #f5c51835;border-radius:12px;background:#f5c51808;margin-bottom:16px}.voiceover-card h3{color:#f5c518;margin:0 0 8px}.voiceover-card p{font-size:12px;line-height:1.5;color:#aab0b7}.voiceover-actions{display:flex;gap:8px;flex-wrap:wrap}.voiceover-card audio,.voiceover-card meter{width:100%;margin:8px 0}.voiceover-card audio{height:36px;color-scheme:dark}

.voiceover-mic-label{display:grid;gap:6px;font-size:12px;color:#c7cbd0}.voiceover-mic-label select{max-width:100%;min-width:0;background:#17191d;color:#e8e8e8;border:1px solid #393c42;border-radius:8px;padding:9px}
/* Inline tools keep their heading outside the scrolling controls. */
.layout.focus:not(.view-board) .player-wrap > .player-frame ~ #sideTools,
#sideTools { overflow: hidden; }
#sideTools > .piece-tools-head { position: static; flex: 0 0 auto; padding: 0 0 10px; background: transparent; }
#sideTools > :not(.piece-tools-head) { min-height: 0; overflow: auto !important; flex: 1 1 auto !important; scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) var(--scroll-track); padding-right: 6px; }
#sideTools .tone-bands { grid-template-columns: minmax(0, 1fr); gap: 10px; }
#sideTools .tone-band { padding: 12px; }
#sideTools .tone-band > p { margin: 5px 0 9px; }
#sideTools .tone-segments button { min-width: 0; min-height: 40px; white-space: nowrap; }
#musicTools .piece-tools-sound > label { display: grid; gap: 6px; }
#musicTools select { padding: 9px; background: #17191d; color: #eee; border: 1px solid #393c42; border-radius: 8px; }
/* Compact studio controls: quiet surfaces, aligned labels, restrained gold. */
#sideTools .tone-studio { padding: 0; background: none; border: 0; }
#sideTools .tone-studio h2 { font-size: 13px; margin-bottom: 4px; }
#sideTools .tone-subtitle { font-size: 11px; margin: 0 0 12px; }
#sideTools .tone-lowcut { padding: 9px 0; border: 0; border-bottom: 1px solid #ffffff10; border-radius: 0; background: transparent; gap: 12px; }
#sideTools .tone-lowcut strong { font-size: 12px; }
#sideTools .tone-lowcut span { font-size: 11px; }
#sideTools .tone-toggle { min-width: 42px; width: 42px; min-height: 26px; height: 26px; padding: 0 6px; border-radius: 7px; font-size: 11px; flex-shrink: 0; }
#sideTools .tone-toggle[aria-checked=true] { background: #f5c5181a; border-color: #f5c51865; color: #f5c518; box-shadow: none; }
#sideTools .tone-bands { gap: 0; }
#sideTools .tone-band { display: grid; grid-template-columns: 90px minmax(0, 1fr); column-gap: 12px; row-gap: 6px; padding: 12px 0; border: 0; border-bottom: 1px solid #ffffff12; border-radius: 0; background: transparent; }
#sideTools .tone-band-head { grid-column: 1; grid-row: 1; flex-wrap: wrap; gap: 6px; align-content: start; }
#sideTools .tone-band-head strong { font-size: 12px; }
#sideTools .tone-band > p { grid-column: 1; grid-row: 2; margin: 0; font-size: 10px; line-height: 1.4; }
#sideTools .tone-segments { grid-column: 2; margin: 0; padding: 2px; border-color: #ffffff0b; background: #080b0e66; }
#sideTools .tone-segments button { min-height: 29px; font-size: 11px; padding: 4px 5px; }
#sideTools .tone-segments button[aria-pressed=true] { background: #f5c51812; color: #f5d557; box-shadow: inset 0 0 0 1px #f5c51820; }
@media (pointer: coarse) {
  #sideTools .tone-toggle { min-height: 40px; }
  #sideTools .tone-segments button { min-height: 40px; }
}
@media (max-width: 360px) {
  #sideTools .tone-band { grid-template-columns: 64px minmax(0, 1fr); column-gap: 8px; }
  #sideTools .tone-segments button { font-size: 10px; padding-inline: 2px; }
}

.insp-why .clip-detail-subs,.why-clip .clip-detail-subs { display:flex;flex-wrap:wrap;gap:6px;margin-top:12px; }
.score-estimate-note {font-size:11px;line-height:1.45;color:#9297a1;margin:9px 0;}

/* Desktop has one home for each action; compact layouts retain their access. */
@media (min-width:1200px) {
  body:has(.layout.focus:not(.view-board)) #focusToggleBtn,
  .layout.focus:not(.view-board) #musicAddBtn,
  .layout.focus:not(.view-board) #postToolsBtn { display: none !important; }
}

/* Piece context and cover-up manipulation share the editor's quiet gold. */
.piece-scope { font-size: 11px; color: var(--accent); margin: 8px 0 12px; }
.player-frame.is-tools-open .hide-layer { pointer-events: none; z-index: 8; }
.player-frame.is-tools-open .hide-patch { pointer-events: auto; touch-action: none; }
.hide-patch .hide-patch-fill, .hide-patch-tag { pointer-events: none; }
.hide-patch .ov-handle { touch-action: none; z-index: 2; }
.cover-lane { position: relative; height: 30px; margin: 4px 0; background: #121415; border-radius: 5px; overflow: hidden; }
.cover-lane[hidden] { display:none; }
.cover-lane-label { position:absolute; top:7px; left:8px; font:10px var(--mono); color:#92979b; pointer-events:none; }
.cover-bar { position:absolute; top:3px; height:24px; background:#564817; border:1px solid #b89629; border-radius:4px; color:#f3d568; font-size:11px; text-align:center; cursor:grab; touch-action:none; overflow:hidden; }
.cover-bar > span:first-child { pointer-events:none; }
.cover-edge { position:absolute; top:0; bottom:0; width:10px; background:#dfb932; opacity:.65; cursor:ew-resize; touch-action:none; }
.cover-edge.start { left:0; }.cover-edge.end { right:0; }

.qlip-confirm { max-width: 420px; width: calc(100vw - 40px); padding: 24px; color: #ededee; background: #151719; border: 1px solid #414245; border-radius: 16px; box-shadow: 0 18px 70px #0009; }
.qlip-confirm::backdrop { background:#050607b8; }
.qlip-confirm h3 { font-size:18px; line-height:1.4; margin:0 0 12px; }
.qlip-confirm p { color:#a5aaae; font-size:13px; line-height:1.6; }
.qlip-confirm-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:20px; }
.full-qlip-controls .restore-original { display:block; margin-top:20px; color:#d6b453; }

/* Project sorting: lightweight floating card, animated neighbours, touch grip. */
.proj-drag-handle { flex:none; border:0; background:none; color:var(--muted); font-size:20px; width:28px; height:30px; cursor:grab; touch-action:none; padding:0; }
.proj-drag-handle:hover { color:var(--accent); }
.project-card.is-project-dragging { opacity:.25; transform:none; border-color:var(--accent); }
.project-drag-ghost { position:fixed; z-index:10000; pointer-events:none; margin:0; transition:none; will-change:transform; box-shadow:0 18px 50px #0009; border-color:var(--accent); opacity:.97; }
.proj-pin { font-size:10px; color:var(--accent); flex:none; }
@media (pointer:coarse) { .proj-drag-handle { width:40px;height:40px; } }
@media (prefers-reduced-motion:reduce) { .project-card { transition:none; } }

.qlip-confirm textarea { width:100%;min-height:90px;max-height:180px;resize:vertical;background:var(--surface);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:10px; }
.save-state.is-err[role="button"] {cursor:pointer;text-decoration:underline;text-underline-offset:3px;}

/* Keep the transcript shortcut beside Done without crowding compact headers. */
#captionTools > .piece-tools-head { align-items:center; }
#captionTools .caption-tools-now { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#captionTools .caption-full-transcript { flex:none; color:var(--accent); white-space:nowrap; }
#captionTools .piece-tools-close { flex:none; margin-left:0; }

/* Master finishing wraps media only; frame guides and editor controls stay outside. */
.video-composition { position:absolute;inset:0;pointer-events:none; display:flex;align-items:center;justify-content:center; }
.composition-crop { position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:auto;touch-action:none; }
#stageOverlay .crop-guide { pointer-events:none; }

/* Plan branding is outside editable content and Full Qlip filters. */
.preview-watermark { position:absolute;right:3%;top:3%;z-index:10;pointer-events:none;
  font-family:var(--font);font-weight:800;font-size:var(--watermark-size,12px);line-height:1.2;
  color:#f5c518;background:rgba(10,11,13,.71);border-radius:3px;padding:.28em; }
.credits-prompt .auth-card { max-height:calc(100dvh - 32px);overflow-y:auto; }
.credits-prompt #creditsHighlights { margin-top:8px;white-space:normal; }

/* Review state belongs to the poster; actual video playback stays in color. */
.clip-grid .clip-card.is-watched .card-thumb > img { filter: grayscale(1) brightness(.72); }
.clip-grid .card-thumb > img { transition: filter .2s ease; }
.card-review { display:flex; align-items:center; justify-content:flex-end; gap:6px; }
.card-review button { border:1px solid transparent; background:transparent; color:var(--text-dim); font:500 11px/1.2 var(--font); border-radius:6px; min-height:32px; padding:6px 8px; cursor:pointer; }
.card-thumb .card-dislike {
  position:absolute; right:40px; left:auto; bottom:8px; z-index:3; width:28px; height:28px; padding:0;
  display:grid; place-items:center; border:0; border-radius:6px; background:transparent;
  color:rgba(255,255,255,.55); cursor:pointer; filter:drop-shadow(0 1px 3px rgba(0,0,0,.8));
  transition:color .14s ease, transform .12s ease;
}
.card-thumb .card-dislike:hover { color:#fff; transform:scale(1.12); }
.card-thumb .card-dislike:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.card-thumb .card-dislike:disabled { opacity:.65; cursor:wait; }
.clip-grid.is-selecting .card-dislike { display:none; }
.card-review button:hover { color:var(--text); background:rgba(255,255,255,.05); }
.card-thumb .card-dislike.is-on { color:var(--accent); background:rgba(245,197,24,.08); border-color:rgba(245,197,24,.22); }
.card-review button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.card-review button:disabled { cursor:wait; opacity:.65; }
.clip-grid.is-selecting .card-review { visibility:hidden; }
@media(pointer:coarse) { .card-review button { min-height:44px; } .card-thumb .card-fav, .card-thumb .card-dislike { width:44px; height:44px; } .card-thumb .card-dislike { right:56px; left:auto; } }
@media(prefers-reduced-motion:reduce) { .clip-grid .card-thumb > img { transition:none; } }

.transcript-actions { display:flex; flex-direction:column; align-items:flex-start; gap:8px; padding:10px 14px; border-bottom:1px solid var(--line); }
.transcript-bulk { display:flex; flex-wrap:wrap; gap:4px; }
.tr-para { position:relative; padding-right:30px; }
.tr-section-remove { position:absolute; right:0; top:0; border:0; background:transparent; color:var(--text-dim); width:28px; height:28px; font-size:18px; cursor:pointer; border-radius:5px; }
.tr-section-remove:hover { background:rgba(255,255,255,.06); color:var(--accent); }
.tr-section-remove:focus-visible { outline:2px solid var(--accent); }
@media(pointer:coarse){.tr-section-remove{width:40px;height:40px}.tr-para{padding-right:42px}}

.transcript-restore-hint { font-size:11px; line-height:1.5; margin:6px 0 10px; color:var(--text-dim); }

/* Quiet hover playback; pointer motion or keyboard focus reveals controls. */
.card-state { right:auto; left:8px; bottom:42px; }
.card-thumb > button, .card-thumb > .card-score, .card-thumb > .card-dur, .card-thumb > .card-state {
  transition:opacity .18s ease, color .14s ease, transform .12s ease;
}
/* Keyboard focus reveals, a mouse click does not: :focus-within would leave
   the whole cluster up for the rest of the hover after one click on the
   heart, which is half of why this never looked like it hid. */
.card-thumb.is-controls-idle:not(:has(:focus-visible)) > :is(button,.card-score,.card-dur,.card-state) {
  opacity:0; pointer-events:none;
}
/* Play is the pause/pin control for the preview that is running: it stays
   hit-testable while the rest is out of the way, and shows itself on hover
   without calling the badges back. Listed after the rule above so it wins the
   specificity tie. */
.card-thumb.is-controls-idle:not(:has(:focus-visible)) > .card-play { pointer-events:auto; }
.card-thumb.is-controls-idle > .card-play:hover,
.card-thumb.is-controls-idle > .card-play:focus-visible { opacity:1; }
/* ...and it shows itself quietly. The gold hover treatment is the loud "start
   this" call to action; over a clip that is already playing it reads as a
   badge stuck on the video, which is the very thing hover playback is trying
   to get out of the way. Pause/pin only needs to be findable. */
.card-thumb.is-controls-idle > .card-play:hover {
  background:rgba(6,7,9,.72); color:#fff; border-color:rgba(255,255,255,.22);
  transform:translate(-50%,-50%);
}
@media(prefers-reduced-motion:reduce) { .card-thumb > button, .card-thumb > .card-score, .card-thumb > .card-dur { transition:none; } }

/* Transcript follows the same time axis as picture and source audio. */
.transcript-lane { position:relative; height:34px; margin-top:4px; overflow:hidden; border:1px solid #292b30; border-radius:5px; background:#101114; }
.transcript-lane .audio-lane-tag { z-index:2; pointer-events:none; font-size:8px; top:1px; }
.transcript-region { position:absolute; top:12px; bottom:2px; padding:0 5px; border:1px solid #675925; border-radius:3px; background:#302a18; color:#ead99a; font-size:10px; text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer; }
.transcript-region:hover,.transcript-region:focus-visible { background:#514523; border-color:#f5c518; outline:none; }
.transcript-region.is-hidden { opacity:.45; text-decoration:line-through; }
.transcript-empty { position:absolute; left:88px; top:10px; font-size:11px; color:#8a8d96; }

.caption-time-handle { position:absolute; top:0; bottom:0; width:8px; cursor:ew-resize; touch-action:none; background:rgba(245,197,24,.5); z-index:2; }
.caption-time-handle.is-start { left:0; border-radius:2px 0 0 2px; }
.caption-time-handle.is-end { right:0; border-radius:0 2px 2px 0; }
.transcript-region { padding-left:10px; padding-right:10px; }

/* Independent foreground inside the chosen export canvas. */
.foreground-canvas { display:none;position:absolute;inset:0;width:100%;height:100%;pointer-events:none; }
.is-fit-layout .foreground-canvas { display:block; }
.player-frame.is-fit-layout #video { visibility:hidden; }
.is-fit-layout .source-extent { display:none; }
.foreground-guide { position:absolute;border:1px solid #e7c75c;pointer-events:auto;touch-action:none;cursor:move;box-sizing:border-box;z-index:6; }
.foreground-guide[hidden] { display:none; }
.foreground-resize { position:absolute;right:-7px;bottom:-7px;width:18px;height:18px;border:2px solid #161616;border-radius:5px;background:#e7c75c;cursor:nwse-resize;touch-action:none; }
.video-layout-controls { display:grid;gap:8px;margin-top:8px; }
.video-layout-modes { display:flex;gap:5px;flex-wrap:wrap; }
.video-layout-modes button[aria-pressed="true"] { color:#f5d768;border-color:#8d763a;background:#292517; }
.video-layout-detail { display:grid;gap:8px; }
.video-layout-detail[hidden] { display:none; }
.video-layout-detail label { display:flex;align-items:center;gap:10px;font-size:12px;color:#ccc; }
.video-layout-detail input { min-width:60px;flex:1;accent-color:#e7c75c; }

.subject-controls { display:grid;gap:7px;padding-top:12px;margin-top:10px;border-top:1px solid #292b30; }
.player-frame.is-picking-subject { cursor:crosshair; }
.player-frame.is-picking-subject .crop-guide { pointer-events:none; }


/* Editor's two optional left drawers share the clip rail. Keys uses the same
   overlay, so opening it never steals width from the picture or timeline. */
.rail-switch { flex: none; }
.rail.rail-mode-keys .rail-import,
.rail.rail-mode-keys #analyzeBtn,
.rail.rail-mode-keys .rail-prompt,
.rail.rail-mode-keys .rail-status,
.rail.rail-mode-keys .clip-notice,
.rail.rail-mode-keys .clip-list { display: none !important; }
.rail-key-guide { display: none; flex: 1; min-height: 0; overflow-y: auto; padding: 0 18px 24px; }
.rail.rail-mode-keys .rail-key-guide { display: block; }
.rail-key-guide .help-keys { grid-template-columns: minmax(104px, 42%) minmax(0, 1fr); gap: 11px 12px; }
.rail-key-guide .help-keys-title { margin-top: 23px; }
.rail-key-guide .help-keys dd { line-height: 1.4; }
.rail-key-guide .help-gestures { line-height: 1.5; }
.layout:not(.view-board):not(.rail-hidden) .rail.rail-mode-keys { width: min(360px, 90vw); }
.layout.view-board .rail-key-guide { display: none !important; }

.key-reveal {
  display: none; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-1);
  position: fixed; left: 0; top: calc(50% - 45px); transform: translateY(-50%);
  z-index: 40; min-width: 32px; min-height: 84px;
  background: var(--surface); border: 1px solid var(--line-strong); border-left: 0;
  border-radius: 0 6px 6px 0; color: var(--muted);
  padding: var(--sp-3) var(--sp-2); cursor: pointer;
  transition: color .15s ease, background .15s ease;
}
.key-reveal:hover { color: var(--text); background: rgba(255,255,255,.06); }
.key-reveal:hover .rail-reveal-label { color: var(--text); }
.key-reveal:focus-visible, .rail-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
body:has(.layout:not(.view-board).rail-hidden) .key-reveal { display: flex; }
body:has(.layout:not(.view-board).rail-hidden) .rail-reveal { top: calc(50% + 45px); }

/* Phone layout: give account switching and navigation actual room instead of
   clipping the labels in the old single-row 52px header. */
.workspace-card {
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  width:100%; max-width:1440px; margin:0 auto 20px; padding:16px 18px;
  border:1px solid rgba(245,197,24,.28); border-radius:var(--r-lg);
  background:linear-gradient(110deg,rgba(245,197,24,.09),var(--surface) 42%);
}
.workspace-card[hidden] { display:none; }
.workspace-card-copy { min-width:0; }
.workspace-card-eyebrow { display:block; margin-bottom:6px; color:var(--accent); font:700 10px/1.2 var(--mono); letter-spacing:.12em; }
.workspace-card strong { display:block; color:var(--text); font-size:15px; overflow-wrap:anywhere; }
.workspace-card p { margin:5px 0 0; color:var(--text-dim); font-size:12px; line-height:1.5; }
.workspace-card button { flex:none; min-height:38px; padding-inline:14px; }

@media (max-width:600px) {
  :root { --topbar-h: 96px; }
  .topbar {
    display:grid; grid-template-columns:auto minmax(0,1fr) auto auto auto;
    grid-template-rows:42px 42px; height:var(--topbar-h);
    align-items:center; gap:4px 6px; padding:4px 10px; overflow:visible;
  }
  .topbar .brand { grid-column:1; grid-row:1; min-width:0; }
  .topbar .brand-name { white-space:nowrap; }
  .topbar .crumb { display:none; }
  .topbar .topbar-actions { display:contents; }
  .topbar-actions #teamButton {
    grid-column:2/5; grid-row:1; display:block; width:100%; height:36px;
    padding:0 10px; font-size:12px; line-height:34px; text-align:center;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    color:var(--text); border:1px solid var(--line-strong);
  }
  .topbar-actions #teamButton[hidden] { display:none; }
  .topbar-actions #signOutBtn { grid-column:5; grid-row:1; width:36px; height:36px; }
  .topbar .view-switch { grid-column:1/3; grid-row:2; justify-self:start; margin:0; }
  .topbar-actions #focusToggleBtn { grid-column:3; grid-row:2; width:42px; height:36px; font-size:11px; }
  .topbar-actions #walkthroughBtn { grid-column:4; grid-row:2; width:36px; height:36px; flex-basis:auto; }
  .topbar-actions #creditChip { grid-column:5; grid-row:2; min-width:0; justify-self:end; }
  .topbar-actions #healthChip { display:none; }
  .library { padding:16px 16px 48px; }
  .library-head { flex-wrap:wrap; gap:12px; margin-bottom:14px; }
  .library-actions { width:100%; justify-content:space-between; }
  .library-actions .btn { height:40px; }
  .library-actions .btn-gold { flex:1; justify-content:center; }
  .library-grid { grid-template-columns:minmax(0,1fr); }
  .workspace-card { flex-direction:column; align-items:stretch; gap:12px; padding:16px; }
  .workspace-card button { width:100%; }
  .layout.view-board { padding:16px; }
  .board-head { margin:16px 0 12px; }
  .board-tools { flex-wrap:wrap; overflow:visible; gap:8px; }
  .board-tools .seg { flex:0 0 100%; }
  .board-tools #favFilter, .board-tools #hiddenClipsFilter, .board-tools #selectModeBtn { min-height:38px; }
  .clip-grid { grid-template-columns:minmax(0,1fr); gap:10px; }
  .clip-card {
    display:grid; grid-template-columns:112px minmax(0,1fr);
    grid-template-rows:auto auto 1fr; align-items:start; gap:6px 12px;
    padding:8px; min-height:194px; border:1px solid var(--line);
    background:var(--surface); border-radius:var(--r-lg);
  }
  .card-thumb { grid-column:1; grid-row:1/4; width:112px; height:176px; aspect-ratio:auto; }
  .clip-card .card-title { grid-column:2; grid-row:1; margin-top:5px; font-size:14px; }
  .clip-card .card-review { grid-column:2; grid-row:2; justify-content:flex-start; flex-wrap:wrap; }
  .clip-card .card-post { grid-column:2; grid-row:3; align-self:end; }
  .walkthrough-head { padding:12px 16px; }
  .walkthrough-head h2 { font-size:17px; margin-top:4px; }
  .walkthrough-index { padding:8px 12px; }
  .walkthrough-topics { margin-top:6px; max-height:48px; }
  .walkthrough-topic { min-height:38px; flex-basis:145px; padding:7px 9px; font-size:12px; }
  .walkthrough-card { padding:16px; }
  .walkthrough-card h3 { font-size:22px; margin:8px 0 10px; }
  .walkthrough-intro { font-size:14px; line-height:1.5; }
  .walkthrough-location { margin:14px 0; padding:10px 12px; }
  .walkthrough-steps { margin:14px 0; }
  .walkthrough-steps li { margin-bottom:10px; }
}
@media (max-width:360px) {
  .topbar .brand-name { display:none; }
  .topbar .brand { width:30px; }
  .clip-card { grid-template-columns:96px minmax(0,1fr); min-height:168px; }
  .card-thumb { width:96px; height:150px; }
}

.mobile-project-name { display:none; }
@media (max-width:600px) {
  .mobile-project-name {
    display:block; margin:0 0 12px; padding:0 2px;
    color:var(--text); font-size:15px; font-weight:600;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
}
@media (min-width:361px) and (max-width:600px) { .topbar .brand-name { display:inline; } }
/* iPhone Safari zooms small form fields when they receive focus. Keep the
   visual viewport at page scale so the editor is centered after sign-in. */
@media (max-width:600px) {
  html, body { width:100%; max-width:100%; overflow-x:clip; }
  .topbar, .library, .layout { width:100%; max-width:100%; }
  input:is([type="text"],[type="email"],[type="password"],[type="url"],[type="number"],[type="search"]),
  textarea, select { font-size:16px !important; }
}
