/* ── punch.css ─────────────────────────────────────────────────────────────
   The punch / defense labeler owns its styling, the way chin_tuck 2.0–4.0 do.

   Deliberately NOT ../shared/style.css: that sheet still dresses eight other
   pages (rules, callout, impact, guard_drop, bodyshot, hip_rotation,
   bladedness, punch_directions, chin_tuck 0.0/1.0), and every one of them
   leans on its ids — #top-bar, #video-container, #shortcuts-bar, .punch-btn.
   Restyling it in place would silently redesign all of them. So this is a
   COPY-AND-DIVERGE, same call 2.0 made: shared/style.css is untouched and the
   punch page stops linking it.

   The token set, type scale, card/eyebrow pattern and segmented-control
   recipe below are lifted verbatim from chin_tuck_4.0 so the two tools read
   as one product rather than two eras of one.

   Apple's system palette in both schemes, an 8pt rhythm, and the video as the
   only saturated thing on screen — everything else is grey so the footage and
   the punch colours are the two things the eye can land on.
   ────────────────────────────────────────────────────────────────────────── */

:root {
  --bg: #f5f5f7;
  --card: #ffffff;
  --line: rgba(0,0,0,.10);
  --ink: #1d1d1f;
  --ink-dim: #6e6e73;
  /* Two blues and two greens, because a colour that reads well AS TEXT and a
     colour that white text can sit ON are not the same colour. The plain
     token is the text/stroke one and follows the scheme; the -solid token is
     a fill that always carries white, so it is pinned in both schemes to a
     value that clears 4.5:1 against white.
     Apple's own systemGreen (#34c759) is only 2.2:1 under white and systemBlue
     dark (#0a84ff) only 3.65:1 — both fine for a glyph, neither legal for the
     label on a primary button, which is exactly what this page puts on them. */
  --accent: #0071e3;        /* accent text and washes            */
  --accent-solid: #0071e3;  /* fills that carry white — 4.70:1   */
  --yes: #1c7c33;           /* green text on a light background  */
  --yes-solid: #1c7c33;     /* fills that carry white — 5.21:1   */
  --no: #ff3b30;
  --radius: 12px;
  --shadow: 0 1px 2px rgba(0,0,0,.04), 0 4px 16px rgba(0,0,0,.06);
  /* The one recessed fill on the page — segmented-control tracks, filled
     fields, skip-shaped buttons. Apple's own `systemFill`, which is a grey
     defined in the same way for both schemes rather than two hand-picked
     colours that drift apart. */
  --fill: rgba(120,120,128,.12);
  --fill-2: rgba(120,120,128,.20);
  /* Secondary ink for a label sitting ON one of those fills rather than on the
     card. --ink-dim outright lands at 4.44:1 there — just under the 4.5 a body
     label needs, because the fill lifts the background toward the text. Mixed
     back toward --ink it clears 6:1 in both schemes and still reads demoted.
     (chin_tuck_4.0 hit the identical 4.3:1 on its skip button; same fix.) */
  --ink-on-fill: color-mix(in srgb, var(--ink-dim) 70%, var(--ink));
  /* Same trap one step further: an accent-tinted wash pulls the background
     toward the very hue of the label on it, so plain --accent lands at 3.95:1
     there. Darkened in light, lightened in dark — 5.3:1 and 7.1:1. (4.0 calls
     the same colour --open-ink, for the same three disclosure pills.) */
  --accent-on-wash: color-mix(in srgb, var(--accent) 82%, black);
  /* The wash itself, named once so the tint and its ink cannot drift apart. */
  --accent-wash: color-mix(in srgb, var(--accent) 14%, transparent);
  /* Green and red washes for the drive link's status, with their own inks for
     the same reason --accent-on-wash exists: a tinted background pulls toward
     the hue of the label on it. Plain --yes on its wash is 4.01:1 in light and
     plain --no is 2.90:1, both under the bar; darkened they clear it. */
  --yes-wash: color-mix(in srgb, var(--yes) 15%, transparent);
  --no-wash:  color-mix(in srgb, var(--no) 15%, transparent);
  --yes-on-wash: color-mix(in srgb, var(--yes) 85%, black);
  --no-on-wash:  color-mix(in srgb, var(--no) 62%, black);
  /* Chrome that floats over content (nav bar, popover, toast) is a material,
     not a colour: mostly the background, blurred, so what is behind it still
     shows through as a hint of depth. */
  --chrome: rgba(255,255,255,.72);
  /* The timeline's "you are outside a round" hatching, and the seek track. */
  --stripe: rgba(0,0,0,.10);
  --track: rgba(120,120,128,.22);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #000000; --card: #1c1c1e; --line: rgba(255,255,255,.14);
    --ink: #f5f5f7; --ink-dim: #98989d;
    /* Text/wash colours come UP off a near-black card; the two -solid fills
       stay where they are, because what has to stay readable on them is the
       white label, not the page behind them. */
    --accent: #0a84ff; --yes: #30d158;
    --accent-on-wash: color-mix(in srgb, var(--accent) 55%, white);
    /* Off the background rather than into it, the opposite direction from
       light. Green already clears the bar at full strength (6.4:1); red does
       not — #ff3b30 on its own wash is 4.11:1 here — so it is lifted toward
       white until it does. */
    --yes-on-wash: var(--yes);
    --no-on-wash: color-mix(in srgb, var(--no) 78%, white);
    --shadow: 0 1px 2px rgba(0,0,0,.5);
    --fill: rgba(120,120,128,.24);
    --fill-2: rgba(120,120,128,.34);
    --chrome: rgba(28,28,30,.72);
    --stripe: rgba(255,255,255,.10);
    --track: rgba(120,120,128,.30);
  }
}

* { box-sizing: border-box; margin: 0; padding: 0; }
/* …except a <dialog>, which the browser centres with `margin: auto` on top
   of its own fixed inset:0 — the `margin: 0` above was silently overriding
   that and pinning every modal on this page to the top-left corner. Applies
   to all of them (shortcuts, "already labeled", agreement), so it belongs
   here next to the reset that broke it rather than in each dialog's rule. */
dialog { margin: auto; }
html, body { height: 100%; }
body {
  font: 15px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
        system-ui, sans-serif;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  /* Safe area: harmless on a Mac, load-bearing on an iPad in landscape where
     the home indicator otherwise sits on top of the transport row. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }
/* One tactile press for every button on the page rather than each rule
   reinventing it — the platform's controls all give the same small, quick
   "I felt that". Outline (not box-shadow) for focus, because several buttons
   already carry a shadow of their own and outline layers cleanly on either. */
button { transition: transform .1s ease; }
button:active:not(:disabled) { transform: scale(.96); }
button:focus-visible, input:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  button { transition: none; }
  button:active:not(:disabled) { transform: none; }
}

/* labeler_name.js prepends a full-width identity strip to every page. Here the
   name has a real home in the navigation bar (#labeler-input below, which that
   same script keeps in sync), so the strip would be a second copy of one short
   word above a tool whose whole point is the video. Page-scoped — every other
   labeler keeps the shared bar.
   #labeler-badge is app.js's own copy of the name; hidden for the same reason,
   and with !important because app.js sets `display:inline` inline. */
#cm-labeler-chip { display: none !important; }
#labeler-badge   { display: none !important; }

/* (.sr-only lived here to hide the training/stance <select>s behind their
   segmented controls. Those selects are the visible UI now — see
   .session-select — and nothing else on the page used the class.) */

#app { height: 100%; display: flex; flex-direction: column; overflow: hidden; }

/* ── navigation bar ──────────────────────────────────────────────────────── */
/* Title on the left, actions on the right, one hairline underneath: the
   platform's own top-level chrome. Translucent over --bg so it reads as a
   layer above the content rather than another block of the same page. */
#top-bar {
  flex: none;
  display: flex; align-items: center; gap: 16px;
  padding: 0 16px; height: 56px;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 30;
}
.nav-lead { display: flex; align-items: center; gap: 16px; min-width: 0; flex: 1; }
.nav-title {
  font-size: 17px; font-weight: 640; letter-spacing: -.02em;
  white-space: nowrap; flex: none;
}
/* The video row IS a .src-field (see the block further down) — same height,
   fill, radius, trailing copy button and confirmation chip, and the same
   green wash once it holds something real. Only the leading control differs:
   a file-picker button here, a pasted URL there. Everything below is just
   that difference. Left padding drops to 4px because the button carries its
   own. */
/* Only the left padding differs — the picker button carries its own. Gap is
   deliberately NOT overridden: it has to match .src-field's, or the trailing
   copy button lands a couple of px off the one in the row below. */
#video-loader { padding-left: 4px; }
#btn-pick-file {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 26px; padding: 0 8px; border-radius: 6px;
  border: 0; background: transparent; color: inherit;
  font-size: 12.5px; font-weight: 510; white-space: nowrap;
}
/* --fill-2, not a black tint: this sits ON --fill, and a black wash over an
   already-dark surface is invisible in dark mode. The systemFill greys are
   defined to read in both schemes. */
#btn-pick-file:hover { background: var(--fill-2); }
#btn-pick-file svg { width: 13px; height: 13px; flex: none; }

/* player.js writes the FULL name as real text (never truncated in JS) — the
   ellipsis here is purely visual, so the whole name is still there to select
   and copy even where it's cut off on screen. No max-width cap: reading the
   whole name is how you confirm you opened the right clip. */
#video-name {
  flex: 1; min-width: 0;
  font-size: 12.5px; font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-right: 6px;
}
/* Once a file has actually decoded the row goes green as a whole (.ok on the
   field, exactly like the link row's "Saved" state) — so the name only needs
   the weight bump, not a colour of its own. */
#video-name.loaded { font-weight: 510; }

/* Folder-connect rows (video + skeleton, each its own connection) — same
   leading-button pattern as #btn-pick-file above, plus the same .ok green
   wash once a folder is actually connected (see vfPaint() in
   video-folder.js). Shared classes since the two rows are otherwise
   identical, just pointed at different IDs by video-folder.js. */
/* .src-field.folder-connect-row, not just .folder-connect-row: both are
   single-class selectors otherwise, so whichever one's rule comes LATER in
   this file wins on equal specificity — and .src-field's own shorthand
   (padding: 0 8px 0 12px, further down) was winning that fight and
   resetting padding-left back to 12px, silently indenting these two rows
   past #video-loader/#skeleton-loader/#predictions-loader (ID selectors,
   which always win regardless of order). Two classes beats one regardless
   of source order — same fix as guaranteed there. */
.src-field.folder-connect-row { padding-left: 4px; }
.btn-connect-folder {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 26px; padding: 0 8px; border-radius: 6px;
  border: 0; background: transparent; color: inherit;
  font-size: 12.5px; font-weight: 510; white-space: nowrap;
}
.btn-connect-folder:hover { background: var(--fill-2); }
.btn-connect-folder svg { width: 13px; height: 13px; flex: none; }
.folder-connect-name {
  flex: 1; min-width: 0;
  font-size: 12.5px; font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-right: 6px;
}
.folder-connect-row.ok .folder-connect-name { font-weight: 510; }
.btn-forget-folder {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px; border: 0;
  background: transparent; color: inherit; opacity: .65;
}
.btn-forget-folder:hover { opacity: 1; background: var(--fill-2); }
.btn-forget-folder svg { width: 11px; height: 11px; }
.btn-forget-folder[hidden] { display: none; }

/* Skeleton row — same leading-button pattern as #btn-pick-file above. */
#skeleton-loader { padding-left: 4px; }
#btn-pick-skeleton {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 26px; padding: 0 8px; border-radius: 6px;
  border: 0; background: transparent; color: inherit;
  font-size: 12.5px; font-weight: 510; white-space: nowrap;
}
#btn-pick-skeleton:hover { background: var(--fill-2); }
#btn-pick-skeleton svg { width: 13px; height: 13px; flex: none; }
#skeleton-name {
  flex: 1; min-width: 0;
  font-size: 12.5px; font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-right: 6px;
}
/* Reuses the speed-menu's "active" look for "overlay is currently on" —
   same green-accent language as everywhere else a toggle is engaged. */
#btn-toggle-skeleton {
  flex: none; height: 24px; padding: 0 10px; border-radius: 999px;
  border: 0; background: var(--fill-2); color: var(--ink);
  font-size: 11.5px; font-weight: 590; white-space: nowrap;
}
#btn-toggle-skeleton.speed-active { background: var(--accent-solid); color: #fff; }
#btn-toggle-skeleton[hidden] { display: none; }

/* "+ Add more" — skeleton rounds and prediction models alike. Dashed
   outline rather than a filled pill on purpose: it reads as "add to this"
   rather than as a state toggle (#btn-toggle-skeleton/predictions' chips
   own that solid-pill language already), and only appears once there is
   something to add to — see setupSkeletonLoader()/setupPredictionsLoader(). */
#btn-add-more-skeletons, #btn-add-more-predictions {
  flex: none; height: 24px; padding: 0 9px; border-radius: 999px;
  border: 1px dashed var(--ink-dim); background: transparent; color: var(--ink-dim);
  font-size: 11.5px; font-weight: 590; white-space: nowrap;
}
#btn-add-more-skeletons:hover, #btn-add-more-predictions:hover {
  color: var(--ink); border-color: var(--ink-dim); background: var(--fill-2);
}
#btn-add-more-skeletons[hidden], #btn-add-more-predictions[hidden] { display: none; }

/* Predictions row — same leading-button pattern as the two above. */
#predictions-loader { padding-left: 4px; }
#btn-pick-predictions {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: 26px; padding: 0 8px; border-radius: 6px;
  border: 0; background: transparent; color: inherit;
  font-size: 12.5px; font-weight: 510; white-space: nowrap;
}
#btn-pick-predictions:hover { background: var(--fill-2); }
#btn-pick-predictions svg { width: 13px; height: 13px; flex: none; }
#predictions-name {
  flex: 1; min-width: 0;
  font-size: 12.5px; font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-right: 6px;
}
/* One chip per loaded prediction model — see renderPredictionsList() in
   predictions.js. Wraps rather than scrolling; the row is hidden outright
   while there's nothing loaded. Indented to line up under the leading
   button's icon+label above it rather than the row's bare left edge. */
#predictions-list {
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 0 8px 0 34px;
}
#predictions-list[hidden] { display: none; }
.pred-chip {
  display: flex; align-items: center; gap: 5px;
  padding: 3px 4px 3px 9px; border-radius: 999px;
  background: var(--who, var(--ink-dim)); color: #fff;
  font-size: 11px; font-weight: 590; line-height: 1;
}
.pred-chip-name { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pred-chip-count { opacity: .75; font-weight: 500; }
/* Hidden (per the Others menu's same per-owner mute) reads exactly like a
   muted labeler elsewhere — dimmed, not restyled, so it's still obviously
   the same chip once shown again. */
.pred-chip.pred-hidden { opacity: .45; }
.pred-chip button {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  height: 18px; padding: 0 6px; border-radius: 999px; border: 0;
  background: rgba(255,255,255,.2); color: #fff;
  font-size: 10px; font-weight: 620;
}
.pred-chip button:hover { background: rgba(255,255,255,.35); }
.pred-chip-remove { width: 18px; padding: 0; font-size: 12px; }

.nav-actions { display: flex; align-items: center; gap: 8px; flex: none; }
/* The two halves either side of .nav-div below — identity/language, then
   session status and actions on the current video. Each is its own flex
   run so the gap between its own members stays 8px regardless of the wider
   gap the divider puts between the halves. */
.nav-group { display: flex; align-items: center; gap: 8px; }
/* Same hairline the video-controls toolbar's own .tdiv uses, just closer to
   .tdiv's spirit than its exact rule (that one assumes it always has a
   neighbour snug on both sides; this one needs its own left/right breathing
   room since .nav-group already carries its members' gap). */
.nav-div { flex: none; width: 1px; height: 20px; background: var(--line); margin: 0 2px; }

/* Your name. A filled field with a person glyph, the way a system settings
   row looks — no border, no label, because the glyph is the label.
   `display:flex !important` is load-bearing: labeler_name.js hides inputs
   with this id by inline style on load, assuming its own bar is the name UI,
   and an inline style only loses to !important. */
/* min-width shared with #lang-field/#btn-agreement/#btn-problems/
   #admin-presence beside it — five different kinds of control (a text
   input, a <select>, two buttons, a status chip) that used to each size to
   their own content and read as five mismatched widths in a row. 135px is
   this one's own natural width (icon + the 96px input) — the widest of the
   five — so it's the floor the other four are set to match rather than an
   arbitrary number. */
#name-field {
  display: flex !important; align-items: center; gap: 6px;
  height: 32px; min-width: 135px; padding: 0 10px; border-radius: 8px;
  background: var(--fill); color: var(--ink-on-fill);
}
#name-field svg { width: 13px; height: 13px; flex: none; }
#labeler-input {
  display: block !important;
  width: 96px; border: 0; background: none; color: var(--ink);
  font-size: 13px; font-weight: 510;
}
#labeler-input::placeholder { color: var(--ink-on-fill); font-weight: 400; }
#labeler-input:focus { outline: none; }
#name-field:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Committed — the name in the box is the name every save is filed under.
   Same green-field confirmation 4.0 uses, for the same reason: until now the
   only sign it had taken was that saves stopped going to "Labeler 1". */
#name-field.saved { background: color-mix(in srgb, var(--yes) 14%, transparent); }
#name-field.saved svg { color: var(--yes); }

#lang-field {
  display: flex; align-items: center; gap: 6px;
  height: 32px; min-width: 135px; padding: 0 8px 0 10px; border-radius: 8px;
  background: var(--fill); color: var(--ink-on-fill);
}
#lang-field svg { width: 13px; height: 13px; flex: none; }
#lang-select {
  border: 0; background: none; color: var(--ink);
  font-size: 13px; font-weight: 510;
}
#lang-select:focus { outline: none; }
#lang-field:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Unsaved-label count. Amber rather than red: nothing is lost — the labels
   are on disk and will retry — so this is "not done yet", not "broken". */
#outbox-chip {
  height: 32px; padding: 0 11px; border-radius: 8px; border: 0;
  background: color-mix(in srgb, #b26a00 16%, transparent); color: #8a5200;
  font-size: 12.5px; font-weight: 590; white-space: nowrap;
}
#outbox-chip[hidden] { display: none; }
#outbox-chip:hover { background: color-mix(in srgb, #b26a00 26%, transparent); }
@media (prefers-color-scheme: dark) {
  #outbox-chip { color: #f0b355; background: color-mix(in srgb, #f0b355 20%, transparent); }
}

/* Standing headcount of everyone currently in admin mode (the viewer
   included) — see updateAdminPresenceChip() in app.js. Amber (the unsaved
   chip's own "aware of, not an error" colour) is the default/base look —
   2 to 4 people; .tone-solo (just the viewer) and .tone-busy (5+, crowded
   enough that two quiet overwrites is a real risk) override it. */
#admin-presence {
  display: inline-flex; align-items: center; justify-content: center;
  height: 32px; min-width: 135px; padding: 0 11px;
  border-radius: 8px; white-space: nowrap;
  background: color-mix(in srgb, #b26a00 16%, transparent); color: #8a5200;
  font-size: 12.5px; font-weight: 590;
}
#admin-presence[hidden] { display: none; }
#admin-presence.tone-solo { background: var(--yes-wash); color: var(--yes-on-wash); }
#admin-presence.tone-busy { background: var(--no-wash); color: var(--no-on-wash); }
@media (prefers-color-scheme: dark) {
  /* Base (amber, no tone class) only — .tone-solo/.tone-busy above already
     win on specificity (ID+class beats this plain ID rule) regardless of
     the media query, and --yes-wash/--no-wash etc. already carry their own
     dark-mode redefinitions at the top of this file. */
  #admin-presence { color: #f0b355; background: color-mix(in srgb, #f0b355 20%, transparent); }
}

/* Who is reviewing in admin mode — the `actor` on every logged correction,
   see setupAdminReviewer() in app.js. Same footprint as #admin-presence
   beside it; .tone-unnamed = nobody named yet, so corrections would log as
   plain "Admin" — the red wash is the nudge to click it. */
#admin-reviewer {
  display: inline-flex; align-items: center; justify-content: center;
  height: 32px; min-width: 135px; padding: 0 11px;
  border: 0; border-radius: 8px; white-space: nowrap; cursor: pointer;
  background: var(--yes-wash); color: var(--yes-on-wash);
  font: inherit; font-size: 12.5px; font-weight: 590;
}
#admin-reviewer[hidden] { display: none; }
#admin-reviewer.tone-unnamed { background: var(--no-wash); color: var(--no-on-wash); }

/* Admin-only, shown by setupAgreement(). Same min-width as #name-field/
   #lang-field/#btn-problems/#admin-presence in .nav-group — five different
   kinds of control that used to each size to their own content and read as
   five mismatched widths in a row (135px is #name-field's own natural
   width, the widest of the five — see the comment there). */
#btn-agreement {
  display: inline-flex; align-items: center; justify-content: center;
  height: 32px; min-width: 135px; padding: 0 11px; border-radius: 8px; border: 0;
  background: var(--fill); color: var(--ink);
  font-size: 12.5px; font-weight: 510; white-space: nowrap;
}
#btn-agreement[hidden] { display: none; }
#btn-agreement:hover { background: var(--fill-2); }

/* Everyone's — see problems.js. Same shape/width as #btn-agreement beside
   it, plus a count badge (amber, matching every other "worth a look, not an
   error" signal in this tool) that only shows once something's open. */
#btn-problems {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 32px; min-width: 135px; padding: 0 11px; border-radius: 8px; border: 0;
  background: var(--fill); color: var(--ink);
  font-size: 12.5px; font-weight: 510; white-space: nowrap;
}
#btn-problems:hover { background: var(--fill-2); }
#problems-count {
  padding: 1px 6px; border-radius: 999px;
  background: color-mix(in srgb, #b26a00 18%, transparent); color: #8a5200;
  font-size: 11px; font-weight: 650; font-variant-numeric: tabular-nums;
}
#problems-count[hidden] { display: none; }
@media (prefers-color-scheme: dark) {
  #problems-count { color: #f0b355; background: color-mix(in srgb, #f0b355 22%, transparent); }
}

/* 32x32 is below the 44pt touch minimum on purpose: these two are pointer-only
   chrome on a desktop tool, and the hit area is padded out to 44 by the
   ::before below so a stylus or a touchscreen still lands them. */
.icon-btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 8px;
  color: var(--ink-dim); border: 1px solid transparent;
}
.icon-btn::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.icon-btn:hover { background: var(--fill); color: var(--ink); }
.icon-btn[aria-expanded="true"] {
  background: var(--accent-wash);
  color: var(--accent);
}
.icon-btn svg { width: 16px; height: 16px; }

/* .menu-item is the shared row style for .ctx-menu below (the timeline
   right-click menu) — the pull-down "other labelers" menu that used to be
   the other consumer (.menu/.menu-panel/.menu-sep) was removed along with
   its button. */
.menu-item {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; border-radius: 7px;
  font-size: 13px; color: var(--ink); text-decoration: none;
}
.menu-item:hover { background: var(--fill); }
.menu-item svg { width: 13px; height: 13px; color: var(--ink-dim); flex: none; }

/* Right-click menu on a timeline chip — same chrome as .menu-panel above, but
   position:fixed and placed by JS at the cursor instead of anchored under a
   button (see setupSegmentContextMenu() in ui.js). .menu-item is reused for
   the rows; the two overrides below turn it from an <a> back into a real
   <button> (no default border/background/font) without touching the shared
   class other menus rely on. */
.ctx-menu {
  position: fixed; z-index: 100; min-width: 200px; padding: 6px;
  border-radius: 12px; background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line);
  box-shadow: 0 8px 32px rgba(0,0,0,.18);
}
.ctx-menu[hidden] { display: none; }
.ctx-menu .menu-item {
  width: 100%; border: none; background: none; cursor: pointer;
  font: inherit; text-align: left;
}
.ctx-menu .menu-item-danger:hover { color: var(--no); background: var(--fill-2); }
.ctx-sep { height: 1px; margin: 5px 10px; background: var(--line); }

/* ── video source ─────────────────────────────────────────────────────────── */
/* What this video IS — the local file AND the Drive link that keys the
   sheet, grouped as one bar (each its own pill) instead of one living in
   the nav bar and the other in a strip below it. Training type and stance
   used to live here too; they're per-session context you set once and
   rarely revisit, so they moved down next to the move catalogue itself —
   see #punch-session — leaving this bar for just the two things that
   answer "what video is this".
   Stacked, each pill on its own full-width row: side by side left the file
   name squeezed into a couple hundred pixels and ellipsis-truncated even at
   a reasonable length, which is exactly the text most worth reading in
   full — it's how you confirm you opened the right clip. */
#video-source-bar {
  flex: none;
  display: flex; flex-direction: column; gap: 8px;
  padding: 8px 16px;
  background: var(--card);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 20;
}
/* Collapses #video-source-rows away once the four rows below are set up
   and don't need another look — see setupVideoSourceFold() in ui.js. */
#video-source-toggle {
  display: inline-flex; align-items: center; gap: 5px; align-self: flex-start;
  height: 18px; padding: 0 2px; border: 0; background: none; color: var(--ink-dim);
  font-size: 11px; font-weight: 590; text-transform: uppercase; letter-spacing: .04em;
}
#video-source-toggle:hover { color: var(--ink); }
#video-source-toggle svg { width: 10px; height: 10px; flex: none; transition: transform .15s ease; }
#video-source-toggle[aria-expanded="false"] svg { transform: rotate(-90deg); }
#video-source-rows { display: flex; flex-direction: column; gap: 8px; }
#video-source-rows[hidden] { display: none; }
.src-field {
  width: 100%;
  display: flex; align-items: center; gap: 8px;
  height: 32px; padding: 0 8px 0 12px; border-radius: 8px;
  background: var(--fill); color: var(--ink-on-fill);
}
.src-field svg { width: 13px; height: 13px; flex: none; }
.src-field:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
#drive-link {
  flex: 1; min-width: 0; border: 0; background: none; color: var(--ink);
  font-size: 13px;
}
#drive-link:focus { outline: none; }
#drive-link::placeholder { color: var(--ink-on-fill); }

.src-field-wrap { position: relative; width: 100%; }

/* Chevron button opening #video-picker-panel — same slot/sizing as the copy
   buttons just after it, so the two sit flush together. */
#btn-pick-video {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px; border: 0;
  background: transparent; color: inherit; opacity: .65;
}
#btn-pick-video:hover, #btn-pick-video.open { opacity: 1; background: var(--fill-2); }
#btn-pick-video svg { width: 12px; height: 12px; }
#btn-pick-video.open svg { transform: rotate(180deg); }

#video-picker-panel {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 30;
  background: var(--card); border-radius: 10px;
  box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.14);
  padding: 8px; display: flex; flex-direction: column; gap: 6px;
}
#video-picker-panel[hidden] { display: none; }
.vp-tabs { display: flex; gap: 4px; }
.vp-tab {
  flex: 1; height: 26px; border: 0; border-radius: 6px; background: none;
  color: var(--ink-dim); font-size: 12.5px; cursor: pointer;
}
.vp-tab:hover { background: var(--fill); }
.vp-tab.active { background: var(--fill-2); color: var(--ink); font-weight: 600; }
/* A round this labeler marked as labeled (app-rounds.js) — or, for Admin
   and Analyst, that anyone did. */
.vp-row.vp-done { background: var(--yes-wash); }
.vp-row.vp-done .vp-name { color: var(--yes-on-wash); font-weight: 600; }
/* The Labeling done button under the link box, on an app round only. */
#btn-app-done {
  align-self: flex-start; height: 28px; padding: 0 12px; border: 0; border-radius: 7px;
  background: var(--fill-2); color: var(--ink); font-size: 12.5px; cursor: pointer;
}
#btn-app-done[hidden] { display: none; }
#btn-app-done.done { background: var(--yes-wash); color: var(--yes-on-wash); font-weight: 600; }
#btn-app-done:disabled { opacity: .6; cursor: default; }
.vp-called { flex: none; color: var(--ink-dim); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.vp-signin {
  margin-top: 8px; height: 28px; padding: 0 12px; border: 0; border-radius: 7px;
  background: var(--accent); color: #fff; font-size: 12.5px; cursor: pointer;
}
#video-picker-search {
  height: 30px; padding: 0 10px; border-radius: 7px; border: 0;
  background: var(--fill); color: var(--ink); font-size: 13px;
}
#video-picker-search:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
#video-picker-list { max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.vp-row {
  display: flex; align-items: center; gap: 6px;
  width: 100%; text-align: left; border: 0; background: none;
  color: var(--ink); font-size: 12.5px; padding: 7px 9px; border-radius: 6px;
  /* Without this, a flex column with a capped max-height (below) treats
     each row's overflow:hidden as an invitation to shrink it past its own
     content height instead of just scrolling — dozens of rows then get
     squeezed thinner than their own text line, overlapping into an
     unreadable smear. flex:none pins every row to its natural height so
     the LIST scrolls instead of the ROWS collapsing. This is the row's OWN
     flex-item sizing (inside #video-picker-list); it says nothing about
     .vp-row itself being display:flex for its own children, which is what
     lets the version badges sit flush right of a truncating name below. */
  flex: none;
}
.vp-row:hover, .vp-row.active { background: var(--fill-2); }
.vp-n { flex: none; display: inline-block; min-width: 20px; color: var(--ink-on-fill); font-variant-numeric: tabular-nums; }
.vp-name {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* One badge per labeler (J/A) — from the tracking sheet's
   labeling_version_<Name> columns (Code.js's doGetTrackingVideos(),
   statusBadges()/versionMeta() in app.js). Color and text both track the
   VERSION itself: v1 green, v0 yellow, no version at all red "none" — the
   full John_progress/Arianne_progress status text sits in the title
   tooltip regardless, since it's still useful context, just not what
   decides the badge's look. */
.vp-ver {
  flex: none; font-size: 10px; font-weight: 620; letter-spacing: .02em;
  padding: 1px 5px; border-radius: 5px; font-variant-numeric: tabular-nums;
}
.vp-v-v1    { background: var(--yes-wash); color: var(--yes-on-wash); }
.vp-v-v0    { background: color-mix(in srgb, #b26a00 16%, transparent); color: #8a5200; }
.vp-v-empty { background: var(--no-wash); color: var(--no-on-wash); }
.vp-v-other { background: var(--fill-2); color: var(--ink-dim); }
@media (prefers-color-scheme: dark) {
  .vp-v-v0 { color: #f0b355; background: color-mix(in srgb, #f0b355 20%, transparent); }
}
/* Fixed-width column per labeler (John's, then Arianne's) — always
   rendered even when empty, so a row where only one of the two has a
   badge doesn't pull it into the other's slot. Right-aligned within its
   own column so the badge itself still hugs the row's trailing edge. */
.vp-slot { flex: none; width: 42px; display: flex; justify-content: flex-end; }
.vp-empty, .vp-loading {
  padding: 10px 9px; font-size: 12.5px; color: var(--ink-on-fill); text-align: center;
}

/* One-click copy on top of each field already being real, selectable text —
   shown only once there's something worth copying. Briefly turns green on
   click, the same "it happened" language #name-field's save flash and the
   round markers' colors already use elsewhere. Both rows share this. */
#btn-copy-link, #btn-copy-name {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px; border: 0;
  background: transparent; color: inherit; opacity: .65;
}
#btn-copy-link:hover, #btn-copy-name:hover { opacity: 1; background: var(--fill-2); }
#btn-copy-link[hidden], #btn-copy-name[hidden] { display: none; }
#btn-copy-link svg, #btn-copy-name svg { width: 12px; height: 12px; }
#btn-copy-link.copied, #btn-copy-name.copied { opacity: 1; color: var(--yes); }

/* The trailing confirmation chip, on both rows: "Loaded" for the file,
   "Saved" for the link. The FIELD carries the state — the whole control
   tints, leading icon included — because the question being answered is
   about the field, not about a badge next to it.
   Unlike the name field's 1.8s green flash these PERSIST: "did my video
   open" and "are my labels being filed under this link" are questions asked
   at any moment, so the answer has to still be there when you look up. */
.field-status {
  flex: none; display: flex; align-items: center; gap: 5px;
  font-size: 11.5px; font-weight: 590; white-space: nowrap;
  /* Wide enough for the longer of the two resting words ("Loaded" vs
     "Saved") and right-aligned, so the copy buttons ahead of them line up
     between the two rows instead of sitting a few px apart. Longer error
     text just pushes past it. */
  min-width: 64px; justify-content: flex-end;
}
.field-status[hidden] { display: none; }
.field-status svg { width: 12px; height: 12px; flex: none; }
/* Only rendered in the error state — see setLinkStatus() in ui.js. */
.link-retry {
  flex: none; margin-left: 2px; padding: 2px 8px; border-radius: 5px;
  border: 0; background: currentColor; color: var(--card);
  font-size: 11px; font-weight: 620; letter-spacing: 0;
}
.link-retry:hover { opacity: .85; }
.src-field.ok      { background: var(--yes-wash); color: var(--yes-on-wash); }
.src-field.err     { background: var(--no-wash);  color: var(--no-on-wash); }
.src-field.syncing { color: var(--ink-on-fill); }
.spin { animation: spin 900ms linear infinite; transform-origin: 50% 50%; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }

/* A hairline between clusters of controls, in both toolbars and the transport
   row. This is what replaced the TYPE / STANCE micro-caps: two segmented
   controls side by side read as one five-segment control, so SOMETHING has to
   separate them — but "Shadowboxing | Sparring | Bagwork" already says what it
   is, and a label spelling that out was noise standing in for a rule. */
.tdiv { flex: none; width: 1px; height: 22px; background: var(--line); }

/* Segmented control — three or fewer mutually exclusive options, which is
   exactly what the platform's segmented control is for. Recessed track, the
   selected segment raised out of it on a card-coloured chip. */
.seg {
  display: flex; gap: 3px; padding: 2px; border-radius: 9px; background: var(--fill);
}
.seg button {
  padding: 5px 12px; border-radius: 7px; border: 0;
  font-size: 12.5px; font-weight: 500; color: var(--ink-on-fill);
  white-space: nowrap;
  transition: background .18s ease, color .18s ease, box-shadow .18s ease,
              transform .1s ease;
}
/* Two attribute names, one look: aria-checked is right for the radiogroup
   controls (Type, Stance) above, aria-selected is the correct one for an
   actual tablist (#label-tabs below) — the segmented-control APPEARANCE
   doesn't care which ARIA pattern earned it. */
.seg button[aria-checked="true"], .seg button[aria-selected="true"],
.seg button[aria-pressed="true"] {
  background: var(--card); color: var(--ink); font-weight: 590;
  box-shadow: 0 1px 3px rgba(0,0,0,.10), 0 0 0 .5px rgba(0,0,0,.04);
}
@media (prefers-color-scheme: dark) {
  .seg button[aria-checked="true"], .seg button[aria-selected="true"],
  .seg button[aria-pressed="true"] {
    background: color-mix(in srgb, var(--ink) 22%, var(--card));
    box-shadow: 0 1px 3px rgba(0,0,0,.5);
  }
}
@media (prefers-reduced-motion: reduce) { .seg button { transition: none; } }

/* The Labels panel's own tablist — same segmented-control look as Type/Stance
   above, sized to the sidebar instead of hugging its label text: both tabs
   split the card's width evenly, which the toolbar's controls never needed
   because their two segments name different-length things. */
#label-tabs { width: 100%; margin: 8px 0 10px; }
#label-tabs button { flex: 1; text-align: center; }

/* ── main layout ─────────────────────────────────────────────────────────── */
#main-layout {
  flex: 1; min-height: 0;
  display: flex; gap: 12px; padding: 12px;
  align-items: stretch;
}

.card {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 16px;
}
/* Settings.app's grouped-list pattern: a small caps label names the section,
   so a card never has to be read to find out what it is. */
.card-eyebrow {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; font-weight: 590; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-dim); margin: 0 0 10px;
}

/* A collapsible section's own header, layered on TOP of .card-eyebrow (Move
   Type/Session, #punch-panel below) or .punch-group-header (Offense/Defense/
   Other, one level in — see buildPunchButtons() in app.js): turns the label
   into a real button with a chevron that rotates to say which way it goes.
   Reused wherever something folds — same idea as #video-source-toggle
   above, just generalised instead of copy-pasted a third and fourth time. */
.fold-toggle {
  width: 100%; border: 0; background: none; padding: 0; text-align: left; cursor: pointer;
}
.fold-toggle:hover { color: var(--ink); }
.fold-toggle .fold-chev {
  width: 9px; height: 9px; flex: none; color: inherit; transition: transform .15s ease;
}
.fold-toggle[aria-expanded="false"] .fold-chev { transform: rotate(-90deg); }
@media (prefers-reduced-motion: reduce) { .fold-toggle .fold-chev { transition: none; } }

/* ── left sidebar: the move catalogue ────────────────────────────────────── */
#punch-panel {
  width: 272px; flex: none;
  background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 16px; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--track) transparent;
  /* Positions #punch-loading below — an absolutely placed overlay pinned to
     this panel's own box rather than scrolling away with its content. */
  position: relative;
}
#punch-panel::-webkit-scrollbar { width: 8px; }
#punch-panel::-webkit-scrollbar-thumb { background: var(--track); border-radius: 4px; }
#punch-buttons { display: flex; flex-direction: column; }
/* The positioning context for #punch-loading — scoped to the catalogue so
   the overlay covers the move buttons and nothing else. On #punch-panel it
   also covered Session below, and (the panel being a scroll container)
   stretched to the full scroll height rather than the visible box. */
#punch-catalogue { position: relative; }

/* Blocks the move catalogue while a just-opened video's own labels are still
   in flight — see setLoadingLocked() in app.js. Sits over #punch-buttons
   rather than replacing it, so nothing reflows when it clears. */
#punch-loading {
  position: absolute; inset: -6px -4px; z-index: 5;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; border-radius: 12px;
  background: color-mix(in srgb, var(--card) 82%, transparent);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  font-size: 12.5px; font-weight: 510; color: var(--ink-dim);
}
#punch-loading[hidden] { display: none; }
.spinner {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2.5px solid var(--fill); border-top-color: var(--accent-solid);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }

/* ── session: training type + stance ─────────────────────────────────────── */
/* Moved down here from the toolbar above the video — see the comment on
   #video-source-bar. A hairline (not another card) separates it from the
   move catalogue above: same panel, a different kind of choice.
   Label left, value right on ONE row — the settings-row shape, and the only
   thing that fits: as a segmented control the three training options had to
   share ~240px and "Shadowboxing" wrapped to three lines. */
#punch-session {
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line);
}
.session-row {
  display: flex; align-items: center; gap: 10px;
  min-height: 32px; margin-top: 8px;
}
.session-row:first-of-type { margin-top: 4px; }
/* Fixed label column so the two pop-ups share a left edge and the same
   width — "Training" and "Stance" are different lengths, and without this
   the controls stagger. */
.session-label {
  flex: none; min-width: 58px;
  font-size: 12.5px; font-weight: 500; color: var(--ink-dim);
}
/* The pop-up itself: a filled pill matching #lang-field/#name-field in the
   nav bar, sized to the row's remaining width so the two line up with each
   other regardless of how long the chosen value is. */
.session-select {
  flex: 1; min-width: 0; height: 30px;
  padding: 0 6px 0 9px; border: 0; border-radius: 8px;
  background: var(--fill); color: var(--ink);
  font-size: 12.5px; font-weight: 510;
}
.session-select:hover { background: var(--fill-2); }

.punch-group-header {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 590; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-dim);
  margin: 18px 0 8px;
}
.punch-group-header:first-child { margin-top: 0; }

/* The offense matrix. Twelve punches used to be twelve rows in a flat list;
   they are really SIX punches asked twice — head or body — which is exactly
   how the keyboard already spelled it (1–6 vs Shift+1–6). Laid out as a grid
   the shortcut map becomes the picture: read down for the punch, across for
   the target, and the keycap in each cell teaches the binding by sitting
   where the mouse already is. */
.pmatrix {
  display: grid; grid-template-columns: 1fr 48px 48px; gap: 6px;
  align-items: center;
}
.pmh {
  font-size: 10.5px; font-weight: 590; letter-spacing: .03em;
  text-transform: uppercase; color: var(--ink-dim); text-align: center;
  padding-bottom: 2px;
}
.prow-name {
  font-size: 13px; font-weight: 500; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Defense pairs off lead/rear the same way, so it gets the same two columns —
   with the names in the cells, because there is no shared row noun to hoist. */
.pgrid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.pgrid1 { display: grid; grid-template-columns: 1fr; gap: 6px; }

/* Explicit, not left to the browser's own [hidden]{display:none} default:
   that UA rule and .pmatrix/.pgrid2/.pgrid1's own `display: grid` above are
   the same specificity, and author styles beat the UA stylesheet on a tie —
   so without this, a folded group (see wireMoveGroupFold() in app.js)
   toggled its chevron and kept the grid fully visible underneath it. */
.pmatrix[hidden], .pgrid2[hidden], .pgrid1[hidden] { display: none; }

.punch-btn {
  display: flex; align-items: center; gap: 7px;
  min-height: 38px; padding: 0 9px; border-radius: 9px;
  border: 1px solid var(--line); background: transparent;
  font-size: 12.5px; font-weight: 500; text-align: left;
  transition: background .12s ease, border-color .12s ease, transform .1s ease;
  /* Without this, a .pgrid2/.pgrid1 button (a grid item) won't shrink below
     its unwrapped text width — a grid item's automatic minimum size is its
     max-content size unless something resets it, so a long translated label
     ("Задний слип") pushed the button past its column and into the video
     panel instead of letting .pname's ellipsis (below) actually kick in. */
  min-width: 0;
}
.punch-btn:hover { border-color: var(--ink-dim); }
/* Matrix cells hold a dot and a keycap only — the punch's name is the row
   label to their left, and repeating it twice per row was the thing that made
   twelve buttons unreadable in the first place. Centred, since there is no
   text to align to. */
.punch-btn.cell { justify-content: center; gap: 6px; padding: 0 4px; }
.punch-btn .pname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; }
/* The colour that this punch will be on the timeline and on the video overlay.
   A dot, never text: the catalogue's palette is tuned for 6px segments on a
   black seek bar and several of them (#ffdd00, #88ff00) fall far below 4.5:1
   as a foreground colour on --card. */
.punch-btn .dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  box-shadow: 0 0 0 1px rgba(0,0,0,.12);
}
.punch-btn .shortcut {
  flex: none; min-width: 20px; padding: 2px 5px; border-radius: 5px;
  background: var(--fill); color: var(--ink-on-fill);
  font: 500 10.5px/1.3 ui-monospace, "SF Mono", Menlo, monospace;
  text-align: center;
}
.punch-btn.selected {
  background: var(--accent-solid); border-color: transparent; color: #fff;
  font-weight: 590;
}
.punch-btn.selected .shortcut { background: rgba(0,0,0,.25); color: #fff; }
.punch-btn.selected .dot { box-shadow: 0 0 0 1.5px rgba(255,255,255,.85); }
/* Nothing to pick a type FOR before a start time exists — see
   updateMoveButtonsEnabled() in app.js. Dimmed and inert rather than just
   inert, so it reads as "not yet", not as broken. */
.punch-btn:disabled { opacity: .4; cursor: not-allowed; }
.punch-btn:disabled:hover { border-color: var(--line); }
/* Whatever move is under the playhead RIGHT NOW during playback — see
   highlightActiveMoveButtons() in app.js, fed by the same activeLabels
   updateVideoOverlay() already computes for the tags over the video. Its
   own move's colour (--play-color, set inline), not the flat accent
   .selected uses — .selected is "this is what I'm about to log", this is
   "this is what's playing", and the two can be true of different buttons
   at once (mid-workflow on one move while scrubbing past another). */
.punch-btn.playing-now {
  border-color: var(--play-color, var(--accent-solid));
  background: color-mix(in srgb, var(--play-color, var(--accent-solid)) 14%, transparent);
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--play-color, var(--accent-solid)) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) { .punch-btn { transition: none; } }

/* ── centre: the video stage ─────────────────────────────────────────────── */
#video-container {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 10px;
}

/* Always black, in both schemes. The footage is the content and the content is
   the star: a light-grey mat around a dark frame is the one place where
   following the system appearance would work against the picture. */
#video-viewport {
  flex: 1; min-height: 0; position: relative; overflow: hidden;
  border-radius: var(--radius); background: #000;
  box-shadow: var(--shadow);
  display: flex; align-items: center; justify-content: center;
}
/* transform-origin 0 0 is what makes zoom-at-cursor solvable in closed form:
   the offset that keeps a point under the pointer is then a plain subtraction
   in the element's own coordinates. Same reason chin_tuck_4.0's stage uses it.
   A transform does not affect layout, so the centring above still holds and
   the overlays — which are siblings pinned to the viewport, not the video —
   stay put while the picture moves under them. */
#video-player {
  display: block; max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  transform-origin: 0 0;
}
/* Click = play/pause, at rest zoom only — scoped so it doesn't outrank the
   grab/grabbing cursors #video-viewport.zoomed/.panning already set. An
   unscoped rule directly on #video-player would win over those by
   specificity even though they're declared for the click-to-pan state. */
#video-viewport:not(.zoomed) #video-player { cursor: pointer; }
/* Past this magnification the browser's smoothing is inventing pixels between
   the real ones and a glove edge becomes a smear. Pixelated shows the actual
   grid the frame is made of — no more information, but the edge is where the
   pixels say it is. */
#video-player.sharp { image-rendering: pixelated; }

#video-viewport.zoomed  { cursor: grab; }
#video-viewport.panning { cursor: grabbing; }

/* Sits opposite #video-overlay's tags (top-left) so the two never collide. */
#video-zoom-badge {
  position: absolute; top: 12px; right: 12px; z-index: 11;
  padding: 5px 10px; border-radius: 8px;
  background: rgba(0,0,0,.55); color: #fff;
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  font: 590 12px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
}
#video-zoom-badge[hidden] { display: none; }
#video-zoom-badge:hover { background: rgba(0,0,0,.78); }

/* Sized and positioned in JS (skeleton.js's positionSkeletonCanvas()) to
   match #video-player's own rendered box exactly, transform included —
   inset:0 would only be right when the video happens to fill the viewport
   edge to edge, which most footage here doesn't (non-16:9 source). */
#skeleton-canvas {
  position: absolute; left: 0; top: 0; z-index: 8;
  pointer-events: none;
}

#video-dim-overlay {
  position: absolute; inset: 0; pointer-events: none; z-index: 9;
  transition: opacity .3s; display: none;
}
#video-dim-overlay.active {
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.5);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
#video-dim-overlay .dim-label {
  font-size: 15px; font-weight: 590; letter-spacing: .02em;
  color: #fff; background: rgba(0,0,0,.55);
  padding: 8px 16px; border-radius: 999px;
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
}

#video-overlay {
  position: absolute; top: 12px; left: 12px; z-index: 10;
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  pointer-events: none;
}
#video-overlay:empty { display: none; }
/* Over the picture, so it is its own material regardless of the page scheme —
   white text on a dark blur, the way a video HUD reads on any footage. */
.video-overlay-tag {
  padding: 6px 13px; border-radius: 9px;
  font-size: 14px; font-weight: 590; letter-spacing: -.01em; color: #fff;
  background: rgba(0,0,0,.55);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border-left: 3px solid; white-space: nowrap;
  pointer-events: auto; cursor: pointer;
  transition: background .15s;
}
.video-overlay-tag:hover { background: rgba(0,0,0,.75); }
/* The round-status tag, replacing two hardcoded hex values (#28a745, #e94560)
   left over from the page's pre-redesign palette — they read as a jarring
   mismatch against the token system everywhere else on the page. Inside a
   round tints the border only, matching the plain dark HUD every other tag
   uses; outside a round tints the FILL too, because a punch thrown out here
   is one the pipeline discards — that is worth more than a border's worth of
   colour, on the one tag whose whole job is to say "this doesn't count". */
.video-overlay-tag.round-in  { border-left-color: var(--yes-solid); }
.video-overlay-tag.round-out {
  border-left-color: var(--no);
  background: color-mix(in srgb, var(--no) 32%, rgba(0,0,0,.55));
}
.video-overlay-tag.round-out:hover { background: color-mix(in srgb, var(--no) 40%, rgba(0,0,0,.75)); }
.video-overlay-tag.unusable-in {
  border-left-color: var(--no);
  background: color-mix(in srgb, var(--no) 32%, rgba(0,0,0,.55));
}

/* ── timeline ────────────────────────────────────────────────────────────── */
#video-controls {
  flex: none; display: flex; flex-direction: column; gap: 8px;
  background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 14px 16px 12px;
}

#timeline-minimap {
  position: relative; width: 100%; height: 10px;
  background: var(--fill); border-radius: 3px; cursor: pointer;
}
#minimap-segments { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
#minimap-viewport {
  position: absolute; top: 0; height: 100%; z-index: 1; cursor: grab;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
  border-radius: 3px;
}
#minimap-viewport:active { cursor: grabbing; }
#minimap-playhead {
  position: absolute; top: 0; width: 2px; height: 100%; z-index: 2;
  background: var(--ink); border-radius: 1px; pointer-events: none;
}

#seek-bar-wrapper { position: relative; width: 100%; margin-top: 18px; }

#timeline-ticks {
  position: absolute; top: -17px; left: 0; right: 0; height: 17px;
  /* auto, not none: clicking a tick seeks to the time it names — see
     setupZoomedClickToSeek() in ui.js. The ticks/labels themselves stay
     pointer-events:none (below) since they're not the target, the ROW is;
     hit-testing a 1px tick mark or the gap between two labels would make
     most of the ruler dead space. */
  pointer-events: auto; cursor: pointer;
  overflow: hidden; z-index: 2;
}
.timeline-tick, .timeline-tick-label { pointer-events: none; }
.timeline-tick { position: absolute; bottom: 0; width: 1px; height: 4px; background: var(--track); }
.timeline-tick.major { height: 7px; background: var(--ink-dim); }
.timeline-tick-label {
  position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap;
  font: 500 9.5px/1 ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--ink-dim); font-variant-numeric: tabular-nums;
}

/* The punch strips, in two lanes of their own above the scrubber — offense on
   top, defense underneath, so a slip thrown mid-combo doesn't have to hide
   behind the jab it answers. They used to be painted onto the 8px scrub
   track, underneath the range input — which meant they were 8px tall, often
   3px wide, and sat below a control that swallows every mouse event that
   reaches it. Nothing about that could be dragged. Up here each is 14px of
   grabbable chip, the scrub thumb never rides over them, and "where the
   punches are" reads as a separate thing from "where the playhead is" —
   which is what it is. */
/* position:relative because #round-markers is now a CHILD (not a sibling
   spanning down into #scrub) — see the comment on #seg-lanes in index.html. */
#seg-lanes { position: relative; display: flex; flex-direction: column; gap: 3px; margin-bottom: 6px; }
.seg-lane {
  position: relative; height: 14px;
  border-radius: 5px; background: var(--fill); overflow: hidden;
}
/* Which lane is which — as ::before content, not a real DOM child: app.js's
   renderTimelineOverlay() does `lane.innerHTML = ''` on every repaint (every
   frame while dragging), which would wipe a real label element right along
   with the chips it's clearing. A pseudo-element isn't part of innerHTML at
   all, so it survives every one of those repaints for free, no JS change
   needed. z-index above the chips, with a halo instead of a solid backing —
   a filled pill would be one more opaque thing over a lane that is already
   only 14px tall; the halo keeps the tiny caption readable over a bright
   chip without adding a second visual layer to the lane. */
.seg-lane::before {
  position: absolute; top: 0; bottom: 0; left: 5px; z-index: 2;
  display: flex; align-items: center;
  font-size: 8px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink-dim);
  text-shadow: 0 0 3px var(--fill), 0 0 3px var(--fill), 0 0 3px var(--fill);
  pointer-events: none;
  /* attr(), not a per-id rule: the lanes are built at render time now, one
     pair per labeler, so their captions aren't known to the stylesheet. */
  content: attr(data-lane-label);
}
/* A teammate's lane: their colour on the caption and a bar down the leading
   edge, so a glance down the stack tells you whose row you're on before you
   read anything. The tint comes in as --lane-tint from buildSegLanes(). */
.seg-lane.lane-foreign {
  background: color-mix(in srgb, var(--lane-tint, var(--ink-dim)) 9%, var(--fill));
}
.seg-lane.lane-foreign::before { color: var(--lane-tint, var(--ink-dim)); }
.seg-lane.lane-foreign::after {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--lane-tint, var(--ink-dim)); z-index: 3; pointer-events: none;
}
/* Yours reads as the primary row — the one you can actually drag. */
.seg-lane.lane-own::before { color: var(--ink); }
/* Admin owns no rows (see isForeignLabel() in app.js) — this "own" lane can
   never hold a real move, unlike everyone else's. Muted rather than removed
   so the lane stack keeps the same shape video to video: a diagonal hatch
   in place of the solid fill reads as "not a place things live" at a
   glance, and low opacity keeps the caption from competing with the real
   lanes below it. */
.seg-lane.lane-admin-empty {
  opacity: .35;
  background-image: repeating-linear-gradient(-45deg, var(--ink-dim) 0, var(--ink-dim) 1px, transparent 1px, transparent 6px);
  background-color: var(--fill);
}
.seg-lane.lane-admin-empty::before { color: var(--ink-dim); }
/* Admin dragging a move: the lane it will land on if released here. */
.seg-lane.lane-drop {
  background: color-mix(in srgb, var(--accent) 16%, var(--fill));
  box-shadow: inset 0 0 0 1.5px var(--accent);
}
/* Alt+drag duplicates — the pointer says so. */
#seg-lanes.drag-copy, #seg-lanes.drag-copy .seek-segment { cursor: copy !important; }
/* Admin's paste target — the lane clicked last, once something is copied. */
.seg-lane.lane-target {
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent) 70%, transparent);
}

.seg-lane .seek-segment {
  position: absolute; top: 2px; height: 10px; border-radius: 3px;
  /* A real punch (0.3-0.9s) against a multi-minute video is under 0.3% of
     the lane's width — 3px used to be the actual floor here (the JS side's
     own 0.15% floor computes even smaller and never wins), which on an
     856px lane is a barely-there fleck. 7px is still a mark, not a bar, but
     it is a mark you can actually see and aim a cursor at without zooming
     in first. */
  min-width: 7px; opacity: .9; pointer-events: auto; cursor: grab;
  transition: opacity .12s ease, top .12s ease, height .12s ease;
}
.seg-lane .seek-segment:hover { opacity: 1; top: 0; height: 14px; }
/* Set from JS while a drag is live, and while hovering an edge — the pointer
   is the only thing that says whether this drag will move the punch or stretch
   one end of it. */
.seg-lane .seek-segment.dragging { opacity: 1; top: 0; height: 14px;
                                   cursor: grabbing; box-shadow: 0 0 0 1.5px var(--ink); }
/* The strip whose row is selected in the Labels panel. Full height and a
   dark ring rather than a colour change — the fill still has to say which
   MOVE it is, so the selection can only be carried by the outline.
   .marquee-hit gets the identical ring while a marquee is still being
   dragged over it — purely a live preview, not yet committed to
   state.multiSelected (see setupSegmentEditing()'s marquee-select block). */
.seg-lane .seek-segment.seg-selected,
.seg-lane .seek-segment.marquee-hit {
  opacity: 1; top: 0; height: 14px; z-index: 4;
  box-shadow: 0 0 0 1.5px var(--card), 0 0 0 3px var(--accent);
}
@media (prefers-reduced-motion: reduce) { .seg-lane .seek-segment { transition: none; } }

/* The click-drag selection box — see setupSegmentEditing()'s marquee-select
   block in ui.js. position:fixed and appended to <body>, not #seg-lanes: a
   lane clips its own strips (overflow:hidden), and the box has to be able to
   span vertically across several of them. */
.marquee-select {
  position: fixed; z-index: 90; pointer-events: none;
  border: 1px solid var(--accent-solid);
  background: var(--accent-wash);
}

/* The roll detector's p(roll), under the lanes — prob-curve.js. One lane tall
   when folded (a sparkline), user-sized when open. `color` is the curve's
   colour: the canvas reads it back, since it cannot resolve var() itself. */
#prob-lane {
  position: relative; flex: none; margin-top: 3px;
  border-radius: 6px; background: var(--fill); overflow: hidden;
  color: var(--accent);
}
#prob-lane > canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.prob-caption {
  position: absolute; top: 0; left: 2px; height: 14px; z-index: 2;
  display: flex; align-items: center; gap: 3px; pointer-events: none;
}
.prob-fold {
  pointer-events: auto; width: 14px; height: 14px; padding: 0; border: 0; border-radius: 4px;
  display: grid; place-items: center; background: none; color: var(--ink-dim); cursor: pointer;
}
.prob-fold:hover { color: var(--ink); background: var(--fill-2); }
.prob-fold:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.prob-fold svg { width: 9px; height: 9px; transition: transform .15s ease; }
.prob-fold[aria-expanded="false"] svg { transform: rotate(-90deg); }
.prob-title, .prob-model, .prob-readout {
  text-shadow: 0 0 3px var(--card), 0 0 3px var(--card), 0 0 3px var(--card);
  white-space: nowrap;
}
.prob-title {
  font-size: 8px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink);
}
.prob-model {
  font: 500 8.5px/1 ui-monospace, "SF Mono", Menlo, monospace; color: var(--ink-dim);
}
.prob-readout {
  position: absolute; top: 0; right: 6px; height: 14px; z-index: 2;
  display: flex; align-items: center; pointer-events: none; white-space: pre;
  font: 600 9.5px/1 ui-monospace, "SF Mono", Menlo, monospace; font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}
.prob-readout.above { color: var(--accent-on-wash); }
.prob-dot {
  position: absolute; z-index: 3; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 1.5px var(--card);
  pointer-events: none;
}
.prob-dot[hidden] { display: none; }
/* The threshold's handle, riding the dashed line at the right edge. Neutral
   while it shows the model's fold threshold; accent once you've set your own. */
.prob-thr {
  position: absolute; right: 4px; z-index: 5; transform: translateY(-50%);
  height: 13px; padding: 0 5px; border-radius: 7px;
  display: flex; align-items: center; gap: 3px;
  font: 600 9px/1 ui-monospace, "SF Mono", Menlo, monospace; font-variant-numeric: tabular-nums;
  color: var(--ink-on-fill); background: var(--card);
  box-shadow: 0 0 0 .5px var(--line), 0 1px 3px rgba(0,0,0,.12);
  cursor: ns-resize; user-select: none; touch-action: none;
}
.prob-thr::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  border: 1px dashed currentColor; opacity: .7;
}
.prob-thr[hidden] { display: none; }
.prob-thr:hover { color: var(--ink); }
.prob-thr.custom { color: #fff; background: var(--accent-solid); box-shadow: 0 1px 3px rgba(0,0,0,.18); }
.prob-thr.custom::before { border-style: solid; background: currentColor; opacity: .9; }
.prob-thr:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
#prob-lane.thr-dragging .prob-thr { box-shadow: 0 0 0 2px var(--accent-wash), 0 1px 4px rgba(0,0,0,.2); }
/* Bottom-edge grabber, the sheet-handle idiom: invisible until the row is
   hovered, so a resting timeline carries no extra chrome. */
.prob-grabber {
  position: absolute; left: 0; right: 0; bottom: 0; height: 8px; z-index: 4;
  display: grid; place-items: end center; padding-bottom: 2px; cursor: row-resize;
}
.prob-grabber span {
  width: 32px; height: 3px; border-radius: 2px; background: var(--ink-dim);
  opacity: 0; transition: opacity .15s ease;
}
#prob-lane:hover .prob-grabber span { opacity: .35; }
.prob-grabber:hover span, #prob-lane.resizing .prob-grabber span { opacity: .75; }
#prob-lane.collapsed .prob-grabber { display: none; }
@media (prefers-reduced-motion: reduce) {
  .prob-fold svg, .prob-grabber span { transition: none; }
}

/* What you are about to drag, named. Follows the pointer along the lane and
   is the only place the punch's exact times appear next to the picture. */
#seg-tip {
  position: absolute; bottom: calc(100% + 6px); z-index: 30;
  padding: 5px 9px; border-radius: 7px; white-space: nowrap; pointer-events: none;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line); box-shadow: 0 6px 24px rgba(0,0,0,.18);
  font-size: 11.5px; color: var(--ink);
}
#seg-tip[hidden] { display: none; }
#seg-tip b { font-weight: 590; }
#seg-tip span {
  color: var(--ink-dim); margin-left: 6px;
  font: 500 11px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
}

/* Where the rounds are, built by renderRoundStrip(). Still TWO layers, but
   they no longer share a rule: #round-markers lives inside #seg-lanes and
   zooms with it (positioned by timeToViewportPct), and is a labelled span
   per round; #round-markers-scrub lives inside #scrub, which never zooms
   (timeToScrubPct) — see setupScrubOverview() in ui.js for why the scrub
   track is the one part of the timeline that always shows the whole video —
   and is boundary ticks, because 8px has no room for a label.
   The rounds ribbon is a row of its own at the top of #seg-lanes, in flow
   rather than an overlay, so a round reads as the stretch of time it is
   instead of as two vertical rules through the lanes. Those rules were the
   same shape as a playhead and were being mistaken for one. */
#round-markers {
  position: relative; height: 13px; flex: none; margin-bottom: 2px;
}
.round-span {
  position: absolute; top: 0; bottom: 0; min-width: 3px;
  border-radius: 3px; cursor: pointer;
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  /* Bracketed ends — the boundary is the edge of the span, which is what
     makes it read as a period rather than a marker. */
  border-left: 2px solid var(--yes-solid);
  border-right: 2px solid var(--accent-solid);
  overflow: hidden;
}
.round-span:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
/* Grabbing an edge — see setupRoundSpanDragging() in ui.js. Same visual
   language as .seek-segment.dragging: brighter fill, boundary bars pop. */
.round-span.dragging-round {
  background: color-mix(in srgb, var(--accent) 32%, transparent);
  border-left-width: 3px; border-right-width: 3px;
}
.round-span-label {
  position: absolute; inset: 0; display: flex; align-items: center;
  padding-left: 5px;
  font-size: 8.5px; font-weight: 700; letter-spacing: .03em;
  text-transform: uppercase; color: var(--accent-on-wash);
  white-space: nowrap; pointer-events: none;
}

/* An app round's callouts (app-rounds.js's renderCalloutStrip()): a row on
   top of the ribbons, one mark per call from its moment to the next call —
   a tick where it was called, then what was called. Nothing to drag. */
#callout-markers { position: relative; height: 14px; flex: none; margin-bottom: 2px; }
#callout-markers[hidden] { display: none; }
.callout-span {
  position: absolute; top: 0; bottom: 0; min-width: 3px; padding-left: 4px;
  border-left: 2px solid var(--ink-dim); cursor: pointer;
  font-size: 9.5px; font-weight: 700; line-height: 14px; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: clip;
}
.callout-span:hover { background: var(--fill-2); }

/* Unusable spans: the same ribbon idiom one row up, in red and hatched — a
   stretch to leave out, not a period of the take. */
#unusable-markers { position: relative; height: 13px; flex: none; }
#unusable-markers[hidden] { display: none; }
.round-span.unusable-span {
  background-color: var(--no-wash);
  background-image: repeating-linear-gradient(45deg, transparent 0 3px, color-mix(in srgb, var(--no) 16%, transparent) 3px 6px);
  border-left-color: var(--no); border-right-color: var(--no);
}
.round-span.unusable-span:hover { background-color: color-mix(in srgb, var(--no) 24%, transparent); }
.round-span.unusable-span.dragging-round { background-color: color-mix(in srgb, var(--no) 32%, transparent); }
.round-span.unusable-span .round-span-label { color: var(--no-on-wash); }
/* Clicked on the ribbon — see selectMarkerSpan() in app.js. Delete removes it. */
.round-span.span-selected { box-shadow: inset 0 0 0 1.5px var(--accent-solid); background: color-mix(in srgb, var(--accent) 28%, transparent); }
.round-span.unusable-span.span-selected { box-shadow: inset 0 0 0 1.5px var(--no); background-color: color-mix(in srgb, var(--no) 28%, transparent); }
.seek-segment.unusable-scrub {
  opacity: 1; border-radius: 0;
  background: color-mix(in srgb, var(--no) 45%, transparent);
}

/* The scrub overview keeps boundary ticks — it is 8px tall, with no room
   for a labelled span. Short stubs anchored to the track, deliberately not
   full-height, so they read as edges rather than as a position. */
#round-markers-scrub {
  position: absolute; top: 0; bottom: 0; left: 0; right: 0;
  pointer-events: none; z-index: 15;
}
.round-tick {
  position: absolute; bottom: 50%; width: 2px; height: 9px; margin-left: -1px;
  transform: translateY(50%);
  border-radius: 1px; background: var(--yes-solid);
  pointer-events: auto; cursor: pointer;
}
.round-tick.rt-end { background: var(--accent-solid); }

/* Where you are. A dark core with a light edge either side, so it survives
   crossing a saturated strip — the scrub thumb alone used to disappear into
   any bright punch once zoomed in. */
#playhead {
  position: absolute; top: 0; bottom: 0; width: 1px; z-index: 20;
  background: var(--ink);
  box-shadow: 0 0 0 1px var(--card), 0 0 3px 1px rgba(0,0,0,.28);
  pointer-events: none;
}
#playhead[hidden] { display: none; }
/* A small handle at the head of the line — the part the eye lands on when
   scrubbing, and what separates it at a glance from a round bracket. */
.playhead-grip {
  position: absolute; top: -4px; left: 50%; transform: translateX(-50%);
  width: 9px; height: 9px; border-radius: 2px;
  background: var(--ink); box-shadow: 0 0 0 1.5px var(--card);
}

/* Another labeler's punch strip, shown read-only for shared context — same
   muted treatment as .rm-foreign above, minus the hover/drag affordance
   that would otherwise invite a grab isForeignLabel() refuses anyway. */
.seg-lane .seek-segment.seg-foreign {
  opacity: .4; cursor: default;
  background-image: repeating-linear-gradient(45deg, rgba(0,0,0,.25) 0 2px, transparent 2px 4px);
}
.seg-lane .seek-segment.seg-foreign:hover { opacity: .55; top: 2px; height: 10px; }

#scrub { position: relative; }
#seek-bar-overlay {
  position: absolute; top: 50%; left: 0; right: 0; height: 8px;
  transform: translateY(-50%);
  pointer-events: none; z-index: 0; overflow: hidden; border-radius: 4px;
}
.seek-segment {
  position: absolute; top: 0; height: 100%; border-radius: 2px;
  opacity: .85; pointer-events: none;
}
/* Outside a round: hatched rather than tinted, so it reads as "not part of the
   take" on a track that is already carrying twenty punch colours. */
.seek-segment.outside-round {
  opacity: 1; border-radius: 0; background: var(--fill);
  background-image: repeating-linear-gradient(45deg,
    transparent 0 3px, var(--stripe) 3px 6px);
}

#seek-bar {
  position: relative; z-index: 1; width: 100%; height: 18px;
  -webkit-appearance: none; appearance: none; background: transparent;
  cursor: pointer; margin: 0;
}
#seek-bar::-webkit-slider-runnable-track {
  background: var(--track); height: 8px; border-radius: 4px;
}
#seek-bar::-moz-range-track { background: var(--track); height: 8px; border-radius: 4px; }
#seek-bar::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; border-radius: 50%; margin-top: -4px;
  background: #fff; border: .5px solid rgba(0,0,0,.15);
  box-shadow: 0 1px 3px rgba(0,0,0,.30), 0 3px 8px rgba(0,0,0,.15);
  cursor: pointer;
}
#seek-bar::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: #fff; border: .5px solid rgba(0,0,0,.15);
  box-shadow: 0 1px 3px rgba(0,0,0,.30); cursor: pointer;
}

/* ── transport row ───────────────────────────────────────────────────────── */
/* Five clusters, hairline-separated, every control the same 32px tall and from
   the same shape family: transport | timecode | speed | overlays | zoom.
   Before this it was nine loose pills of five different widths in one
   undifferentiated row, headed by a big filled blue Play — the loudest object
   on a page whose actual primary action ("Set End Time") lives in the side
   panel. Nothing here is filled accent any more: the row is chrome, and chrome
   does not outrank the work.
   Wrapping is left-aligned (no margin-left:auto): the clusters need ~840px and
   the video column is 890 at 1512, so it is one row on a laptop and two
   left-flowing rows below that. */
#controls-row {
  display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap;
}
.tgroup { display: flex; align-items: center; gap: 6px; }

/* Icon buttons — play/pause and mute. Square, so they keep the rhythm of the
   joined stepper between them; Play is the only tinted one, which is all the
   emphasis a transport control needs. Their glyphs are OWNED BY ui.js:
   player.js writes the words "Play"/"Pause" and a speaker emoji into them, so
   ui.js repaints an SVG over the top — see paintPlay()/paintMute(). */
.tbtn {
  display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: 8px;
  border: 1px solid var(--line); color: var(--ink);
}
.tbtn:hover { border-color: var(--ink-dim); }
.tbtn svg { width: 15px; height: 15px; display: block; }
#btn-play {
  background: var(--accent-wash); border-color: transparent;
  color: var(--accent-on-wash);
}
#btn-play:hover { border-color: transparent;
                  background: color-mix(in srgb, var(--accent) 22%, transparent); }
#btn-play svg { width: 16px; height: 16px; }

/* Joined group for the frame steppers — the chevrons ARE the labels, and four
   separate bordered buttons read as four unrelated actions instead of one
   nudge control with two magnitudes each way. Double chevron = two frames. */
.seg-static { display: flex; }
.seg-static button {
  display: grid; place-items: center;
  height: 32px; min-width: 32px; padding: 0 8px;
  border: 1px solid var(--line); background: transparent;
  font-size: 13px; line-height: 1; color: var(--ink);
}
.seg-static button:first-child { border-radius: 8px 0 0 8px; }
.seg-static button:last-child  { border-radius: 0 8px 8px 0; }
.seg-static button + button { margin-left: -1px; }
.seg-static button:hover { border-color: var(--ink-dim); z-index: 1; }
.seg-static button:active { background: var(--fill); }
.seg-static svg { width: 13px; height: 13px; display: block; }

/* Volume, 0–100. The slider does not live in the row — that row is 15px from
   wrapping — it floats above the mute button on hover or keyboard focus, the
   way a player's volume control has worked since forever. Absolutely
   positioned, so revealing it costs the layout nothing and pushes nothing
   sideways. The button underneath still mutes on a single click. */
#volume { position: relative; display: flex; }
#vol-pop {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  z-index: 40; display: flex; align-items: center; gap: 9px;
  padding: 9px 12px; border-radius: 11px;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line);
  box-shadow: 0 8px 32px rgba(0,0,0,.18);
  opacity: 0; visibility: hidden;
  transition: opacity .14s ease, visibility .14s;
}
/* :focus-within is what makes it reachable without a mouse — tabbing to the
   slider reveals the thing being adjusted. */
#volume:hover #vol-pop, #volume:focus-within #vol-pop { opacity: 1; visibility: visible; }
@media (prefers-reduced-motion: reduce) { #vol-pop { transition: none; } }

/* Filled up to the thumb, empty past it — the fill IS the reading, which is
   why the number beside it can stay this quiet. */
#vol-range {
  -webkit-appearance: none; appearance: none;
  width: 92px; height: 14px; background: transparent; cursor: pointer; margin: 0;
}
#vol-range::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px;
  background: linear-gradient(to right,
    var(--accent) var(--pct, 100%), var(--track) var(--pct, 100%));
}
#vol-range::-moz-range-track {
  height: 4px; border-radius: 2px;
  background: linear-gradient(to right,
    var(--accent) var(--pct, 100%), var(--track) var(--pct, 100%));
}
#vol-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; border-radius: 50%; margin-top: -4px;
  background: #fff; border: .5px solid rgba(0,0,0,.15);
  box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
#vol-range::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%; border: .5px solid rgba(0,0,0,.15);
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
#vol-val {
  min-width: 24px; text-align: right;
  font: 500 11.5px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums; color: var(--ink);
}

/* A READOUT that is ALSO a control — bare text next to eight buttons said
   neither. Recessed into the same fill the segmented tracks use, so the row
   reads "controls, then a display, then controls". Fixed width because the
   string is monospaced and must not twitch the row every frame — held on the
   WRAPPER now, so swapping the span for the input on click keeps the same
   footprint instead of the pill shrinking to fit "0:08.250" alone. */
#time-wrap {
  position: relative; flex: none; cursor: pointer;
  display: grid; place-items: center;
  height: 32px; min-width: 156px; padding: 0 12px; border-radius: 8px;
  background: var(--fill);
  transition: background .12s ease;
}
#time-wrap:hover { background: var(--fill-2); }
/* The ring belongs to the pill, not the bare input sitting inside it — same
   convention as .src-field / #name-field. */
#time-wrap:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { #time-wrap { transition: none; } }

#time-display, #time-edit {
  font: 500 12.5px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* `hidden` toggles which of the two is present. Restated with !important:
   an ID selector elsewhere setting `display` would otherwise outrank the
   UA stylesheet's `[hidden]{display:none}` on specificity alone. */
#time-display[hidden], #time-edit[hidden] { display: none !important; }

/* Same pill language as #time-wrap, dimmer text — a secondary reading of
   the same position, not a second thing to focus on. Read-only (no click-
   to-edit like the timecode), so no cursor/hover/focus treatment. */
#frame-display {
  flex: none; display: grid; place-items: center;
  height: 32px; min-width: 108px; padding: 0 10px; border-radius: 8px;
  background: var(--fill); color: var(--ink-dim);
  font: 500 11.5px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
}
#frame-display:empty { display: none; }

#time-edit {
  width: 100%; border: 0; background: none; text-align: center; padding: 0;
}
#time-edit:focus { outline: none; }

/* Speed: a pop-up menu, not four permanent buttons.
   0.25/0.5/1/2 as an always-visible segmented control spent ~200px of the row
   on a setting touched a few times an hour, and pinned the choice to whatever
   four values happened to fit. A pull-down showing the CURRENT rate costs
   62px, offers six, and is how the platform presents a value with a small
   closed set of options. Opens UPWARD — this row sits at the window's foot. */
#speed-controls { position: relative; display: flex; align-items: center; }
#speed-btn {
  display: flex; align-items: center; gap: 5px;
  height: 32px; padding: 0 9px 0 11px; border-radius: 8px;
  border: 1px solid var(--line); color: var(--ink);
  font: 500 12.5px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
}
#speed-btn:hover { border-color: var(--ink-dim); }
/* Anything but 1× is a state worth seeing without reading the number: a 2×
   pass left running is a real way to mislabel a start frame. */
#speed-btn[aria-expanded="true"], #speed-btn.off-normal {
  background: var(--accent-wash); border-color: transparent; color: var(--accent-on-wash);
}
#speed-val { min-width: 38px; text-align: right; }
#speed-btn svg { width: 9px; height: 9px; flex: none; opacity: .6;
                 transition: transform .2s ease; }
#speed-btn[aria-expanded="true"] svg { transform: rotate(180deg); }

/* Nine rates open upward from a row at the foot of the window: 253px normally,
   410px once coarse-pointer sizing kicks in, which on an 812px-tall phone
   leaves 22px of headroom and on anything shorter runs off the top.
   A pop-up that scrolls when it cannot fit is what the platform does — but the
   ceiling is the space ABOVE THE BUTTON, not the window height, so ui.js sets
   max-height when it opens the menu (a `calc(100vh - …)` here is wrong by
   exactly however far up the page the transport row happens to sit). */
#speed-menu {
  position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  overflow-y: auto; overscroll-behavior: contain;
  z-index: 40; min-width: 104px; padding: 6px; border-radius: 12px;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line);
  box-shadow: 0 8px 32px rgba(0,0,0,.18);
}
#speed-menu[hidden] { display: none; }
/* The tick sits in a fixed gutter so the rates stay in one column whether or
   not they are the current one — the platform's own pop-up menu behaviour. */
#speed-menu button {
  display: flex; align-items: center; gap: 7px; width: 100%;
  padding: 7px 10px; border-radius: 7px; border: 0;
  font: 500 12.5px/1 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
#speed-menu button:hover { background: var(--fill); }
#speed-menu button::before {
  content: '\2713'; width: 11px; flex: none; font-size: 11px;
  color: var(--accent); opacity: 0;
}
#speed-menu button[aria-checked="true"]::before { opacity: 1; }
#speed-menu button[aria-checked="true"] { font-weight: 700; }

/* ── Export clip (admin, export-clip.js) — its pop-up sits above the button,
   right-aligned so it stays on screen at the toolbar's far end. Same chrome
   as #speed-menu. ──────────────────────────────────────────────────────── */
#export-clip-seg { position: relative; }
#export-clip-pop {
  position: absolute; bottom: calc(100% + 6px); right: 0; z-index: 40;
  width: 316px; padding: 10px; border-radius: 12px; text-align: left;
  display: flex; flex-direction: column; gap: 6px;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line);
  box-shadow: 0 8px 32px rgba(0,0,0,.18);
}
#export-clip-pop[hidden] { display: none; }
.ec-from { font-size: 11.5px; color: var(--ink-dim); min-height: 14px; }
.ec-row { display: flex; align-items: center; gap: 6px; }
.ec-row label { flex: none; width: 40px; font-size: 12px; color: var(--ink-dim); }
.ec-row input {
  flex: 1; min-width: 0; padding: 5px 8px; border-radius: 7px;
  border: 1px solid var(--line); background: var(--card); color: var(--ink);
  font: 500 12.5px/1.2 ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
}
.ec-row input:focus { outline: none; border-color: var(--accent); }
.ec-row small { font-size: 11px; color: var(--ink-dim); white-space: nowrap; }
.ec-now {
  flex: none; padding: 5px 8px; border-radius: 7px; border: 0;
  background: var(--fill); color: var(--ink); font-size: 11.5px; white-space: nowrap;
}
.ec-now:hover { background: var(--fill-2); }
.ec-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 2px; }
.ec-actions button {
  padding: 6px 10px; border-radius: 7px; border: 0;
  background: var(--fill); color: var(--ink); font-size: 12.5px; font-weight: 500;
}
.ec-actions button:hover { background: var(--fill-2); }
.ec-actions .ec-primary { background: var(--accent-solid); color: #fff; font-weight: 590; }
.ec-actions button:disabled { opacity: .45; }
body.exporting-clip #btn-export-clip { opacity: .6; }


/* ── right sidebar: the labeling workflow ────────────────────────────────── */
#label-panel {
  width: 304px; flex: none;
  display: flex; flex-direction: column; gap: 12px;
  min-height: 0; overflow: hidden;
}

/* Analyst mode (see the DOMContentLoaded block in app.js that sets
   isAnalyst): every surface that exists to START or CHANGE a label is
   visually disabled, not hidden — the controls stay in place so the layout
   doesn't jump, they just read as inert. The actual refusal happens in
   app.js (isForeignLabel()/isAnalyst checks); this is purely so a button
   that can never do anything doesn't sit there looking clickable. Viewing
   surfaces (timelines, the Labels list, filters) are untouched. */
body.analyst-mode #punch-panel,
body.analyst-mode #cap,
body.analyst-mode #round-indicator,
body.analyst-mode #unusable-indicator {
  opacity: .45; filter: grayscale(.4); pointer-events: none;
}

/* Round state. Not decoration — every punch outside a round is a punch the
   pipeline will ignore, so this is the one status on the page that has to be
   readable without being looked for. It is also the button that changes it. */
#round-indicator {
  flex: none;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 44px; padding: 10px 14px; border-radius: var(--radius);
  font-size: 13px; font-weight: 590; text-align: center;
  transition: background .15s ease, color .15s ease, transform .1s ease;
  box-shadow: var(--shadow);
}
#round-indicator.round-idle { background: var(--card); color: var(--ink-dim); }
#round-indicator.round-idle:hover { color: var(--ink); }
#round-indicator.round-active {
  background: var(--yes-solid); color: #fff;
  box-shadow: var(--shadow), 0 0 0 3px color-mix(in srgb, var(--yes) 22%, transparent);
  animation: pulse 2.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  #round-indicator.round-active { animation: none; }
  #round-indicator { transition: none; }
}
/* The quieter sibling: most of a session has no unusable stretch open, so it
   rests as a slim secondary control and only turns red while one is. */
#unusable-indicator {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  min-height: 32px; padding: 6px 12px; border-radius: var(--radius);
  font-size: 12px; font-weight: 590; text-align: center; cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
#unusable-indicator.unusable-idle { background: var(--fill); color: var(--ink-on-fill); }
#unusable-indicator.unusable-idle:hover { color: var(--ink); background: var(--fill-2); }
#unusable-indicator.unusable-active {
  background: color-mix(in srgb, var(--no) 78%, black); color: #fff;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--no) 22%, transparent);
}
@media (prefers-reduced-motion: reduce) { #unusable-indicator { transition: none; } }

/* The three-step capture flow, drawn from the ONE piece of state the page
   already publishes: the class on #btn-timestamp. app.js sets `.ready` in
   start mode, `.end-mode` once a type is picked, and neither while it is
   waiting for that pick — so :has() can read the mode off the button and light
   the matching step without a line of new JavaScript.
   A connected tracker, not three separate status pills: a line runs through
   the row of dots and fills in green behind you as each step clears, the
   current dot is filled and ringed, a finished one swaps its number for a
   check — the standard shape of a multi-step flow (Setup Assistant, Apple ID
   two-factor, any "step 2 of 3" screen), rather than three buttons that
   happened to sit next to each other. */
.steps {
  display: flex; margin: 0 0 14px; padding: 0 2px; list-style: none;
}
.step {
  flex: 1; position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  font-size: 10.5px; font-weight: 590; color: var(--ink-dim);
  transition: color .18s ease;
}
/* The connector: drawn on every step EXCEPT the first, running from THIS
   step's own dot-centre back to the PREVIOUS one's. Three equal-width flex
   items with no gap between them makes that span exactly right:50%/width:100%
   of the item's own box — the previous dot's centre is, by construction, one
   full item-width to the left of this one's. Sits behind the dot (z-index 0
   vs. the dot's 1) so the dot visually breaks the line at each stop, the way
   a stop breaks a train line on a transit map. */
.step:not(:first-child)::before {
  content: ''; position: absolute; top: 9px; right: 50%; width: 100%; height: 2px;
  background: var(--track); z-index: 0;
}
.step-dot {
  position: relative; z-index: 1;
  display: grid; place-items: center;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--card); border: 1.5px solid var(--track);
  transition: background .18s ease, border-color .18s ease, box-shadow .18s ease;
}
/* A step you have not reached is a bare numeral in an outlined, unfilled
   dot — reaching it fills the dot in; clearing it swaps the numeral for a
   check (below). Nothing is painted behind an unreached numeral until there
   is a filled state to paint, so it inherits the dot's own contrast instead
   of risking white-on-light-grey. */
.step-dot b {
  font-size: 10px; font-weight: 700; color: var(--ink-dim);
  transition: opacity .12s ease;
}
/* The check that replaces it once a step is done — see the .step-dot b
   { opacity: 0 } pairing below each "done" rule. One glyph, not an SVG: at
   10px a checkmark character is as crisp as a path would be. */
.step-dot::after {
  content: '\2713'; position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 10px; font-weight: 700; color: #fff; opacity: 0;
  transition: opacity .12s ease;
}
@media (prefers-reduced-motion: reduce) {
  .step, .step-dot, .step-dot b, .step-dot::after { transition: none; }
}
/* No .on / .done classes: nothing on the page adds them. Every state below is
   read straight off #btn-timestamp by :has(), which is the whole point — the
   tracker lights up without app.js knowing it exists. */

/* ── current step ─────────────────────────────────────────────────────────
   Filled dot, white numeral, a soft ring — the one visual language for
   "you are here" already used on this page (#round-indicator's active glow
   is the same box-shadow trick). */
#cap:has(#btn-timestamp.ready) .step[data-s="1"],
#cap:not(:has(#btn-timestamp.ready)):not(:has(#btn-timestamp.end-mode)) .step[data-s="2"],
#cap:has(#btn-timestamp.end-mode) .step[data-s="3"] { color: var(--ink); }

#cap:has(#btn-timestamp.ready) .step[data-s="1"] .step-dot,
#cap:not(:has(#btn-timestamp.ready)):not(:has(#btn-timestamp.end-mode)) .step[data-s="2"] .step-dot,
#cap:has(#btn-timestamp.end-mode) .step[data-s="3"] .step-dot {
  background: var(--accent-solid); border-color: transparent;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
#cap:has(#btn-timestamp.ready) .step[data-s="1"] .step-dot b,
#cap:not(:has(#btn-timestamp.ready)):not(:has(#btn-timestamp.end-mode)) .step[data-s="2"] .step-dot b,
#cap:has(#btn-timestamp.end-mode) .step[data-s="3"] .step-dot b { color: #fff; }

/* ── done steps ──────────────────────────────────────────────────────────
   Green fill, numeral swapped for a check, and the connector immediately
   BEFORE this step turns the same green — the line itself is the record of
   what has already happened, not just the dots. */
#cap:not(:has(#btn-timestamp.ready)) .step[data-s="1"],
#cap:has(#btn-timestamp.end-mode) .step[data-s="2"] { color: var(--yes); }

#cap:not(:has(#btn-timestamp.ready)) .step[data-s="1"] .step-dot,
#cap:has(#btn-timestamp.end-mode) .step[data-s="2"] .step-dot {
  background: var(--yes-solid); border-color: transparent; box-shadow: none;
}
#cap:not(:has(#btn-timestamp.ready)) .step[data-s="1"] .step-dot b,
#cap:has(#btn-timestamp.end-mode) .step[data-s="2"] .step-dot b { opacity: 0; }
#cap:not(:has(#btn-timestamp.ready)) .step[data-s="1"] .step-dot::after,
#cap:has(#btn-timestamp.end-mode) .step[data-s="2"] .step-dot::after { opacity: 1; }

#cap:not(:has(#btn-timestamp.ready)) .step[data-s="2"]::before,
#cap:has(#btn-timestamp.end-mode) .step[data-s="3"]::before { background: var(--yes-solid); }

/* The primary action, and the only full-width filled control in the panel.
   Blue to open a segment, green to close one: green is the platform's
   "this completes the thing", and the old red read as a warning on the very
   keystroke that saves the label. */
#btn-timestamp {
  width: 100%; min-height: 44px; margin-top: 10px; padding: 12px;
  border-radius: 10px; border: none;
  background: var(--fill); color: var(--ink-on-fill);
  font-size: 14px; font-weight: 590;
  transition: background .15s ease, color .15s ease, transform .1s ease;
}
#btn-timestamp:disabled { cursor: default; }
#btn-timestamp.ready { background: var(--accent-solid); color: #fff; }
#btn-timestamp.end-mode { background: var(--yes-solid); color: #fff; }
@media (prefers-reduced-motion: reduce) { #btn-timestamp { transition: none; } }

#pending-label {
  margin-top: 8px; font-size: 11.5px; line-height: 1.4; color: var(--ink-dim);
  text-align: center;
}
#pending-label:empty { display: none; }

/* Escape already cancelled a half-built label; nothing on screen said so.
   Shown only in the two states where there is something to cancel — same
   :has() read of #btn-timestamp's class as the step pips above. */
#btn-cancel {
  display: none;
  width: 100%; margin-top: 8px; padding: 7px; border-radius: 8px;
  font-size: 12px; font-weight: 510; color: var(--ink-dim);
}
#btn-cancel:hover { background: var(--fill); color: var(--ink); }
#cap:not(:has(#btn-timestamp.ready)) #btn-cancel { display: block; }

/* The list. It is the longest-lived thing in the panel, so it gets the
   remaining height and scrolls inside its own card. */
.labels-card {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  padding-bottom: 8px;
}

#label-count { color: var(--ink); font-variant-numeric: tabular-nums; }
#foreign-filter { position: relative; margin-left: auto; }
#type-filter, #agr-type-filter, .agr-version-filter { position: relative; }
#btn-foreign-filter, #btn-type-filter, #btn-unsure-filter, #btn-agr-type-filter,
#btn-agr-version-john, #btn-agr-version-arianne {
  display: flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: 6px; border: none;
  background: var(--fill); color: var(--ink-on-fill);
  font-size: 10.5px; font-weight: 590; letter-spacing: 0; text-transform: none;
}
#btn-foreign-filter.on, #btn-type-filter.on, #btn-unsure-filter.on, #btn-agr-type-filter.on,
#btn-agr-version-john.on, #btn-agr-version-arianne.on {
  background: var(--accent-wash); color: var(--accent-on-wash);
}
#btn-foreign-filter svg, #btn-type-filter svg, #btn-agr-type-filter svg,
#btn-agr-version-john svg, #btn-agr-version-arianne svg {
  width: 8px; height: 8px; opacity: .7; transition: transform .15s ease;
}
#btn-foreign-filter[aria-expanded="true"] svg,
#btn-type-filter[aria-expanded="true"] svg,
#btn-agr-type-filter[aria-expanded="true"] svg,
#btn-agr-version-john[aria-expanded="true"] svg,
#btn-agr-version-arianne[aria-expanded="true"] svg { transform: rotate(180deg); }

/* Same pop-up chrome and checkmark-row idiom as #speed-menu — a master
   "Show others" row, then one row per foreign owner currently on this
   video, each independently toggleable (see isLabelerHidden() in app.js).
   Rebuilt on open from whoever's actually present, so it never goes stale
   the way a fixed list would as labelers come and go video to video. */
#foreign-filter-menu, #type-filter-menu, #agr-type-filter-menu,
#agr-version-menu-john, #agr-version-menu-arianne {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
  min-width: 190px; max-width: 240px; padding: 6px; border-radius: 12px;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line);
  box-shadow: 0 8px 32px rgba(0,0,0,.18);
  text-transform: none; letter-spacing: 0;
}
#foreign-filter-menu[hidden], #type-filter-menu[hidden], #agr-type-filter-menu[hidden],
#agr-version-menu-john[hidden], #agr-version-menu-arianne[hidden] { display: none; }
/* Admin's Types menu (and the Agreement dialog's own copy of it) — the same
   pop-up, with one checkmark row per catalogue entry laid out two across
   (lead beside rear, head row above body row, as the catalogue does):
   nineteen rows in one column ran off the bottom of a laptop screen. Wider
   than the Others menu to fit the pair. */
#type-filter-menu, #agr-type-filter-menu {
  position: fixed; top: auto; right: auto;   /* placed by setupTypeFilterMenu()/setupAgreementTypeFilterMenu() */
  width: 360px; max-width: none; overflow-y: auto;
}
/* The two version filters have only a handful of rows (v0, v1, none, ...) —
   single column, so they stay the same fixed-position/placed-by-JS pop-up
   as the others but don't need the wide two-across grid. */
#agr-version-menu-john, #agr-version-menu-arianne {
  position: fixed; top: auto; right: auto; left: auto;   /* placed by setupAgreementVersionFilterMenu() */
  width: 160px; max-width: none; overflow-y: auto;
}
.tfm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 4px; }
.tfm-grid .ffm-row { gap: 6px; font-size: 12px; }
/* A teammate row: the visibility toggle takes the width, the reorder arrows
   sit at the end. The master "Show others" row is a bare .ffm-row button. */
.ffm-row {
  display: flex; align-items: center; gap: 2px; width: 100%;
  border-radius: 7px; border: 0; background: none;
  font-size: 12.5px; font-weight: 500; color: var(--ink); text-align: left;
}
button.ffm-row { padding: 7px 8px; gap: 8px; }
.ffm-toggle {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px; border-radius: 7px; border: 0; background: none;
  font: inherit; color: inherit; text-align: left;
}
button.ffm-row:hover, .ffm-toggle:hover { background: var(--fill); }
/* Greyed out while the master "Show others" switch is off — see
   renderForeignFilterMenu() in app.js. */
button.ffm-row:disabled, .ffm-toggle:disabled { opacity: .4; }
button.ffm-row:disabled:hover, .ffm-toggle:disabled:hover { background: none; }
button.ffm-row::before, .ffm-toggle::before {
  content: '\2713'; width: 12px; flex: none; font-size: 11px;
  color: var(--accent); opacity: 0;
}
button.ffm-row[aria-checked="true"]::before,
.ffm-toggle[aria-checked="true"]::before { opacity: 1; }
/* This labeler's timeline-lane colour, so the menu is where you learn which
   colour belongs to whom. */
.ffm-dot {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--who, var(--ink-dim));
}
.ffm-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ffm-count { flex: none; color: var(--ink-dim); font-variant-numeric: tabular-nums; font-size: 11px; }
.ffm-arrows { flex: none; display: flex; }
.ffm-arrow {
  width: 20px; height: 24px; border: 0; border-radius: 5px;
  background: none; color: var(--ink-dim); font-size: 8px; line-height: 1;
}
.ffm-arrow:hover:not(:disabled) { background: var(--fill-2); color: var(--ink); }
.ffm-arrow:disabled { opacity: .25; }
.ffm-sep { height: 1px; margin: 5px 4px; background: var(--line); }
.ffm-empty { padding: 10px 8px; font-size: 12px; color: var(--ink-dim); }

#label-log {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 5px;
  margin-right: -6px; padding-right: 6px;
  scrollbar-width: thin; scrollbar-color: var(--track) transparent;
}
#label-log::-webkit-scrollbar { width: 8px; }
#label-log::-webkit-scrollbar-thumb { background: var(--track); border-radius: 4px; }
#label-log:empty::after {
  content: 'No labels yet — set a start time to begin.';
  display: block; padding: 24px 8px; text-align: center;
  font-size: 12px; color: var(--ink-dim);
}

.label-entry {
  display: flex; align-items: center; gap: 6px;
  padding: 7px 9px; border-radius: 9px; background: var(--fill);
  border-left: 3px solid var(--line);
  font-size: 12px;
}
.label-entry .label-text { flex: 1; min-width: 0; color: var(--ink-on-fill);
                           font-variant-numeric: tabular-nums; }
.label-entry strong { color: var(--ink); font-size: 13px; font-weight: 590;
                      letter-spacing: -.01em; }
/* (A rule used to live here re-pointing hard-coded #555/#666 inline colours
   in renderLabels() at the semantic dim ink. Those strings are gone — the
   row markup no longer sets colour inline — and the rule had outlived them
   in the worst way: it matched ANY span carrying a style attribute, with
   !important, so the .who-badge (which carries an inline --who custom
   property) had its white text forced to grey. Removed with its cause.) */
.label-entry .label-edit,
.label-entry .label-delete {
  flex: none; display: grid; place-items: center;
  width: 22px; height: 22px; border-radius: 6px; border: none;
  color: var(--ink-dim); font-size: 15px; line-height: 1; opacity: .5;
  transition: opacity .12s ease, color .12s ease, background .12s ease;
}
.label-entry .label-edit svg { width: 12px; height: 12px; }
/* Faint until the row is under the pointer — twenty rows each with two lit
   glyphs would be a wall of chrome — but never invisible, because the pencil
   is the only sign these rows can be edited at all. */
.label-entry:hover .label-edit,
.label-entry:hover .label-delete { opacity: .8; }
.label-entry .label-edit:hover { opacity: 1; color: var(--accent); background: var(--fill-2); }
.label-entry .label-delete:hover { opacity: 1; color: var(--no); background: var(--fill-2); }
@media (prefers-reduced-motion: reduce) {
  .label-entry .label-edit, .label-entry .label-delete { transition: none; }
}
/* Round markers used to be the least visible thing on the page — transparent
   background, no border, small grey text that blended into the card behind
   it. They mark the boundary every punch gets judged against (one outside a
   round is one the pipeline discards), so they now get the same weight a
   punch row gets: a tinted background and a coloured left border, colour-
   coded start vs end so the two read apart at a glance. Same green/blue pair
   the timeline's rounds ribbon brackets each span with (.round-span above,
   and .round-tick on the scrub track) — same story, two places. */
.label-entry.round-marker {
  padding: 7px 9px; font-size: 12.5px; border-left-width: 3px;
}
.label-entry.round-marker.rm-start { background: var(--yes-wash); border-left-color: var(--yes-solid); }
.label-entry.round-marker.rm-end   { background: var(--accent-wash); border-left-color: var(--accent-solid); }
.label-entry.round-marker .label-text strong { font-weight: 590; }
.label-entry.round-marker.rm-start .label-text strong { color: var(--yes-on-wash); }
.label-entry.round-marker.rm-end   .label-text strong { color: var(--accent-on-wash); }
.label-entry.round-marker.rm-unusable { background: var(--no-wash); border-left-color: var(--no); }
.label-entry.round-marker.rm-unusable .label-text strong { color: var(--no-on-wash); }
/* --ink-on-fill, not plain --ink-dim: the wash backgrounds above pull it to
   4.1–4.2:1, just under the bar — same trap --ink-on-fill already exists to
   fix everywhere else a dim label sits on a tint instead of bare --card. */
.label-entry.round-marker .label-text small { color: var(--ink-on-fill); font-weight: 500; }
/* Someone else's round marker: read-only context, so it keeps the muted,
   colourless look every round marker used to have — the tint above is
   reserved for a boundary this labeler can actually act on. */
.label-entry.round-marker.rm-foreign {
  background: transparent; border-left-color: transparent; opacity: .6;
}
.label-entry.round-marker.rm-foreign .label-text strong { color: var(--ink-dim); font-weight: 500; }

/* Another labeler's punch row, shown read-only for shared context.
   NO opacity on the row: it used to carry .82, which multiplied through to
   the badge and turned its white name a dull grey — the whole point of the
   badge is that the name reads cleanly. Held back by the muted TIMES
   instead, which is the part that doesn't need to compete. */
.label-entry.label-foreign { background: transparent; }
.label-entry.label-foreign .label-text small { color: var(--ink-dim); }

/* Move name and author on one line, times underneath. */
.label-head { display: flex; align-items: center; gap: 6px; min-width: 0; }
.label-head strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Who logged it. A solid chip in that labeler's colour (--who, set inline
   from labelerColor()) — the SAME colour as their timeline lane, so the two
   views agree at a glance. Text is pure white, and every tint in
   LABELER_TINTS is dark enough to clear 4.5:1 behind it. */
.who-badge {
  flex: none; padding: 1.5px 7px; border-radius: 999px;
  background: var(--who, var(--ink-dim)); color: #fff;
  font-size: 10.5px; font-weight: 680; letter-spacing: .01em;
  max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The row you last clicked. Paired with .seek-segment.seg-selected on the
   timeline — see highlightLabel() — so picking a row in the list points at
   the same punch in both places. */
.label-entry.label-selected {
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}

.label-entry.editing { background: var(--card); border-left-color: var(--accent);
                       box-shadow: 0 0 0 1px var(--accent); padding: 10px; }

/* Right-click a chip → Highlight scrolls the row into view — a one-shot
   pulse says "this one", the way a jump-to-match briefly bolds its target,
   rather than leaving the scroll itself to do all the explaining. */
@keyframes label-flash {
  0% { box-shadow: 0 0 0 2px var(--accent); background: var(--accent-wash); }
  100% { box-shadow: 0 0 0 2px transparent; background: var(--fill); }
}
.label-entry.label-flash { animation: label-flash 1.1s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .label-entry.label-flash { animation: none; box-shadow: 0 0 0 2px var(--accent); }
}

.edit-form { width: 100%; display: flex; flex-direction: column; gap: 4px;
             position: relative; }
.edit-form > .label-delete { position: absolute; top: -3px; right: -3px; }
.edit-row { display: flex; align-items: center; gap: 6px; }
/* A field caption, so "0:08.200 → 0:08.600" is labelled rather than guessed at.
   Same small-caps eyebrow the cards use, one size down. */
.edit-lbl {
  font-size: 10px; font-weight: 590; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-dim); margin-top: 3px;
}
.edit-arrow { color: var(--ink-dim); flex: none; }
.edit-form select, .edit-form input {
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--card); color: var(--ink);
  padding: 6px 8px; font-size: 12px; min-height: 30px;
}
.edit-form select:focus, .edit-form input:focus {
  outline: 2px solid var(--accent); outline-offset: -1px;
}
.edit-form select { flex: 1; min-width: 0; }
.edit-form input[type="text"] {
  flex: 1; min-width: 0;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-variant-numeric: tabular-nums;
}
.edit-actions { margin-top: 5px; }
.edit-row button {
  flex: 1; min-height: 30px; padding: 6px 8px; border: none; border-radius: 7px;
  font-size: 12px; font-weight: 510;
}
/* Save is the committing action and sits last, where the platform puts it. */
.edit-save   { background: var(--accent-solid); color: #fff; font-weight: 590; }
.edit-cancel { background: var(--fill); color: var(--ink); }
.edit-seek   { background: var(--fill); color: var(--ink); flex: 0 0 auto; padding: 6px 10px; }

/* ── shortcuts sheet ─────────────────────────────────────────────────────── */
/* The old page carried all nineteen bindings as a permanent strip of grey text
   across the bottom — 32px of vertical space, every session, spent on
   something read twice in a career. A sheet gives it back to the video and
   makes the reference legible when it IS wanted. */
#sc-dialog {
  width: min(560px, calc(100vw - 32px));
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
#sc-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
.sc-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px; border-bottom: 1px solid var(--line);
}
.sc-head h2 { font-size: 17px; font-weight: 640; letter-spacing: -.02em; }
/* Groups a dialog's OTHER header buttons (e.g. #agr-export-pdf) with its
   close button so .sc-head's own space-between still puts the pair as a
   whole on the right, next to the title. */
.sc-head-actions { display: flex; align-items: center; gap: 10px; flex: none; }
.sc-body { padding: 8px 20px 20px; max-height: 65vh; overflow-y: auto; }
/* A dialog whose header carries a second button beside close (so far just
   #agr-export-pdf) wraps onto two lines on a narrow screen instead of
   squeezing that button into the middle of a title that no longer fits one
   line — .sc-head-actions' own flex:none keeps it from shrinking, and the
   title wrapping under it (rather than beside it) is what a plain
   flex-wrap gives for free here. */
@media (max-width: 480px) {
  .sc-head { flex-wrap: wrap; row-gap: 8px; }
}
.sc-sec {
  font-size: 11px; font-weight: 590; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-dim); margin: 16px 0 6px;
}
.sc-row {
  display: flex; align-items: center; gap: 12px;
  padding: 7px 0; border-top: 1px solid var(--line); font-size: 13px;
}
.sc-sec + .sc-row { border-top: none; }
.sc-row span { flex: 1; }
.sc-keys { flex: none; display: flex; gap: 4px; }
kbd {
  padding: 3px 7px; border-radius: 6px;
  background: var(--fill); color: var(--ink);
  font: 500 11.5px/1.3 ui-monospace, "SF Mono", Menlo, monospace;
}

/* ── "already labeled by someone else" popup ────────────────────────────── */
/* Same sheet chrome as #sc-dialog (.sc-head/.sc-body carry over), so this
   only needs its own body content and action row. */
#fvd-dialog {
  width: min(420px, calc(100vw - 32px));
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
#fvd-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
.fvd-lede { font-size: 13px; color: var(--ink-dim); margin: 4px 0 12px; }
.fvd-rows { display: flex; flex-direction: column; gap: 4px; }
.fvd-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 10px; border-radius: 8px; background: var(--fill);
  font-size: 13px;
}
.fvd-name { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* The split, in the same two colours the timeline lanes use for the two
   buckets — dot, count, word, so it reads without a legend. */
.fvd-split { display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 11.5px; }
.fvd-split span { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-dim); }
.fvd-split span::before {
  content: ''; width: 7px; height: 7px; border-radius: 2px; flex: none;
}
.fvd-off::before { background: var(--no); }
.fvd-def::before { background: var(--accent); }
.fvd-rnd::before { background: var(--yes-solid); }
.fvd-unu::before { background: var(--no); }
.fvd-oth::before { background: var(--ink-dim); }
.fvd-total {
  flex: none; font-size: 15px; font-weight: 640; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.fvd-note {
  margin: 12px 0 0; padding: 8px 10px; border-radius: 8px;
  background: var(--accent-wash); color: var(--accent-on-wash);
  font-size: 12.5px;
}
.fvd-actions {
  display: flex; gap: 8px; padding: 12px 20px 20px;
}
.fvd-actions button {
  flex: 1; min-height: 34px; padding: 7px 10px; border: none; border-radius: 8px;
  font-size: 13px; font-weight: 510; background: var(--fill); color: var(--ink);
}
.fvd-actions .fvd-primary { background: var(--accent-solid); color: #fff; font-weight: 590; }

/* ── name a loaded predictions file ───────────────────────────────────────── */
#pred-name-dialog {
  width: min(420px, calc(100vw - 32px));
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
#pred-name-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
.pred-name-note { font-size: 13px; color: var(--ink-dim); margin: 4px 0 12px; }
.pred-name-field {
  width: 100%; height: 38px; padding: 0 12px; border-radius: 9px;
  border: 1.5px solid var(--line); background: var(--fill); color: var(--ink);
  font-size: 14px; font-weight: 510;
}
.pred-name-field:focus { outline: none; border-color: var(--accent); }

/* ── timing-problem popup + the "View all" list ──────────────────────────── */
#problem-popup-dialog {
  width: min(420px, calc(100vw - 32px));
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
#problem-popup-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
#problem-list-dialog {
  width: min(520px, calc(100vw - 32px));
  max-height: 80vh;
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
#problem-list-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
#problem-list-dialog .sc-body { overflow-y: auto; }
#problem-list-count { color: var(--ink-dim); font-variant-numeric: tabular-nums; font-weight: 500; }

/* ── "labels are loading" popup ─────────────────────────────────────────── */
/* Modal on purpose: phase one is the labeler's OWN rows, and marking a punch
   against a half-loaded own-list is how duplicates get made. Phase two only
   adds other people's read-only rows, so that stage reveals a way out. */
#ldg-dialog, #bulk-save-dialog {
  width: min(380px, calc(100vw - 32px));
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
#ldg-dialog::backdrop, #bulk-save-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
.ldg-body {
  display: flex; flex-direction: column; align-items: center;
  gap: 12px; padding: 28px 24px 24px; text-align: center;
}
.ldg-spinner {
  width: 30px; height: 30px; border-radius: 50%;
  border: 3px solid var(--fill); border-top-color: var(--accent-solid);
  animation: spin .8s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .ldg-spinner { animation: none; } }
#ldg-title { font-size: 16px; font-weight: 640; letter-spacing: -.02em; }
/* Reserved height: the note runs one line in the first stage and three in
   the second, and the sheet should not jump when it swaps. */
.ldg-note {
  font-size: 12.5px; line-height: 1.5; color: var(--ink-dim);
  min-height: 4.5em;
}
#ldg-skip {
  height: 32px; padding: 0 14px; border: 0; border-radius: 8px;
  background: var(--accent-solid); color: #fff;
  font-size: 13px; font-weight: 590; cursor: pointer;
}
#ldg-skip:hover { filter: brightness(1.06); }
#ldg-skip[hidden] { display: none; }

/* ── agreement sheet (admin) ─────────────────────────────────────────────── */
/* Wide and tall: this is a review instrument, not a notification. Flex
   column with a bounded height so the HEAD stays put and only the body
   scrolls — a table this long is unusable if its own headings scroll away
   with it. */
#agr-dialog {
  width: min(1020px, calc(100vw - 40px));
  max-height: 88vh;
  padding: 0; border: none; border-radius: 16px;
  background: var(--card); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
/* Scoped to [open] on purpose — every OTHER dialog on this page leaves
   `display` alone entirely and lets the browser's own dialog[open]/:modal
   UA rules toggle block/none, but this one needs flex-column layout (head
   fixed, body scrolling — see below) while open. An unscoped `display: flex`
   here would beat the UA's `dialog:not([open]) { display: none }` by
   specificity, so a CLOSED dialog stayed rendered — sitting in normal
   document flow at wherever it happens to live in the DOM (near the end of
   <body>), which is exactly the "closes but folds into the bottom of the
   page" bug this fixes. */
#agr-dialog[open] { display: flex; flex-direction: column; }
#agr-dialog .sc-head { flex: none; }
/* Overrides .sc-body's own 65vh cap — here the dialog's max-height is what
   bounds it, and the body takes whatever is left. */
#agr-dialog .sc-body { flex: 1; min-height: 0; max-height: none; overflow-y: auto; padding: 14px 24px 24px; }
#agr-dialog::backdrop {
  background: rgba(0,0,0,.28);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
.agr-empty { font-size: 13px; color: var(--ink-dim); padding: 12px 0; }
/* The video-name line gets the same copy affordance as every family row
   below it — see .agr-copy further down. Overrides .fvd-lede's own block
   layout with a row so the button sits at the end instead of wrapping onto
   its own line under a long name. */
.agr-video-line { display: flex; align-items: center; gap: 6px; }
/* John/Arianne progress badges next to a video's headline — reuses the
   video picker's own .vp-slot/.vp-ver/.vp-status-* markup (statusBadges()
   in app.js) so a video reads the same way here as it does in the picker.
   Its own flex row so the (fixed-width, individually-flex) .vp-slot
   children lay out side by side instead of stacking. */
.agr-video-badges { display: flex; align-items: center; gap: 4px; flex: none; }
/* All-videos view only — one of these per video with data (renderAgreement()
   in app.js), each holding its own .agr-video-line + families. A hairline
   above every one after the first marks where a new video starts, since
   there's no dialog chrome between them the way separate videos otherwise
   get. */
.agr-video-block {
  padding-top: 18px; margin-top: 18px; border-top: 1px solid var(--line);
  /* Only matters if this dialog itself gets printed (Ctrl+P), not the
     normal on-screen scroll — same fix as .video-block gets in the PDF
     export's own stylesheet (exportAgreementPdf() in app.js): without it a
     page break can separate a video's header from its rows, so a printed
     page can open mid-video with no name in sight. */
  break-inside: avoid-page; page-break-inside: avoid;
}
.agr-video-block:first-child { padding-top: 0; margin-top: 0; border-top: 0; }
.agr-video-block .agr-video-line { break-after: avoid-page; page-break-after: avoid; }
/* The trailing "Summary across N videos" block (renderAgreement()) — same
   family/type rows as every other block, just visually called out as a
   total rather than one more video: a thicker, double-style top border and
   a bolder headline instead of the plain hairline every other block gets. */
.agr-summary-block { border-top: 2px solid var(--ink-dim); padding-top: 20px; margin-top: 20px; }
.agr-summary-block .agr-video-line .agr-line { font-weight: 650; font-size: 13.5px; }
/* One family name per section (Jabs, Rolls, ...) — see
   computeAgreementPanel()/renderAgreement() in app.js. Rows underneath
   reuse .fvd-rows/.fvd-row (the "loaded from the sheet" popup's own list
   style) rather than a table — one line per labeler is all this needs. */
.agr-h {
  margin: 26px 0 10px;
  font-size: 10.5px; font-weight: 650; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-dim);
}
.agr-h:first-of-type { margin-top: 6px; }
/* Holds one .agr-type-group per exact type in the family (Lead Roll, Rear
   Roll, ...) under their shared .agr-h — the gap between groups is smaller
   than the gap above the next family's own .agr-h, so the two related
   types read as visually paired without merging their counts into one. */
.agr-family { display: flex; flex-direction: column; gap: 10px; }
/* Roomier than .fvd-row's own default — the rest of this dialog's rows now
   carry a count AND a trailing "(N matched, N unmatched)", and the tighter
   spacing that was fine for one bare line felt cramped once. Scoped to
   this dialog rather than changing .fvd-row/.fvd-rows everywhere, since the
   "loaded from the sheet" popup and the problem list reuse those same
   classes and were never the ones this was about. */
#agr-body .fvd-rows { gap: 6px; }
#agr-body .fvd-row { padding: 11px 14px; }
.agr-line { flex: 1; min-width: 0; }
/* The all-videos view's per-video headline is a <button> (agreementBlockHtml()'s
   `clickable`) so a spotted gap/disagreement goes straight to opening that
   video (agrOpenVideo() in app.js) — reset off the default button chrome,
   left-align the text same as the plain <span> version, and give it just
   enough hover/underline affordance to read as clickable without looking
   like a different kind of control than the rest of this line. */
.agr-video-link {
  border: 0; background: none; padding: 0; margin: 0; color: inherit;
  font: inherit; text-align: left; cursor: pointer;
}
.agr-video-link:hover { text-decoration: underline; color: var(--accent); }
/* Same dot .punch-btn's own carries for this move — see the comment on
   lineHtml() in renderAgreement(). Inline before the text rather than a
   flex child of its own, so it sits on the text's own line exactly the
   way .punch-btn .dot does. */
.agr-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle;
  box-shadow: 0 0 0 1px rgba(0,0,0,.12);
}
/* The "(N matched, N unmatched)" trailing a row's own count — see
   agreementMatchSuffix() in app.js. Muted and slightly smaller so the
   count itself stays the thing the eye lands on first. */
.agr-sub { color: var(--ink-dim); font-size: 12px; }
/* Same 22px icon-button shape as #btn-copy-link/#btn-copy-name elsewhere on
   this page, just its own class since this one lives inside .fvd-row
   instead of a .src-field. */
.agr-copy {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 6px; border: 0;
  background: transparent; color: inherit; opacity: .55;
}
.agr-copy:hover { opacity: 1; background: var(--fill-2); }
.agr-copy svg { width: 12px; height: 12px; }
.agr-copy.copied { opacity: 1; color: var(--yes); }

/* The "Export PDF" button beside the close × — text + icon, same quiet
   chrome-button language as #btn-agreement/#btn-problems in the nav bar
   rather than a filled pill, since this is a secondary action on a dialog
   that's already an admin-only review tool. */
#agr-export-pdf {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 10px; border-radius: 8px; border: 0;
  background: var(--fill); color: var(--ink);
  font-size: 12px; font-weight: 510; white-space: nowrap;
}
#agr-export-pdf:hover { background: var(--fill-2); }
#agr-export-pdf svg { width: 13px; height: 13px; flex: none; }

/* ── toast ───────────────────────────────────────────────────────────────── */
/* showToast() in player.js hard-codes the three classes below, so the names
   stay; only the look changes — one floating capsule of material with a
   coloured dot, rather than three differently-coloured slabs. */
.toast {
  position: fixed; bottom: 24px; right: 24px; z-index: 200;
  display: flex; align-items: center; gap: 8px;
  max-width: 380px; padding: 11px 16px; border-radius: 12px;
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--line);
  box-shadow: 0 8px 32px rgba(0,0,0,.20);
  font-size: 13px; font-weight: 500; color: var(--ink);
  opacity: 0; animation: fadeIn .2s, fadeOut .3s 2.7s; animation-fill-mode: forwards;
}
.toast::before {
  content: ''; flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--ink-dim);
}
.toast.success::before { background: var(--yes); }
.toast.error::before   { background: var(--no); }
.toast.info::before    { background: var(--accent); }

@keyframes fadeIn  { from { opacity: 0; transform: translateY(8px); }
                     to   { opacity: 1; transform: translateY(0); } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes pulse   { 0%, 100% { opacity: 1; } 50% { opacity: .72; } }
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; opacity: 1; }
}

/* ── touch ───────────────────────────────────────────────────────────────── */
/* The 44pt minimum is a TOUCH rule, and this is a desktop tool driven by a
   mouse and the number row — sized for touch throughout, the transport row
   alone would be 44px of button per control and the video would lose a third
   of its height to chrome nobody taps. So the density adapts to the input
   instead: fine pointers keep the compact sizes above, and anything coarse
   (iPad, a touchscreen laptop) gets every control grown past 44.
   The one exception is .icon-btn, which pads its hit area out to 44 with a
   ::before on every device — it is small enough that a stylus would miss it. */
@media (pointer: coarse) {
  #video-loader, #name-field, .src-field,
  .tbtn, .seg-static button, #speed-btn, #time-wrap { height: 44px; }
  /* Width matters as much as height for the glyph-only controls — play, mute,
     the frame steppers and the zoom pair are all 32px wide by default. */
  .tbtn { width: 44px; }
  .seg-static button { min-width: 44px; }
  /* Pop-up menu rows are targets too, and 27px was the tightest thing left.
     min-height rather than padding: the row's line-height is 1, so padding
     alone lands at 39 and would need re-deriving if the type size ever moves. */
  #speed-menu button, .ffm-row { min-height: 44px; padding: 0 12px; }
  /* The zoom badge is a reset button, not just a readout. */
  #video-zoom-badge { min-width: 56px; min-height: 44px; }
  /* No hover on a touch screen, so the volume slider cannot be something you
     reveal — it sits inline in the row, which wraps at these widths anyway. */
  #vol-pop {
    position: static; transform: none; opacity: 1; visibility: visible;
    background: none; border: 0; box-shadow: none; backdrop-filter: none;
    -webkit-backdrop-filter: none; padding: 0 0 0 10px;
  }
  #vol-range { height: 44px; }
  #vol-range::-webkit-slider-thumb { width: 18px; height: 18px; margin-top: -7px; }
  #vol-range::-moz-range-thumb { width: 18px; height: 18px; }
  .icon-btn { width: 44px; height: 44px; }
  .seg button { padding-top: 13px; padding-bottom: 13px; }
  .punch-btn { min-height: 44px; }
  .pmatrix { grid-template-columns: 1fr 54px 54px; }
  .menu-item { padding: 12px 10px; }
  #btn-unsure-filter, #btn-type-filter { padding: 10px 12px; }
  #btn-cancel { padding: 12px; }
  .label-entry .label-edit,
  .label-entry .label-delete { width: 44px; height: 44px; opacity: .8; }
  .edit-row button { padding: 12px 8px; }
  #seek-bar { height: 44px; }
  #seek-bar::-webkit-slider-thumb { margin-top: -6px; width: 20px; height: 20px; }
  #seek-bar::-moz-range-thumb { width: 20px; height: 20px; }
  #timeline-minimap { height: 16px; }
}

/* ── adaptive layout ─────────────────────────────────────────────────────── */
/* Regular width (desktop / iPad landscape) is the three-column layout above.
   Below that the two sidebars narrow, and below ~1000px they stop being
   sidebars: the video takes the full width and the catalogue and the label
   list stack under it, which is the compact size class this tool can
   actually be used in. */
@media (max-width: 1240px) {
  #punch-panel { width: 244px; }
  #label-panel { width: 276px; }
  #time-wrap { min-width: 0; }
}
@media (max-width: 1000px) {
  #main-layout { flex-direction: column; overflow-y: auto; }
  #punch-panel, #label-panel { width: auto; order: 2; overflow: visible; }
  #video-container { order: 1; }
  #video-viewport { min-height: 46vh; }
  #label-panel { min-height: 0; }
  #label-log { max-height: 320px; }
  .pmatrix { grid-template-columns: 1fr 56px 56px; }
}
@media (max-width: 720px) {
  #top-bar { height: auto; flex-wrap: wrap; gap: 8px; padding: 8px 12px; }
  .nav-title { font-size: 15px; }
  /* .nav-actions itself is flex:none (never shrinks) — fine at desktop
     widths, but on a narrow screen its own children (name field, language
     field, help button) can add up to more than the row has room for. That
     used to just barely fit; a wider language name (e.g. "Nederlands") was
     enough to push the help button off the edge. Letting the row wrap keeps
     every control reachable instead of clipping whatever runs off the
     right. */
  .nav-actions { flex-wrap: wrap; justify-content: flex-end; row-gap: 8px; }
  /* .nav-group itself needs the same wrap now that .nav-actions' direct
     children are the two groups plus the divider between them, not every
     control individually — otherwise a group wider than the viewport would
     overflow instead of wrapping control-by-control the way this row did
     before the groups existed. The divider is a mid-row ornament, not
     something worth its own wrapped line, so it hides here instead. */
  .nav-group { flex-wrap: wrap; row-gap: 8px; }
  .nav-div { display: none; }
}
