/* ══ Identity: the shot timer ═════════════════════════════════════════════
   The product's world is the timer on your belt: black polymer shell, amber
   branding, a grey-green LCD. The palette and type come from that object —
   Chakra Petch for the equipment voice, DSEG7 for the LCD digits. */
@font-face {
  font-family: "Chakra Petch";
  src: url("/fonts/ChakraPetch-SemiBold.ttf") format("truetype");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Chakra Petch";
  src: url("/fonts/ChakraPetch-Bold.ttf") format("truetype");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "DSEG7";
  src: url("/fonts/DSEG7Classic-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

:root {
  --bg: #0f1216;              /* polymer shell */
  --bg2: #12161c;
  --panel: #171c24;
  --panel2: #1e242e;
  --line: #2a313d;
  --line2: #3b4451;
  --text: #edf1f7;
  --muted: #93a0b4;
  --accent: #f5b921;          /* timer amber */
  --accent2: #ffd35c;
  --blue: #6d9fce;
  --green: #46d68c;
  --red: #e4574f;
  /* Hit quality — an ordinal severity scale for match analytics, NOT a
     categorical palette. Validated against --panel for colourblind separation
     (worst pair deutan ΔE 7.7, legal only because every segment carries a text
     label) and for the dark-mode lightness band. Do not re-pick by eye. */
  --hit-a: #35ad6d;   /* A — full value */
  --hit-c: #b3861a;   /* C — points dropped */
  --hit-d: #8a6d15;   /* D — same hue, one step darker */
  --pen:   #b2413a;   /* M and NS — both the same −10 penalty */
  /* --pen as ink is 3.0:1 on the panel — a fill colour, not a text
     colour. This is the same hue lifted to 5.7:1 so a headline number
     can be red and still readable. */
  --pen-ink: #ef6b62;
  --shadow: 0 6px 20px rgba(0,0,0,.35);
  --radius: 16px;
  --steel: #6d9fce;
  --accent-grad: linear-gradient(135deg, #ffd35c, #f0a31a);
  --elev-1: 0 4px 14px rgba(0,0,0,.35);
  --elev-2: 0 14px 40px rgba(0,0,0,.5);
  --t-fast: .15s ease;
  --t-med: .28s ease;
  --font: "Segoe UI Variable Text", "Segoe UI", -apple-system, "SF Pro Text",
          Roboto, Arial, sans-serif;
  --display: "Chakra Petch", var(--font);
  --lcd-font: "DSEG7", monospace;
  --lcd-bg: #161c15;          /* the LCD window */
  --lcd-ink: #cbe6ab;
  --lcd-ghost: rgba(203,230,171,.09);   /* unlit segments */
  /* Reserved bottom clearance so the sticky tabbar never overlaps a panel's
     last control (Check steel, Export audio, hints, …). Tabbar is ~62px tall
     plus breathing room; safe-area is added on top where used. */
  --tabbar-clearance: 84px;
}

body { font-family: var(--font); }

/* ── creator style controls (Export tab) ─────────────────────────────────── */
.block-label { display: block; margin: 8px 0 4px; }
.style-extras { margin: 8px 0; padding: 10px; background: var(--panel2);
  border: 1px solid var(--line); border-radius: var(--radius); }
.music-row { align-items: center; gap: 10px; margin-top: 8px; }
.music-row .hint { flex: 1; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
#music-vol { width: 100%; accent-color: var(--accent); }
.hint { color: var(--muted); font-size: 12px; margin: 4px 0; }
.reel-card { flex: 1 1 100px; text-align: center; line-height: 1.5;
  border: 2px solid var(--line); }
.reel-card small { font-weight: 400; opacity: .75; }
.reel-card:disabled { opacity: .45; cursor: not-allowed; }
/* The tapped card lights up for the whole render so it's obvious which
   template is cooking (feedback: selection was invisible). */
.reel-card.selected { border-color: var(--accent);
  background: rgba(245, 185, 33, .16);
  box-shadow: 0 0 0 2px rgba(245, 185, 33, .35), var(--elev-1); }
.reel-card.selected::after { content: "✓ selected"; display: block;
  margin-top: 4px; color: var(--accent); font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em; }
.reel-card.rendering::after { content: "⏳ rendering…"; }
#reel-balance, #reel-track { width: 100%; }
#reel-track { accent-color: var(--accent); }

/* Toast (transient confirmations) */
#toast { position: fixed; left: 50%; bottom: 84px; transform: translate(-50%, 16px);
  background: var(--panel2); color: var(--text); border: 1px solid var(--accent);
  border-radius: 999px; padding: 9px 18px; font-size: 13px; opacity: 0;
  pointer-events: none; transition: opacity var(--t-med), transform var(--t-med);
  z-index: 60; box-shadow: var(--elev-2); max-width: 86vw; }
/* `pointer-events: auto` is NOT optional here. The base rule above turns hit
   testing off so a faded-out pill can't swallow taps meant for the page
   underneath, and children inherit that — so without restoring it on .show,
   every BUTTON a toast can carry (Undo, the share prompt's Share/Keep
   private, the detection prompt's thumbs) is unclickable even while fully
   opaque. Opacity and hit testing are one gate; they must be lifted
   together. */
#toast.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }

/* ── Tactical Premium components ─────────────────────────────────────────── */
.card { background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--elev-1); }
.btn-primary { background: var(--accent-grad); color: #0d0f13; font-weight: 700;
  border: none; border-radius: 13px; padding: 13px 16px; cursor: pointer;
  transition: transform var(--t-fast), filter var(--t-fast); }
.btn-primary:hover { filter: brightness(1.05); }
.btn-primary:active { transform: translateY(1px); }
.btn-ghost { background: var(--panel2); color: var(--text);
  border: 1px solid var(--line); border-radius: 13px; padding: 13px 16px;
  cursor: pointer; transition: border-color var(--t-fast); }
.btn-ghost:hover { border-color: var(--line2); }
.chip, .pill { display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; border-radius: 8px; padding: 4px 9px;
  background: var(--panel2); border: 1px solid var(--line); color: var(--text); }
.pill--hf { background: var(--accent-grad); color: #0d0f13; border: none; font-weight: 700; }
.badge--pb { font-size: 10.5px; font-weight: 700; color: #0d0f13;
  background: var(--accent-grad); border-radius: 999px; padding: 3px 9px; }
.stat-tile { background: var(--panel); border: 1px solid var(--line);
  border-radius: 13px; padding: 10px; text-align: center; }
.hero { background: var(--panel); border: 1px solid var(--line);
  border-radius: 16px; padding: 14px; box-shadow: var(--elev-1); position: relative; }
@keyframes stpop { from { transform: scale(.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.reveal { animation: stpop var(--t-med) both; }
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; } }

/* ── App nav (phone bottom bar / web top bar) ────────────────────────────── */
.appnav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  display: flex; align-items: flex-end; justify-content: space-between;
  padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
  /* No backdrop blur here: fixed chrome stays composited during EVERY scroll
     frame, and blur is a per-frame GPU cost that visibly janks scrolling on
     low-end phones. A near-opaque background keeps the layered look. */
  background: rgba(20,23,29,.97);
  border-top: 1px solid var(--line); }
.appnav__item { position: relative; flex: 1; text-align: center; font-size: 10px;
  color: var(--muted); text-decoration: none; padding: 4px 0; }
.appnav__item.is-active { color: var(--accent); font-weight: 700; }
/* Match the in-editor .tabbar's active-tab underline so the two navigation
   bars (this one outside the editor, .tabbar inside it) read as one language. */
.appnav__item.is-active::after {
  content: ""; position: absolute; top: -8px; left: 22%; right: 22%; height: 3px;
  border-radius: 0 0 3px 3px; background: var(--accent-grad); }
.appnav__ic { display: block; font-size: 18px; margin-bottom: 2px; }
.appnav__add { flex: 0 0 auto; color: var(--muted); }
.appnav__add .appnav__ic { width: 50px; height: 50px; margin: -18px auto 2px;
  border-radius: 16px; background: var(--accent-grad); color: #0d0f13;
  display: flex; align-items: center; justify-content: center; font-size: 26px;
  box-shadow: 0 6px 16px rgba(232,200,74,.4); }
body { padding-bottom: 78px; }   /* room for the fixed bar */

@media (min-width: 720px) {
  /* Float a slim pill nav into the top centre, alongside the existing topbar. */
  .appnav { position: fixed; top: 9px; left: 50%; transform: translateX(-50%);
    right: auto; bottom: auto; width: auto; z-index: 11; gap: 4px;
    padding: 4px; border: 1px solid var(--line); border-top: 1px solid var(--line);
    border-radius: 999px; box-shadow: var(--elev-1); }
  .appnav__item { flex: 0 0 auto; display: flex; align-items: center; gap: 6px;
    padding: 7px 14px; border-radius: 999px; font-size: 13px; }
  .appnav__item.is-active { background: var(--panel2); }
  .appnav__item.is-active::after { content: none; }  /* pill highlight replaces the underline here */
  .appnav__ic { display: inline; font-size: 15px; margin: 0; }
  .appnav__add { color: var(--text); }
  .appnav__add .appnav__ic { width: auto; height: auto; margin: 0; padding: 3px 8px;
    border-radius: 8px; font-size: 15px; box-shadow: none; }
  body { padding-bottom: 0; }
}
.appnav.is-hidden { display: none; }
body.editing .appnav { display: none; }   /* full-screen editor hides the global nav */
/* …and with the nav gone, its clearance must go too: the editor shell is an
   exact one-screen frame (100dvh − topbar), so a leftover 78px body padding
   makes the whole page overflow and scroll-fight the waveform on phones. */
body.editing { padding-bottom: 0; }

/* ── Home view ───────────────────────────────────────────────────────────── */
.home-greet { display: flex; justify-content: space-between; align-items: center; margin: 4px 2px 10px; }
.home-hi { color: var(--muted); font-size: 14px; }
.acts-row { display: flex; gap: 9px; margin: 0 0 12px; }
.acts-row .acts-add { flex: 2; }
.acts-row .btn-ghost { flex: 1; }
#home-hero { margin-bottom: 11px; }
.home-time { font-size: 40px; font-weight: 800; line-height: 1.02; margin: 5px 0 2px; }
.home-time span { font-size: 17px; opacity: .55; }
#home-hero .pills { display: flex; gap: 6px; margin-top: 9px; }
#home-standing { padding: 13px 14px; margin-bottom: 11px; }
.rank-row { display: flex; align-items: center; gap: 12px; margin-top: 7px; }
.rank-no { font-size: 34px; font-weight: 800; color: var(--steel); }
.rank-no small { font-size: 13px; color: var(--muted); font-weight: 600; }
.rank-bar { flex: 1; height: 8px; background: var(--panel2); border: 1px solid var(--line);
  border-radius: 999px; overflow: hidden; }
.rank-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--steel), #8fc8f5); }
.seebtn { display: inline-block; margin-top: 8px; color: var(--steel); font-size: 12px; text-decoration: none; }
.pb-strip { display: flex; gap: 9px; margin-bottom: 12px; flex-wrap: wrap; }
.pb-strip .stat-tile { flex: 1 1 45%; min-width: 0; }
.pbval { font-size: 19px; font-weight: 800; margin-top: 2px; }
/* Trend tiles: direction line under the latest value + tiny PB reference. */
.trend { font-size: 11px; font-weight: 700; margin-top: 4px; white-space: nowrap; }
.trend--up { color: var(--green); }
.trend--down { color: var(--red); }
.trend--flat, .trend--none { color: var(--muted); }
.pb-sub { font-size: 10px; color: var(--muted); margin-top: 2px; }
.chip.trend--up { color: var(--green); border-color: var(--green); }
.chip.trend--down { color: var(--red); border-color: var(--red); }
.chip.trend--flat { color: var(--muted); }
#home-recent { padding: 6px 14px; }
.recent-row { display: flex; justify-content: space-between; padding: 9px 2px; border-top: 1px solid #23272f; font-size: 13px; }
.recent-row:first-child { border-top: none; }
.recent-row--tap { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.recent-row--tap:active { opacity: .7; }
/* Strava-style action strip revealed by tapping a Home run row */
.run-actions { display: flex; gap: 8px; flex-wrap: wrap; padding: 4px 2px 10px; }
.run-actions .btn { font-size: 12.5px; padding: 8px 12px; min-height: 36px;
  text-decoration: none; display: inline-flex; align-items: center; }
/* Home activity feed: your runs + everyone's shared runs, newest first */
/* The Home feed and the log book wear leaderboard.css's lb-row system (the
   PractiScore anatomy shared with the league/public boards) — these are the
   few app-side extras on top of it. */
.lb-row--tap { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.lb-row--tap:active { opacity: .7; }
.lb-rank small { font-size: 11px; font-weight: 600; color: var(--muted);
  margin-left: 1px; }
.lb-stat.lb-reload { color: var(--steel); }
.feed-vis { font-weight: 400; font-size: 12px; }
/* Log-book rows: a third grid row for the old table's action buttons, and
   room for the (manual)/(N strings) tags beside the big time. Explicit
   columns: the stat lines get their content width (wrapping only past 70%
   of the row — fit-content), the name flexes and truncates, the donut
   keeps the right edge. */
/* Doubled class: leaderboard.css loads AFTER style.css on the pages that
   use these rows, and its own .lb-row rule ties on specificity — a bare
   .lb-row--book would lose the grid-template override to source order. */
.lb-row.lb-row--book { grid-template-columns: minmax(0, 1fr) fit-content(70%) auto;
  grid-template-areas: "name  stats donut"
                       "rank  stats donut"
                       "acts  acts  acts"; }
.lb-row.lb-row--book .lb-rankpct { display: flex; align-items: baseline;
  gap: 6px; flex-wrap: wrap; }
.lb-acts { grid-area: acts; display: flex; justify-content: flex-end;
  align-items: center; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.lb-acts .btn-ghost { padding: 5px 9px; font-size: 12px; }
.home-empty { color: var(--muted); margin-top: 6px; }

/* ── Render-complete result card ─────────────────────────────────────────── */
.result-card { background: var(--panel); border: 1px solid var(--line);
  border-radius: 16px; box-shadow: var(--elev-2); padding: 16px; margin-top: 10px;
  position: relative; overflow: hidden; text-align: center; }
.result-pb { margin-bottom: 6px; }
.result-time { font-size: 44px; font-weight: 800; line-height: 1.02; margin: 4px 0; }
.result-time span { font-size: 18px; opacity: .55; }
.result-card .pills { justify-content: center; margin: 6px 0 12px; display: flex; gap: 6px; flex-wrap: wrap; }
.result-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 10px; }
.result-actions .btn-ghost { flex: 1 1 auto; text-align: center; text-decoration: none; min-width: 120px; }
.result-saved { color: var(--green); font-weight: 700; align-self: center; }
#download-btn.wide { display: block; width: 100%; text-align: center; text-decoration: none; }
.confetti { position: absolute; top: -10px; width: 8px; height: 12px; border-radius: 2px;
  animation: confetti-fall 1.5s ease-in forwards; }
@keyframes confetti-fall {
  0% { transform: translateY(-10px) rotate(0); opacity: 1; }
  100% { transform: translateY(360px) rotate(540deg); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .confetti { display: none; } }

/* ── Editor light consistency pass (styling only — no interaction changes) ── */
.btn.primary { background: var(--accent-grad); color: #0d0f13; border: none; font-weight: 700; }
.btn.success { background: var(--green); color: #06210f; border: none; font-weight: 700; }

/* ── Boards / standings tables (Progress + Leagues) ──────────────────────── */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line); border-radius: 14px; background: var(--panel);
  box-shadow: var(--elev-1); }
/* Scrolling-shadow cue on phones: table.board forces 480px min-width while
   scrollbars are hidden below 720px, so a clipped table gave no sign it
   scrolls at all. The `local` covers ride with the content and mask the
   fixed edge shadows exactly when that edge is fully scrolled into view. */
@media (max-width: 719px) {
  .table-scroll { background:
    linear-gradient(90deg,  var(--panel) 40%, rgba(0,0,0,0)) left  / 28px 100% no-repeat local,
    linear-gradient(270deg, var(--panel) 40%, rgba(0,0,0,0)) right / 28px 100% no-repeat local,
    linear-gradient(90deg,  rgba(0,0,0,.5), rgba(0,0,0,0)) left  / 12px 100% no-repeat scroll,
    linear-gradient(270deg, rgba(0,0,0,.5), rgba(0,0,0,0)) right / 12px 100% no-repeat scroll,
    var(--panel); }
}
table.board { width: 100%; border-collapse: collapse; font-size: 14px; min-width: 480px; }
table.board th, table.board td { text-align: left; padding: 10px 12px;
  border-bottom: 1px solid var(--line); white-space: nowrap; }
table.board thead th { position: sticky; top: 0; z-index: 1;
  background: var(--panel2); color: var(--muted); font-size: 11px;
  text-transform: uppercase; letter-spacing: .06em; }
table.board td.num, table.board th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.board tbody tr:hover { background: rgba(255,255,255,.03); }
table.board tbody tr:last-child td { border-bottom: none; }
tr.you td { background: rgba(90,169,236,.14); }
tr.you td:first-child { box-shadow: inset 3px 0 0 var(--steel); }
.you-chip { display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 700;
  color: #0d0f13; background: var(--accent-grad); border-radius: 999px; padding: 1px 7px; }
tr.gap td { padding: 4px; border: none; background: transparent; }
.lg-card { display: flex; justify-content: space-between; align-items: center;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  padding: 14px; box-shadow: var(--elev-1); transition: border-color var(--t-fast); }
.lg-card:hover { border-color: var(--line2); }
.crumbs { display: flex; gap: 6px; align-items: center; color: var(--muted);
  font-size: 13px; margin: 10px 0; flex-wrap: wrap; }
.crumbs a { cursor: pointer; color: var(--steel); text-decoration: none; }

* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background:
    radial-gradient(1100px 480px at 50% -10%, rgba(232,200,74,.06), transparent 60%),
    linear-gradient(180deg, var(--bg2), var(--bg));
  background-attachment: fixed;
  color: var(--text);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

.topbar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; gap: 12px;
  padding: env(safe-area-inset-top) 18px 0;
  min-height: 56px;
  /* No backdrop blur (sticky chrome — see .appnav for why). */
  background: rgba(20, 23, 29, .95);
  border-bottom: 1px solid var(--line);
}
.brand { font-weight: 800; letter-spacing: .2px; font-size: 16px; }
.expiry { color: var(--muted); font-size: 13px; }

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; position: relative; }
.icon-btn { background: none; border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; width: 44px; height: 44px; font-size: 18px; cursor: pointer; }
.overflow-menu { position: absolute; top: 44px; right: 0; z-index: 20;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: var(--shadow); padding: 6px; min-width: 180px;
  display: flex; flex-direction: column; gap: 2px; }
.overflow-menu a, .overflow-menu button { text-align: left; background: none; border: none;
  color: var(--text); font-size: 14px; padding: 10px 12px; border-radius: 8px;
  cursor: pointer; text-decoration: none; }
.overflow-menu a:hover, .overflow-menu button:hover { background: var(--panel2); }
.menu-email { color: var(--muted); font-size: 12px; padding: 6px 12px;
  border-top: 1px solid var(--line); margin-top: 4px; }

main { max-width: 780px; margin: 0 auto; padding: 18px 16px 48px;
  animation: fade .25s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.view.hidden, .hidden { display: none !important; }
h1, h2 { letter-spacing: -.01em; }

/* Upload */
.dropzone {
  border: 2px dashed var(--line2); border-radius: 18px;
  background: linear-gradient(180deg, var(--panel), var(--bg2));
  padding: 48px 22px; text-align: center; box-shadow: var(--elev-1);
  transition: border-color var(--t-fast), background var(--t-fast),
              box-shadow var(--t-fast), transform var(--t-fast);
}
.dropzone:hover { border-color: var(--accent); }
.dropzone.drag { border-color: var(--accent); transform: scale(1.01);
  box-shadow: 0 0 0 3px rgba(232,200,74,.18), var(--elev-2);
  background: radial-gradient(420px 180px at 50% 0, rgba(232,200,74,.12), transparent 60%), var(--panel2); }
.dz-icon { font-size: 44px; filter: drop-shadow(0 4px 10px rgba(0,0,0,.4)); }
.dz-title { font-size: 21px; font-weight: 800; margin: 10px 0 2px; }
.dz-sub { color: var(--muted); margin: 0 0 18px; }
.dz-limits { color: var(--muted); font-size: 13px; margin-top: 16px; }
.dropzone .dz-limits { display: inline-block; background: var(--panel2);
  border: 1px solid var(--line); border-radius: 999px; padding: 5px 12px; }

/* Buttons */
.btn {
  appearance: none; cursor: pointer; -webkit-tap-highlight-color: transparent;
  background: var(--panel2); color: var(--text);
  border: 1px solid var(--line2); border-radius: 12px;
  padding: 12px 16px; font-size: 15px; font-weight: 650;
  min-height: 46px;
  transition: transform .08s ease, filter .15s ease, background .15s, border-color .15s, box-shadow .15s;
}
.btn:hover { border-color: #4a5160; }
.btn:active { transform: translateY(1px); filter: brightness(.96); }
.btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.btn.primary { background: linear-gradient(180deg, var(--accent2), var(--accent));
  color: #1a1606; border-color: transparent; box-shadow: 0 4px 14px rgba(232,200,74,.25); }
.btn.primary:hover { filter: brightness(1.04); }
.btn.success { background: linear-gradient(180deg, #4fe294, var(--green)); color: #06200f;
  border-color: transparent; text-align: center; text-decoration: none; line-height: 22px;
  box-shadow: 0 4px 14px rgba(61,220,132,.22); }
.btn.alt { background: linear-gradient(180deg, #6fb6f0, var(--blue)); color: #04203a;
  border-color: transparent; box-shadow: 0 4px 14px rgba(90,169,236,.25); }
.btn.alt:hover { filter: brightness(1.04); }
.btn.ghost { background: transparent; color: var(--muted); border-color: var(--line); }
.btn.ghost:hover { color: var(--text); }
.btn.wide { width: 100%; display: block; margin-top: 10px; }

/* Editor — app-like layout */
/* In the editor view the page itself must not scroll: only .tool-area scrolls.
   `body.editing` (set by showView) overrides main's padding to 8px/0, and the
   shell height fills the viewport below the topbar. Using a body class instead
   of :has() keeps broad browser support. */
.editor-shell { display: flex; flex-direction: column;
  height: calc(100dvh - 64px - env(safe-area-inset-top)); /* 56 topbar + 8 editor top pad; env() for notch */ }
/* #run-view's "log a run without a video" form (index.html) also sits inside
   an .editor-shell, but outside body.editing — .stage/.tool-area/.tabbar's
   own internal-scroll layout (below) doesn't apply to it, so its content
   (score card + a save-error that can now carry an action button, e.g. the
   funnel's "Resend the link") just overflowed this fixed-height flex box
   silently, landing behind the fixed .appnav bar with no way to scroll to
   it. Giving the shell its own scroll here (only outside body.editing, so
   the real editor's one-screen frame is untouched) turns that overflow into
   a reachable scroll instead. */
body:not(.editing) .editor-shell { overflow-y: auto; }
body.editing main { padding-top: 8px; padding-bottom: 0; }

.ed-header { display: flex; align-items: center; gap: 8px; padding: 6px 2px 10px; }
.ed-home { margin-left: auto; font-size: 18px; }
.ed-back { background: none; border: none; color: var(--muted); font-size: 26px;
  line-height: 1; padding: 0 6px; cursor: pointer; min-height: 40px; }
.ed-back:hover { color: var(--text); }
.ed-title { font-weight: 700; font-size: 16px; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* flex-shrink + capped max-height + its own scroll: on a short viewport (phone
   with Safari's address bar expanded) the stacked video/strings/beep-confirm/
   waveform/stats can outgrow the available space. Without this the whole
   .stage would overflow its fixed-height .editor-shell and push .tabbar off
   the bottom of the screen. The cap is the editor-shell's own height minus a
   reserved minimum for .ed-header (56px) + .tabbar (~62px, plus its own
   safe-area padding, accounted for separately below) + a genuinely usable
   chunk of .tool-area (~160px — enough to read a button, not just a sliver
   of one), so on a normal-height phone .stage never clips content it had
   room for — it only kicks in once the viewport is genuinely too short,
   and both the tab bar AND the primary buttons below the fold stay reachable. */
.stage { flex: 0 0 auto;
  max-height: calc(100dvh - 64px - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 278px);
  overflow-y: auto; -webkit-overflow-scrolling: touch; }
/* On non-marking tabs the stage is hidden so fields/text get the full height. */
.editor-shell.hide-stage .stage { display: none; }

/* ── floating mini-player ─────────────────────────────────────────────────
   Form tabs (Details/Score/Export) and a manual ▾ collapse keep the video
   floating in the corner instead of eating the phone screen. Tap it to
   bring the full stage back. */
/* Sits to the RIGHT of .overlay-eye (top-left cluster) — it used to share
   left:6/top:6 with the eye and, at the higher z-index, swallowed every tap
   meant for the overlay toggle. The top-right corner is taken on the targets
   tab (⟲ 1× / ⛶), so side-by-side on the left is the collision-free spot. */
.stage-mini-btn { position: absolute; top: 8px; left: 46px; z-index: 5;
  background: rgba(13,15,19,.72); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px; width: 30px; height: 30px;
  font-size: 15px; line-height: 1; cursor: pointer; }
.editor-shell.stage-mini .stage > :not(.player-wrap) { display: none !important; }
/* Dock above the tab bar, not at a fixed 70px guess — Task 1's
   --tabbar-clearance already reserves that much empty space at the bottom
   of .tool-area, so anchoring off it (plus a small gap) guarantees the
   mini-player never lands on top of content like the Export tab's Square
   format button. */
.editor-shell.stage-mini .player-wrap {
  --mini-dock-bottom: calc(var(--tabbar-clearance) + 8px + env(safe-area-inset-bottom, 0px));
  position: fixed; right: 10px;
  bottom: var(--mini-dock-bottom); top: auto;
  width: 148px; z-index: 45; margin: 0; border-radius: 12px; overflow: hidden;
  box-shadow: var(--elev-2); border: 1px solid var(--line2); cursor: pointer; }
/* The floating player sits fixed over the bottom-right corner at every scroll
   position — without a reserved gutter it silently blocks whatever form field
   or primary button (Save run, Render my video, …) happens to scroll under it.
   Reserve that column so tool-area content never renders underneath it. */
.editor-shell.stage-mini .tool-area { padding-right: 172px; }
/* On a phone that gutter is fatal: 172px off a ~343px content column leaves
   169px for controls whose labels demand 220px — every field on the form
   tabs overflowed. Under 720px the floater shrinks and floats OVER content
   instead (it is draggable and dismissible), and the column gets its width
   back. */
@media (max-width: 719px) {
  .editor-shell.stage-mini .tool-area { padding-right: 0; }
  .editor-shell.stage-mini .player-wrap { width: 118px; }
}
.editor-shell.stage-mini .player-wrap video { width: 100%; height: auto;
  pointer-events: none; }
.editor-shell.stage-mini .overlay-eye,
.editor-shell.stage-mini .stage-mini-btn { display: none; }
.editor-shell.stage-mini #overlay-canvas { pointer-events: none; }
/* drag-to-move + a ✕ to dismiss, shown only on the floating mini-player */
.editor-shell.stage-mini .player-wrap { cursor: move; touch-action: none; }
/* Transcode-in-progress overlay: fully covers the (src-less) video element
   so a not-yet-playable source never shows a broken play glyph. */
.player-processing { position: absolute; inset: 0; z-index: 4;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10, 12, 16, 0.92); border-radius: inherit; }
.player-processing.hidden { display: none; }
.player-processing .pp-box { text-align: center; max-width: 320px;
  padding: 16px; color: var(--text); }
.player-processing .bar { margin: 12px auto; width: 220px; }
.player-processing #pp-label { color: var(--muted); font-size: 13px;
  margin: 0; }

.stage-hide-btn { display: none; }
.editor-shell.stage-mini .stage-hide-btn {
  display: flex; align-items: center; justify-content: center;
  position: absolute; top: 4px; right: 4px; z-index: 46;
  width: 30px; height: 30px; min-height: 0; padding: 0;
  background: rgba(13,15,19,.8); color: var(--text);
  border: 1px solid var(--line2); border-radius: 50%;
  font-size: 12px; line-height: 1; cursor: pointer; }
.editor-shell.stage-mini.mini-hidden .player-wrap { display: none !important; }
/* ▶/⏸ on the floating mini-player: its <video> has controls off and
   pointer-events:none (tapping the floater restores the stage), so this is
   the floater's only playback control. Bottom-left, clear of the ✕. */
.mini-play-btn { display: none; }
.editor-shell.stage-mini .mini-play-btn {
  display: flex; align-items: center; justify-content: center;
  position: absolute; bottom: 4px; left: 4px; z-index: 46;
  width: 30px; height: 30px; min-height: 0; padding: 0;
  background: rgba(13,15,19,.8); color: var(--text);
  border: 1px solid var(--line2); border-radius: 50%;
  font-size: 12px; line-height: 1; cursor: pointer; }
/* Dismissing the mini-player shrinks it down to the small "▸ Video" pill —
   release most of the reserved gutter, just enough to still clear the pill. */
.editor-shell.stage-mini.mini-hidden .tool-area { padding-right: 110px; }
/* "show video" pill — visible only while the mini-player is dismissed */
.mini-show-pill { position: fixed; right: 10px;
  bottom: calc(70px + env(safe-area-inset-bottom, 0px)); z-index: 45;
  display: inline-flex; align-items: center; gap: 4px;
  background: rgba(13,15,19,.82); color: var(--text);
  border: 1px solid var(--line2); border-radius: 999px;
  padding: 8px 13px; font-size: 13px; font-weight: 600;
  box-shadow: var(--elev-2); cursor: pointer; }
/* Player reserves its box from the session's known width/height (app.js sets
   --video-aspect as soon as the session loads, well before the <video> tag's
   own loadedmetadata fires) so the box never resizes out from under the
   quick-mark buttons / overlay toggle once the real video metadata arrives.
   The aspect-ratio + max-height cap live on the VIDEO (a replaced element
   with a definite 100% width), NOT on .player-wrap: on a non-replaced box,
   a max-height combined with aspect-ratio TRANSFERS into a max-width
   (30dvh × 4/3 ≈ a ~320px-wide player) and shrinks the whole player to a
   sliver that the quick-mark buttons then bury — a real field regression. */
.player-wrap { position: relative; margin-bottom: 8px; line-height: 0; }
.player { width: 100%; height: auto;
  aspect-ratio: var(--video-aspect, 16 / 9);
  max-height: 30dvh; object-fit: contain;
  border-radius: 12px; background: #000; display: block; }
#overlay-canvas { position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; border-radius: 12px; }
.overlay-eye { position: absolute; top: 8px; left: 8px; z-index: 2;
  background: rgba(0,0,0,.5); border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; padding: 4px 8px; font-size: 13px; cursor: pointer;
  min-height: 30px; min-width: 30px; /* match .stage-mini-btn beside it */ }
.overlay-eye[aria-pressed="false"] { opacity: .45; }

/* Zoom-reset + full-screen toggle: targets-tab-only controls (see the
   .targets-focus rule below), top-right so they never collide with the
   overlay-eye/stage-mini-btn cluster in the top-left. Hidden everywhere
   else, including stage-mini (the floating mini-player never zooms). */
.zoom-reset-btn, .fs-toggle-btn { display: none; }
#editor-view .editor-shell.targets-focus .zoom-reset-btn,
#editor-view .editor-shell.targets-focus .fs-toggle-btn {
  display: flex; align-items: center; justify-content: center;
  position: absolute; top: 8px; z-index: 7;
  background: rgba(13,15,19,.78); color: var(--text);
  border: 1px solid var(--line2); border-radius: 8px;
  min-height: 30px; padding: 4px 9px; font-size: 13px; font-weight: 650;
  cursor: pointer; }
#editor-view .editor-shell.targets-focus .zoom-reset-btn { right: 46px; }
#editor-view .editor-shell.targets-focus .fs-toggle-btn { right: 8px; }
#editor-view .editor-shell.targets-focus .fs-toggle-btn[aria-pressed="true"] {
  border-color: var(--accent); color: var(--accent); }

/* Marking-mode indicator — the "+ Add target" button text alone can scroll
   off-screen, leaving users unsure whether drag-to-mark is armed. This pill
   sits over the video (impossible to miss) while marking is active, and the
   dashed outline on the player itself reinforces the armed state even if
   the pill is glanced past. */
.player-wrap.marking { outline: 2px dashed var(--accent); outline-offset: -2px; }
/* ── Targets tab: a visual-only stage ──────────────────────────────────────
   Marking a target is "find the frame, drag a box on it". Everything driven
   by the AUDIO timeline — the beep/shot quick-marks, the strings bar, the
   beep-confirm strip, the waveform and the stats — answers a different
   question and only competes for the height the video needs.

   The frame-step arrows (.qm-step) deliberately SURVIVE: the panel's own
   instructions say to pause first, and you cannot mark a target on a frame
   where it isn't visible. Hiding them would break the task this mode is for. */
#editor-view .editor-shell.targets-focus .quick-marks > :not(.qm-step),
#editor-view .editor-shell.targets-focus #strings-bar,
#editor-view .editor-shell.targets-focus #beep-confirm,
#editor-view .editor-shell.targets-focus .wave-block,
#editor-view .editor-shell.targets-focus .stats-strip { display: none !important; }

/* The quick-marks row exists only for the two step arrows now — keep it off
   the video's centre so it never eats a drag that starts low in the frame. */
#editor-view .editor-shell.targets-focus .quick-marks { justify-content: center; gap: 24px; }

/* Reclaim the height the waveform and strips were using — and let the box
   take the VIDEO's shape instead of the container's.

   `.player` is `width:100%` + `aspect-ratio` + `max-height`. Once max-height
   clamps, those fight: the element stays full-width but goes short, so the
   box ends up an aspect no video has (e.g. 1050x250 = 4.2), and
   `object-fit:contain` fits the real frame inside it — a 16/9 clip became
   444x250 with the rest black. Widening the box made the picture SMALLER.

   How much that wastes depends entirely on which cap binds. Measured at a
   1050px-wide stage: at a 30dvh cap the box is ~1050x234 and a 16/9 clip
   renders 416x234 — 60% of the area is dead black. At 80dvh the box is
   ~1050x576, the clip renders ~1024x576, and the waste falls to ~4%.

   So the cap IS the lever — but it MUST stay inside `.stage`'s own budget.
   `.stage` is `max-height: calc(100dvh - 64px - safe-areas - 278px)` with
   `overflow-y:auto`, and that 278px reserve is deliberate: header + tabbar +
   a genuinely usable slice of .tool-area. A flat `80dvh` here ignored it —
   on a 720px viewport the stage gets ~378px while the video demanded 576px,
   so the stage just scrolled: a CLIPPED video with the controls pushed out
   of reach. Bigger number, unusable editor.

   So derive the cap from the same budget instead of guessing a dvh. On this
   tab the waveform/strings/beep/stats are hidden, so the video no longer
   shares the stage with them and can take the whole budget minus the
   frame-step row. `max()` keeps it from ever being SMALLER than the old
   desktop cap on a short window.

   NOT `width:auto`: with `height:auto` too, the box has no definite
   dimension until metadata arrives and collapses to 0x0 — taking
   #overlay-canvas (`inset:0` on the wrap) with it. Measured, not assumed.

   `.stage`'s 278px reserve is header(56) + tabbar(62) + 160px of .tool-area.
   On THIS tab the tool-area is the targets panel and does not need 160px
   visible while you are dragging a box — it scrolls, and the type sheet
   opens as a modal anyway. Reclaiming that down to a ~56px sliver (174 total)
   is what actually buys video size. Measured at 1280x720, 1050px stage, all
   fitting with no overflow:

     reserve 278 (today)  ->  565x318   180k px2   panel 268px visible
     reserve 220          ->  668x376   251k px2   panel 210px
     reserve 174 (chosen) ->  750x422   317k px2   panel 164px

   1.76x the picture area, and the tabbar plus a readable slice of the panel
   both stay on screen. */
#editor-view .editor-shell.targets-focus .stage {
  max-height: calc(100dvh - 64px - env(safe-area-inset-top)
                   - env(safe-area-inset-bottom) - 174px); }
#editor-view .editor-shell.targets-focus .player {
  max-height: calc(100dvh - 64px - env(safe-area-inset-top)
                   - env(safe-area-inset-bottom) - 174px - 60px); }

/* ── Targets tab: zoom + pan ───────────────────────────────────────────────
   The transform goes on .player and #overlay-canvas ONLY — never on
   .player-wrap. .player-wrap also contains .marking-pill, which must stay
   put and unscaled (.quick-marks now lives OUTSIDE the wrap, in .stage flow). Driving both
   transformed elements off the same custom properties (set in app.js) keeps
   them pixel-aligned with each other; transform-origin 50% 50% on both
   matches the math zoomAt()/clampZoomPan() in app.js assume. overflow:hidden
   on the wrap stops the enlarged frame spilling into the rest of the editor. */
#editor-view .editor-shell.targets-focus .player-wrap { overflow: hidden; }
#editor-view .editor-shell.targets-focus .player,
#editor-view .editor-shell.targets-focus #overlay-canvas {
  transform: translate(var(--pan-x, 0px), var(--pan-y, 0px)) scale(var(--zoom, 1));
  transform-origin: 50% 50%;
}

/* ── Targets tab: full-screen marking (CSS pseudo-fullscreen) ──────────────
   Deliberately NOT the native Fullscreen API: #target-type-sheet (the "what
   kind of target?" dialog that opens after every drag) is position:fixed at
   BODY level with z-index 60. Native fullscreen only renders descendants of
   the fullscreen element, so the sheet would vanish mid-marking. Instead,
   .stage goes position:fixed covering the viewport at z-index 50 — below
   the sheet's 60, so it still layers on top — while the surrounding chrome
   is hidden. Toggled via a body class (not :has()) because .topbar lives
   outside #editor-view entirely — same reasoning body.editing already uses.

   .stage is centered as a flex item (justify-content, main axis) rather than
   flex-centering ITS contents — .player-wrap's box must stay pixel-identical
   to .player's box (see the zoom comment above / canvasRelPoint() in app.js),
   so the wrap keeps sizing itself purely from .player's aspect-ratio box; the
   stage just centers that whole (possibly letterboxed) unit vertically. */
body.marking-fullscreen .topbar { display: none; }
body.marking-fullscreen #editor-view .ed-header,
body.marking-fullscreen #editor-view .tool-area,
body.marking-fullscreen #editor-view .tabbar { display: none !important; }
body.marking-fullscreen #editor-view .editor-shell.targets-focus .stage {
  position: fixed; inset: 0; z-index: 50; max-height: 100dvh;
  display: flex; flex-direction: column; justify-content: center;
  background: #06070a; overflow: hidden;
  padding: 0 8px env(safe-area-inset-bottom) 8px; }
body.marking-fullscreen #editor-view .editor-shell.targets-focus .player {
  max-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 64px); }

/* While marking is armed the whole frame must stay draggable, so the pill
   drops OUT of the video and sits under it in normal flow — it covered the
   top of the frame, and together with the native control bar across the
   bottom there was very little left to drag a target box on. The stage also
   gets more height, because marking is the one task that needs precision on
   a small/distant target. */
.player-wrap.marking .player { max-height: 72dvh; }
.player-wrap.marking .marking-pill { position: static; transform: none;
  max-width: 100%; margin: 6px 0 0; border-radius: 10px;
  justify-content: center; line-height: 1.4; }

.marking-pill { position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  z-index: 6; max-width: calc(100% - 16px); background: rgba(13,15,19,.88);
  color: var(--text); border: 1px solid var(--accent); border-radius: 999px;
  padding: 6px 10px; font-size: 12px; font-weight: 650; text-align: center;
  display: flex; align-items: center; gap: 6px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.marking-pill #marking-cancel { background: rgba(255,255,255,.12); color: var(--text);
  border: 1px solid var(--line2); border-radius: 999px; padding: 6px 14px;
  font-size: 12px; font-weight: 700; min-height: 30px; cursor: pointer; flex: 0 0 auto; }

/* ── Shooter-follow tap-to-correct ─────────────────────────────────────────
   The tap target for the follow crop's seed. Positioned by app.js
   (syncShooterTapLayer) to the DISPLAYED video rect, stopping above the
   native control strip — the canvas itself stays pointer-events:none, so
   play/scrub keep working while the layer is up. z-index sits above the
   canvas (auto) and below the marking pill (6) / overlay eye's siblings. */
.shooter-tap-layer { position: absolute; z-index: 3; cursor: crosshair;
  line-height: normal; background: transparent; }
.shooter-tap-caption { position: absolute; left: 50%; bottom: 8px;
  transform: translateX(-50%); white-space: nowrap; pointer-events: none;
  background: rgba(13,15,19,.88); color: var(--text);
  border: 1px solid var(--line2); border-radius: 999px;
  padding: 4px 10px; font-size: 12px; font-weight: 650; }

.stats-strip { display: flex; justify-content: space-around; gap: 10px;
  padding: 8px 4px; color: var(--muted); font-size: 13px;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.stats-strip b { color: var(--text); }

.tool-area { flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 12px 2px calc(12px + env(safe-area-inset-bottom)); }
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: fade .2s ease; }

.tabbar { flex: 0 0 auto; display: flex; gap: 2px;
  background: rgba(20,23,29,.96); border-top: 1px solid var(--line);
  padding: 8px 6px calc(8px + env(safe-area-inset-bottom)); }
.tabbar .tab { flex: 1; background: none; border: none; cursor: pointer;
  color: var(--muted); font-size: 11px; font-weight: 650; padding: 4px 0;
  border-radius: 8px; }
.tabbar .tab .i { display: block; font-size: 19px; margin-bottom: 2px;
  filter: grayscale(1) opacity(.6); }
.tabbar .tab.on { color: var(--accent); }
.tabbar .tab.on .i { filter: none; }

/* Desktop / wide screens, and short *landscape* viewports (big OS fonts,
   browser zoom, phone rotated sideways): drop the fixed one-screen "app
   frame" — let the page scroll normally and keep the tab bar pinned (sticky)
   so all tabs are always visible without zooming out. Scoped to landscape
   (or a very short height regardless of orientation) so it never matches an
   ordinary portrait phone — Safari's address bar shrinks the visible height
   well under 640px there, which used to disable the app frame and hide the
   tab bar below the fold. (Phones in portrait keep the compact app frame.) */
@media (min-width: 820px), (max-height: 640px) and (orientation: landscape), (max-height: 380px) {
  body.editing main { padding: 16px 16px 28px; }
  .editor-shell { height: auto; }
  /* bottom padding clears the sticky tab bar so the last controls aren't hidden */
  .tool-area { overflow: visible; padding-bottom: calc(var(--tabbar-clearance) + env(safe-area-inset-bottom)); }
  .tabbar { position: sticky; bottom: 0; z-index: 5; }
}
/* On a comfortable desktop window, give the video a bit more room. */
@media (min-width: 820px) and (min-height: 700px) {
  .player { max-height: 52vh; }
}

/* ≥900px: real two-column desktop frame — video column beside the tool
   column, each sized to the shell's full height. This is what kills the
   nested .stage scroll on desktop (bug: .stage used to cap itself to a
   sliver of the viewport and scroll internally, showing half its content).
   `editor-cols` is added statically in index.html to the editor view's own
   .editor-shell (not the video-less "log a run" shell) and stays inert below
   900px — gated entirely by this media query, no JS branching. */
@media (min-width: 900px) {
  /* App-frame: let the shell FILL the space actually left under the topbar
     instead of guessing it with a hardcoded offset (main's padding drifted
     from the old 8px assumption and pushed the tabbar below the fold). */
  body.editing { height: 100dvh; overflow: hidden;
    display: flex; flex-direction: column; }
  /* The 780px phone-column cap on main would clip the grid (420+20+360 =
     800px minimum) behind overflow:hidden — widen it while editing so the
     two columns actually have the desktop width they exist for. */
  body.editing main { flex: 1; min-height: 0; overflow: hidden;
    display: flex; flex-direction: column; max-width: 1360px; width: 100%; }
  body.editing #editor-view { flex: 1; min-height: 0;
    display: flex; flex-direction: column; }
  .editor-shell.editor-cols {
    display: grid;
    grid-template-columns: minmax(420px, 1fr) minmax(360px, 520px);
    grid-template-rows: auto 1fr auto;
    column-gap: 20px;
    flex: 1;
    min-height: 0;
    height: auto;
  }
  .editor-shell.editor-cols .ed-header { grid-column: 1 / -1; grid-row: 1; }
  /* Video column spans the tool-area row AND the tabbar row so its height
     matches the two stacked on the right, instead of a short video column
     next to a tall one with an empty gap underneath. */
  .editor-shell.editor-cols .stage { grid-column: 1; grid-row: 2 / 4;
    max-height: none; overflow: visible; }
  /* Row 2/3 are adjacent (not overlapping) grid tracks, so — unlike the
     sticky-tabbar single-column mode above — the tool column never needs
     bottom clearance from the tabbar; reset padding-bottom and give it back
     its own scroll (the ≥820px rule above set overflow:visible for the
     page-scroll mode, which would otherwise win here too). Longhand only —
     a padding shorthand here would clobber stage-mini's reserved
     padding-right gutter for the floating player (below). */
  .editor-shell.editor-cols .tool-area { grid-column: 2; grid-row: 2;
    overflow-y: auto; padding-bottom: 12px; }
  .editor-shell.editor-cols .tabbar { grid-column: 2; grid-row: 3; position: static; }
  /* hide-stage / stage-mini both rely on the video column being absent or
     reduced to a fixed floating box — with no stage content, the grid's
     first column would just be reserved empty space (grid-template-columns
     tracks are sized whether or not an item occupies them). Fall back to
     the same flex column used below 900px so the tool column reclaims the
     full width. grid-column/grid-row on the children above are simply
     ignored once their parent is display:flex; .tool-area keeps the
     overflow-y:auto and .tabbar keeps the position:static set above, so
     .tabbar sits as an ordinary (non-overlapping) flex item after
     .tool-area — no tabbar clearance needed here, same as the plain
     portrait app-frame below 900px. */
  .editor-shell.editor-cols.hide-stage,
  .editor-shell.editor-cols.stage-mini { display: flex; flex-direction: column; }
}

/* Quick-mark row in normal flow between the video and the waveform. It was
   absolutely positioned over the bottom of the video, which buried the native
   play/scrub/fullscreen bar on every browser (the "hidden play button" bug) —
   a toolbar row costs ~50px of height instead, and the player stays usable. */
.quick-marks { display: flex; gap: 6px; margin: 0 0 8px; }
.qm-btn { flex: 1; background: var(--panel2); color: var(--text);
  border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px;
  font-size: 13px; font-weight: 650; min-height: 44px; cursor: pointer;
  transition: border-color var(--t-fast); }
.qm-btn:hover { border-color: var(--accent); }
.qm-btn:active { transform: translateY(1px); }
.qm-btn.qm-shot { background: var(--accent-grad); color: #0d0f13; border: none; }
.qm-btn.qm-step { flex: 0 0 44px; font-size: 15px; touch-action: none; }
/* NOT qm-step: the targets tab keeps only the step arrows, and undo is a
   mark-editing control that has no meaning there. */
.qm-btn.qm-undo { flex: 0 0 48px; font-size: 15px; }
/* Mini floating player is too small for the overlay row; stage-mini already
   hides every .stage child except .player-wrap, so hide this explicitly. */
.editor-shell.stage-mini .quick-marks { display: none; }

.wave-block { margin-bottom: 12px; }
.wave-wrap { position: relative; width: 100%; border-radius: 10px; overflow: hidden;
  border: 1px solid var(--line); touch-action: none; user-select: none;
  -webkit-user-select: none; cursor: crosshair; }
.wave-scroll { position: relative; width: 100%; transform-origin: 0 0;
  will-change: transform; }
.wave-img { display: block; width: 100%; height: 96px; object-fit: fill;
  pointer-events: none; }
/* Canvas waveform (peaks data): fixed to the wrap's viewport, redrawn per
   zoom/pan by app.js. When peaks are loaded the stretched PNG hides —
   visibility, not display, because its box gives the strip its height. */
.wave-canvas { position: absolute; inset: 0; width: 100%; height: 100%;
  display: none; pointer-events: none; }
.wave-wrap.peaks .wave-canvas { display: block; }
.wave-wrap.peaks .wave-img { visibility: hidden; }
.markers { position: absolute; inset: 0; pointer-events: none; }
/* Zoom cluster: bottom-right, AWAY from the marker dots along the top edge —
   the old top-right 1× covered the delete handle of any shot near the clip's
   end. Always visible so zoom is discoverable without knowing the gestures. */
.wave-zoom { position: absolute; bottom: 6px; right: 6px; z-index: 3;
  display: flex; gap: 4px; }
.wz-btn { background: rgba(0,0,0,.55); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px;
  width: 34px; height: 34px; min-height: 0; padding: 0;
  font-size: 15px; font-weight: 700; line-height: 1; cursor: pointer; }
.wz-btn.zoom-reset { width: auto; min-width: 34px; padding: 0 6px; font-size: 12px; }
.wz-btn:disabled { opacity: .4; cursor: default; }
/* The thin line is visual only — taps on the waveform body fall through to seek. */
.marker { position: absolute; top: 0; bottom: 0; width: 2px; pointer-events: none; }
.marker.shot { background: rgba(255,255,255,.85); }
.marker.beep { background: var(--accent); width: 3px; }
/* The dot is the finger-sized DELETE handle: a >=32px transparent tap zone
   (.marker-hit) at the top of the waveform with a small visible core (an ✕
   on a coloured disc). The hit zone is grown with padding, then pulled back
   into place with an equal negative margin so it stays centred on the same
   spot as before (was a 28px box) — that padding also shifts the abs-
   positioned ::before glyph's containing block, so its top/left are nudged
   by the same amount to keep the visible ✕ disc pixel-in-place. */
.marker .dot.marker-hit { position: absolute; top: -3px; left: 50%;
  margin: -2px 0 0 -16px; width: 28px; height: 28px; padding: 2px;
  box-sizing: content-box; border-radius: 50%; pointer-events: auto;
  cursor: pointer; -webkit-tap-highlight-color: transparent; }
/* Plain disc, not an ✕ — a tap SELECTS the mark (action bar below the
   waveform) rather than deleting it, so the handle must not promise delete. */
.marker .dot.marker-hit::before { content: ""; position: absolute; top: 4px; left: 8px;
  width: 16px; height: 16px; border-radius: 50%; box-sizing: border-box;
  box-shadow: 0 1px 3px rgba(0,0,0,.55); }
.marker.shot .dot.marker-hit::before { background: #fff; }
.marker.beep .dot.marker-hit::before { background: var(--accent); }
.marker .dot.marker-hit:active::before { transform: scale(.86); }
/* Selected mark: glow the line and ring the handle so the action bar's
   target is unambiguous even with neighbouring shots close by. */
.marker.sel { box-shadow: 0 0 0 2px var(--accent); }
.marker.sel .dot.marker-hit::before {
  box-shadow: 0 0 0 2px var(--accent), 0 1px 3px rgba(0,0,0,.55); }

/* Marker action bar (shown while a mark is selected) */
.mark-actions { display: flex; align-items: center; gap: 6px; margin-top: 6px;
  flex-wrap: wrap; }
.mark-actions.hidden { display: none; }
.mark-actions #ma-label { font-size: 13px; color: var(--accent2);
  font-variant-numeric: tabular-nums; margin-right: 2px; }
.ma-btn { background: var(--panel2); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px; padding: 6px 10px;
  font-size: 12.5px; font-weight: 650; min-height: 38px; cursor: pointer; }
.ma-btn.ma-del { border-color: var(--red, #e4574f); color: var(--red, #e4574f); }

/* Guided next-step buttons (Marks → Score → Export) */
.next-step { margin-top: 12px; }

/* Score-group sub-nav: one section at a time instead of the old five-panel
   scroll. Sticky so switching sections never needs a scroll back to the top.
   The rules key off data-logsec on .tool-area — absent (any other tab, or
   attach mode), nothing is filtered and every panel behaves as before. */
.log-subnav { position: sticky; top: 0; z-index: 5;
  background: var(--bg); padding: 6px 0; margin: 0 0 8px; }
.log-subnav.hidden { display: none; }
.tool-area[data-logsec="score"] #gear-panel.active { display: none; }
.tool-area[data-logsec="score"] #run-panel .log-run-sec { display: none; }
.tool-area[data-logsec="run"] #gear-panel.active { display: none; }
.tool-area[data-logsec="run"] #run-panel .log-score-sec { display: none; }
.tool-area[data-logsec="gear"] #run-panel.active { display: none; }

/* Per-target entry helper (Score panel) */
.per-target { margin: 10px 0 4px; }
.per-target summary { cursor: pointer; color: var(--muted);
  font-size: 14px; font-weight: 650; padding: 8px 0; }
.pt-row { display: flex; gap: 6px; align-items: center; margin: 4px 0; }
.pt-t { flex: 0 0 26px; color: var(--muted); font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.pt-chip { flex: 1; position: relative; display: flex; align-items: center;
  justify-content: center; gap: 6px; background: var(--panel2);
  border: 1px solid var(--line); border-radius: 8px; min-height: 40px;
  padding: 0 26px 0 4px; font-size: 12.5px; font-weight: 650;
  color: var(--text); cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.pt-chip b { font-size: 15px; font-variant-numeric: tabular-nums; }
.pt-chip.pen b { color: var(--red); }
.pt-chip .pt-dec { position: absolute; right: 0; top: 0; bottom: 0;
  width: 24px; display: flex; align-items: center; justify-content: center;
  border-left: 1px solid var(--line); color: var(--muted); }

/* Multi-string times on the Log-a-run form (mrStrings) */
.mr-strings { margin: 4px 0; }
.mr-string-row { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
.mr-string-row .mr-string-n { flex: 0 0 64px; color: var(--muted);
  font-size: 13px; font-weight: 650; }
.mr-string-row input { flex: 1; min-width: 0; }
.mr-string-row .mr-string-del { flex: 0 0 auto; padding: 8px 12px; }
.mr-strings-total { margin: 4px 0 0 64px; font-size: 13px;
  font-variant-numeric: tabular-nums; }

/* Export choice cards: the reel/full decision explained in place */
.export-choice .seg { flex: 1 1 160px; display: flex; flex-direction: column;
  gap: 3px; align-items: center; justify-content: center; text-align: center;
  padding: 12px 10px; min-height: 64px; font-weight: 700; }
.export-choice .seg small { font-size: 11.5px; font-weight: 500;
  color: var(--muted); white-space: normal; line-height: 1.35; }
.export-choice .seg.on small { color: inherit; opacity: .85; }
/* Mag-change band: the gap between two shots, drawn on the waveform where the
   gap is actually visible (the list can only say "between these two rows").
   Cyan matches the shot-log reload row and the burnt-in RELOAD chip. Unmarked
   gaps are a faint outline — an offer, not a claim; a marked one fills in.
   The band never takes pointer events so taps still seek; only the centre
   handle does, and it is a full 32px target like the delete dots. */
.gap-band { position: absolute; top: 20px; bottom: 20px; pointer-events: none;
  border-radius: 5px; border: 1px dashed rgba(34,211,238,.45); }
.gap-band.on { background: rgba(34,211,238,.18);
  border: 1px solid rgba(34,211,238,.9); }
.gap-band .gap-handle { position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%); width: 32px; height: 32px; min-height: 0;
  padding: 0; border-radius: 50%; pointer-events: auto; cursor: pointer;
  background: rgba(8,10,13,.72); border: 1px solid rgba(34,211,238,.9);
  color: #22d3ee; font: 700 13px/1 system-ui, sans-serif;
  -webkit-tap-highlight-color: transparent; }
.gap-band .gap-handle:active { transform: translate(-50%, -50%) scale(.88); }
/* Playhead. A bare 2px blue line disappears against the bright part of a
   waveform (and against the cyan mag-change band), so it carries a dark
   outline for contrast on any background plus a wedge at the top — the same
   trick the shot dots use to stay findable at a glance. */
.playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--blue);
  left: 0; pointer-events: none; box-shadow: 0 0 0 1px rgba(6, 9, 14, .75);
  z-index: 2; }
.playhead::before { content: ""; position: absolute; top: 0; left: 50%;
  transform: translateX(-50%);
  border-left: 6px solid transparent; border-right: 6px solid transparent;
  border-top: 7px solid var(--blue);
  filter: drop-shadow(0 1px 1px rgba(6, 9, 14, .8)); }
.trim-shade { position: absolute; top: 0; bottom: 0; background: rgba(0,0,0,.55);
  pointer-events: none; }
.wave-hint { color: var(--muted); font-size: 12px; margin: 6px 2px 0; }
.hint-sub { display: block; font-size: 11px; font-weight: 400; opacity: 0.7; margin-top: 2px; }

.check { display: flex; align-items: center; gap: 10px; font-size: 14px;
  color: var(--text); margin: 4px 2px 10px; cursor: pointer; }
.check input[type="checkbox"] { flex: 0 0 auto; width: 20px; height: 20px;
  min-height: 0; padding: 0; accent-color: var(--accent); cursor: pointer; }

.tool-area .row { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
.tool-area .row .btn { flex: 1; min-width: 90px; }
/* A fixed min-width (bigger than half a phone's width) makes every labeled
   row wrap to one field per line consistently on phones — some rows used to
   wrap and others didn't depending on their content, which read as random. */
.tool-area .row label { flex: 1 1 220px; min-width: 220px; display: flex;
  flex-direction: column; gap: 4px; font-size: 13px; color: var(--muted); }
.tool-area input, .tool-area select, .tool-area textarea {
  background: var(--panel2); color: var(--text); border: 1px solid var(--line2);
  /* 16px, not 15: iOS Safari auto-zooms the page on focusing any field under
     16px and leaves it zoomed after blur (.auth-card already does this). */
  border-radius: 10px; padding: 10px; font-size: 16px; min-height: 44px;
  transition: border-color .15s, box-shadow .15s;
}
.tool-area textarea { resize: vertical; font-family: inherit; }
input:focus, select:focus, textarea:focus { outline: none;
  border-color: var(--blue); box-shadow: 0 0 0 3px rgba(90,169,236,.18); }

/* Dual-video controls */
.angle-switch { display: flex; gap: 4px; margin-bottom: 8px; }
/* flex-wrap: a row of 3-4 buttons (Format, layout picker, …) can't always
   shrink to fit — e.g. the floating mini-player reserves a gutter that
   narrows .tool-area on form tabs. Without wrap the overflowing buttons
   render past the container's edge and under the floating video instead
   of dropping to a second line. */
.seg-group, .layout-picker { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 12px; }
.seg, .lay, .pipc { flex: 1 1 76px; min-height: 40px; background: var(--panel2);
  border: 1px solid var(--line2); color: var(--muted); border-radius: 10px;
  font-size: 14px; font-weight: 650; cursor: pointer; }
.seg:hover, .lay:hover, .pipc:hover { border-color: #4a5160; }
.seg.on, .lay.on { background: var(--accent); color: #1a1606; border-color: transparent; }
.layout-picker .lay { display: flex; flex-direction: column; gap: 4px;
  align-items: center; justify-content: center; padding: 8px 4px; }
.lay-name { font-size: 11.5px; font-weight: 650; line-height: 1; }
.pip-grid .pipc { font-size: 18px; line-height: 1; }
/* Pure-CSS layout icons (no font glyphs -> identical on iOS / Android / desktop).
   currentColor follows the button text (muted, or dark on the active accent). */
.lico { display: inline-block; position: relative; width: 28px; height: 19px; color: inherit; }
.lico.h {  /* two side-by-side panes */
  background:
    linear-gradient(currentColor, currentColor) left  / 47% 100% no-repeat,
    linear-gradient(currentColor, currentColor) right / 47% 100% no-repeat; }
.lico.v {  /* two stacked panes */
  background:
    linear-gradient(currentColor, currentColor) top    / 100% 46% no-repeat,
    linear-gradient(currentColor, currentColor) bottom / 100% 46% no-repeat; }
.lico.sq { width: 19px; border: 2px solid currentColor; }   /* square frame */
.lico.pip { border: 2px solid currentColor; }               /* frame + inset corner */
.lico.pip::after { content: ""; position: absolute; right: 2px; bottom: 2px;
  width: 10px; height: 7px; background: currentColor; }
.lico.cut { border: 2px solid currentColor; overflow: hidden; }  /* frame, diagonal wipe = "cuts between two" */
.lico.cut::after { content: ""; position: absolute; inset: 0; background: currentColor;
  clip-path: polygon(0 100%, 100% 0, 100% 100%); opacity: .5; }
.seg-label { font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); margin: 8px 2px 2px; }
.pip-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; max-width: 160px; }
.pip-grid .pipc { height: 40px; }
.pip-grid .pipc.on { background: var(--accent); border-color: transparent; }
.lay { position: relative; }
.lay.suggested::after { content: "★"; position: absolute; top: 2px; right: 4px;
  font-size: 10px; color: var(--accent2); }

/* First-run quick tour */
.tutorial { position: fixed; inset: 0; z-index: 50; display: flex;
  align-items: center; justify-content: center; padding: 18px;
  padding-bottom: calc(18px + env(safe-area-inset-bottom));
  background: rgba(8, 10, 13, .72); backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px); animation: fade .2s ease; }
.tut-card { width: 100%; max-width: 420px; max-height: 86dvh; overflow-y: auto;
  background: linear-gradient(180deg, var(--panel), var(--bg2));
  border: 1px solid var(--line); border-radius: 18px; padding: 22px;
  box-shadow: var(--shadow); }
.tut-card h2 { margin: 0 0 4px; font-size: 22px; }
.tut-sub { color: var(--muted); margin: 0 0 14px; font-size: 14px; }
.tut-list { list-style: none; margin: 0 0 16px; padding: 0; display: flex;
  flex-direction: column; gap: 12px; }
.tut-list li { display: flex; gap: 12px; align-items: flex-start; font-size: 14px;
  color: var(--text); line-height: 1.4; }
.tut-list .i { font-size: 22px; line-height: 1; flex: 0 0 auto; }
.tut-list b { color: var(--accent); }
.tut-hint { color: var(--muted); font-size: 12px; text-align: center; margin: 12px 0 0; }

.panel { background: linear-gradient(180deg, var(--panel), var(--bg2));
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px; margin: 12px 0; box-shadow: var(--shadow); }
.panel summary { cursor: pointer; font-weight: 700; list-style: none; }
.panel summary::-webkit-details-marker { display: none; }
.panel summary::before { content: "▸"; color: var(--muted); display: inline-block;
  margin-right: 8px; transition: transform .15s; }
details[open] > summary::before { transform: rotate(90deg); }
.panel summary { margin: -14px; padding: 14px; }
details[open] summary { margin-bottom: 0; border-bottom: 1px solid var(--line);
  padding-bottom: 12px; }


.shot-list { list-style: none; margin: 8px 0 0; padding: 0;
  max-height: 220px; overflow: auto; }
.shot-list li { display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap;
  min-height: 44px; padding: 6px 4px; border-bottom: 1px solid var(--line); }
.shot-list .seek { color: var(--blue); cursor: pointer; flex: 1; padding: 8px 4px; }
.shot-list .del { color: var(--red); cursor: pointer; font-weight: 700;
  background: rgba(229,72,77,.12); border: 1px solid rgba(229,72,77,.45);
  border-radius: 8px; padding: 8px 16px; min-height: 36px;
  -webkit-tap-highlight-color: transparent; }
.shot-list .del:active { background: rgba(229,72,77,.25); }
/* Sound-type picker: re-typing a sound as a non-shot grays its row (no marker
   on the video) but keeps the type editable so labels can be refined. */
.shot-list .sound-type { background: var(--bg2); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px;
  min-height: 36px; margin: 0 8px; font-size: .92em; }
.shot-list li.tagged { opacity: .5; }
.shot-list li.tagged .seek { color: var(--muted); text-decoration: line-through; }
/* Mag change. Cyan is unused elsewhere (amber = PB/primary, green =
   improving/steel-hit, red = club accent), so it reads as "a different kind of
   event". Colour is never the only signal — the row also says RELOAD. */
.shot-row.reload { color: #22d3ee; }
.shot-row.reload .split { font-weight: 700; }
/* .75 base, not .45: hover-reveal never fires on touch, so the old base
   opacity made this control read as permanently disabled on phones. */
.gap-tap { display: block; width: 100%; text-align: center; opacity: .75;
           background: none; border: 1px dashed currentColor; color: inherit;
           border-radius: 6px; padding: 6px 0; font-size: .8rem; }
.gap-tap:hover, .gap-tap:focus-visible { opacity: 1; color: #22d3ee; }

/* Badge remove ✕ (equipment / target chips): a bare 11px glyph before —
   inflate the hit area with padding pulled back by negative margin so the
   chip's visual size doesn't grow. */
.badge .badge-rm { display: inline-flex; align-items: center; cursor: pointer;
  padding: 8px 8px; margin: -8px -6px -8px -2px; }
/* A hand-typed stage time (runs.time_source = 'manual') or a multi-string
   cumulative, marked next to the Total it describes — quiet text, not
   colour alone, because it qualifies a number rather than flagging a
   problem. (The reload/unloaded signals live in the lb-row stat lines now —
   see .lb-reload and the "Draw (unloaded)" label.) */
.manual-tag { color: var(--muted); font-size: 11px; }
/* Sound inspector: full-width signal line under a row — loudness bar vs the
   run's typical shot, detector buckets, verdict + fix-it hint. */
.sound-info { flex-basis: 100%; display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; padding: 2px 4px 6px; font-size: .85em; color: var(--muted); }
.sound-info b { color: var(--text); font-weight: 600; }
.snd-bar { flex: 0 0 90px; height: 8px; border-radius: 4px; overflow: hidden;
  background: var(--bg2); border: 1px solid var(--line); }
.snd-bar i { display: block; height: 100%;
  background: linear-gradient(90deg, var(--blue), #7cc4ff); }
/* --yellow was never defined, so this always painted its fallback — a
   fourth gold nobody chose. It is the accent role. */
.snd-hint { color: var(--accent2); }
/* Learned suspicion flag — advisory review hint, deliberately louder than
   the other signal chips but never destructive. */
.snd-sus { color: var(--accent); font-weight: 700; flex-basis: 100%; }
li.sus-shot { box-shadow: inset 3px 0 0 var(--accent); }

/* Undo toast — a pill pinned just above the bottom tab bar. */
.toast { position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(74px + env(safe-area-inset-bottom, 0px)); z-index: 50;
  /* flex-wrap is load-bearing: this is ONE row of [message, buttons], and a
     long message (the share prompt has to spell out what becomes public)
     otherwise squeezes the buttons off the pill's edge on a phone — visible,
     but with nothing left to tap. Wrapping drops them onto their own line
     instead. `justify-content: flex-end` keeps them together on that line. */
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  align-items: center; gap: 8px 16px; max-width: calc(100vw - 28px);
  background: #1c1c20; color: #fff; border: 1px solid var(--line);
  border-radius: 999px; padding: 10px 12px 10px 18px;
  box-shadow: 0 8px 28px rgba(0,0,0,.55); font-size: 14px; }
.toast .undo { background: none; border: none; color: var(--accent);
  font-weight: 800; font-size: 14px; padding: 6px 10px; min-height: 0;
  cursor: pointer; }

.progress { margin-top: 12px; }
.progress .bar { background: var(--panel2); border-radius: 999px; height: 10px;
  overflow: hidden; border: 1px solid var(--line); }
.progress .bar span { display: block; height: 100%; width: 0%;
  background: var(--accent); transition: width .2s; }
.progress p { color: var(--muted); font-size: 13px; margin: 6px 2px 0; }
.error { color: var(--red); margin-top: 12px; }

/* My Videos list */
.jobs-head { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 4px; }
.jobs-list { list-style: none; margin: 0; padding: 0; }
/* flex-wrap exists for ONE child: .oneclick-card below, which is basis:100%
   and therefore always starts its own row. .info (flex:1) and .acts
   (flex-shrink:0) still share the first line exactly as before — nothing
   that used to fit is pushed off it. */
.job { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 12px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  margin-bottom: 10px; }
.job .info { flex: 1; min-width: 0; }
.job .name { font-weight: 600; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.job .meta { color: var(--muted); font-size: 12px; margin-top: 2px; }
.job .acts { display: flex; gap: 8px; flex-shrink: 0; }
.job .acts .btn { min-height: 38px; padding: 8px 12px; }
/* ✨ One-click edit — the same control on both surfaces (editor tool area and
   the videos-view card). No new idiom: the destination picker is the existing
   .seg-group/.seg, the button is .btn.primary, the trust lines are .btn.ghost. */
.job .oneclick-card { flex: 1 1 100%; display: flex; flex-wrap: wrap;
  align-items: center; gap: 8px; }
.job .oneclick-card .seg-group { flex: 1 1 220px; margin: 0; }
.job .oneclick-card .btn { min-height: 38px; }
#oneclick-bar .seg-group { margin-bottom: 8px; }
/* The compact Music dropdown beside the destination picker — same row idiom
   on both surfaces; the select stretches on the editor bar, stays compact on
   a card. */
.oneclick-music-row { display: flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--muted); white-space: nowrap; }
.oneclick-music-row select { min-height: 38px; background: var(--panel);
  color: var(--text); border: 1px solid var(--line2); border-radius: 8px;
  padding: 6px 8px; }
#oneclick-bar .oneclick-music-row { margin-bottom: 8px; }
#oneclick-bar .oneclick-music-row select { flex: 1; }
.job .oneclick-card .oneclick-music-row { flex: 1 1 180px; }
.job .oneclick-card .oneclick-music-row select { flex: 1; min-width: 0; }
.trust-list { list-style: none; margin: 8px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px; }
.trust-list .trust-line { width: 100%; text-align: left; }
.trust-list .trust-line[disabled] { opacity: .7; cursor: default; }
/* Stale: the controls no longer say what these lines say. Greyed and struck
   through so the panel reads as history rather than as a promise — it stays
   tappable, because jumping to the control that drifted is exactly what a
   reader wants next. Not colour alone: the note below it says so in words. */
#oneclick-trust.stale .trust-list .trust-line { opacity: .5;
  text-decoration: line-through; }
#oneclick-trust.stale #oneclick-trust-stale { color: var(--accent2); }
.empty { color: var(--muted); padding: 24px; text-align: center; }
.jstat { font-size: 11px; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--panel2); }
.jstat.rendered { color: var(--green); border-color: var(--green); }

/* Squad promo recap */
#recap-league, #recap-title { width: 100%; margin-top: 4px; min-height: 44px;
  background: var(--panel2); color: var(--text); border: 1px solid var(--line2);
  border-radius: 10px; padding: 0 10px; box-sizing: border-box; }
#recap-view label.seg-label { display: block; }
#recap-clips .job { align-items: flex-start; }
#recap-clips .recap-name { display: block; width: 100%; min-height: 38px;
  background: var(--panel2); color: var(--text); border: 1px solid var(--line2);
  border-radius: 8px; padding: 6px 10px; font-weight: 600; box-sizing: border-box; }
#recap-clips .row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
#recap-clips [data-recapsec] { margin-top: 6px; min-height: 34px; padding: 6px 10px; }
#recap-close:disabled { opacity: .5; cursor: not-allowed; }
#recap-result { margin-top: 10px; }
.jstat.rendering { color: var(--accent); border-color: var(--accent); }
.jstat.failed { color: var(--red); border-color: var(--red); }
.jstat.open { color: var(--muted); border-color: var(--line2); }

/* Score card */
.muted { color: var(--muted); font-weight: 400; }
.scorecard { background: var(--panel2); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px; margin: 10px 0; }
.sc-head { font-size: 13px; color: var(--text); margin-bottom: 10px; }
.sc-scoring { float: right; font-size: 0.85em; }
.sc-cell.hit-t .sc-lab { color: var(--accent2); }   /* T-zone accent */
/* width:100% (not just the block default of auto) works around a layout
   quirk where this grid collapses its 1fr columns to ~0 in some nesting
   contexts (e.g. the standalone "Log a run" form) despite plenty of room. */
.sc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; width: 100%; }
.sc-cell { display: flex; flex-direction: column; align-items: stretch;
  border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
  background: var(--panel); }
.sc-lab { text-align: center; font-weight: 800; font-size: 13px; padding: 5px 0;
  color: #10120f; }
.sc-cell input { border: none; background: transparent; color: var(--text);
  text-align: center; font-size: 20px; font-weight: 700; padding: 10px 0;
  min-height: 44px; -moz-appearance: textfield; }
.sc-cell input:focus { outline: none; background: rgba(255,255,255,.04); }
.hit-a .sc-lab { background: var(--green); }
.hit-c .sc-lab { background: #c9d1da; }
.hit-d .sc-lab { background: var(--accent); }
/* Dark text like the other headers, not white: white on this red measured
   ~3.3:1 at 13px — under AA. */
.pen .sc-lab { background: var(--red); color: #10120f; }
/* Tap-to-count cells (manual run form): the whole cell is a +1 target; the
   readonly input is just the display, and a corner − decrements. */
.sc-cell.counter { position: relative; cursor: pointer; user-select: none;
  -webkit-tap-highlight-color: transparent; }
.sc-cell.counter input { pointer-events: none; font-size: 26px; padding: 12px 0; }
.sc-cell.counter:active { background: rgba(255,255,255,.06); }
/* Full-height right strip, not a 28px disc floating INSIDE the +1 target —
   a slightly-off tap on the disc incremented instead of decrementing. The
   strip gives − its own unambiguous column for the cell's whole height. */
.sc-dec { position: absolute; right: 0; top: 26px; bottom: 0; width: 34px;
  min-height: 0; border: none; border-left: 1px solid var(--line);
  border-radius: 0; background: rgba(0,0,0,.22);
  color: var(--text); font-size: 18px; line-height: 1; display: flex;
  align-items: center; justify-content: center; cursor: pointer; padding: 0;
  -webkit-tap-highlight-color: transparent; }
.sc-dec:active { background: var(--line2); }
.sc-hint { margin: 8px 0 0; text-align: center; color: var(--muted); font-size: 12px; }
.hf-preview { margin-top: 10px; text-align: center; color: var(--muted);
  font-size: 14px; }
.hf-preview b { color: var(--accent); font-size: 16px; }

/* Auth pages */
.auth-wrap { max-width: 420px; margin: 9vh auto; padding: 0 16px; }
.auth-card { background: linear-gradient(180deg, var(--panel), var(--bg2));
  border: 1px solid var(--line); border-radius: 18px; padding: 28px;
  box-shadow: var(--shadow); }
.auth-card h1 { font-size: 23px; margin: 0 0 4px; }
.auth-card .sub { color: var(--muted); margin: 0 0 18px; }
.auth-card label { display: block; font-size: 13px; color: var(--muted);
  margin: 12px 0 4px; }
.auth-card input { width: 100%; background: var(--panel2); color: var(--text);
  border: 1px solid var(--line2); border-radius: 10px; padding: 12px;
  font-size: 16px; min-height: 46px; }
.auth-card .btn { width: 100%; margin-top: 18px; }
.auth-links { display: flex; justify-content: space-between; margin-top: 14px;
  font-size: 14px; gap: 10px; }
.auth-links a, .linkish { color: var(--blue); cursor: pointer; text-decoration: none; }
.auth-links a:hover, .linkish:hover { text-decoration: underline; }
.auth-msg { margin-top: 12px; font-size: 14px; }
.auth-msg.ok { color: var(--green); }
.auth-msg.err { color: var(--red); }

/* Back-link / account area (admin.html, stats.html) */
.acct { display: flex; align-items: center; gap: 4px; font-size: 13px;
  margin-left: auto; }
.acct a, .acct button { color: var(--muted); background: none; border: none;
  cursor: pointer; font-size: 13px; padding: 6px 8px; border-radius: 8px;
  transition: color .15s, background .15s; }
.acct a:hover, .acct button:hover { color: var(--text); background: var(--panel2); }

/* Admin table */
.user-table { width: 100%; border-collapse: collapse; margin-top: 12px; }
.user-table th, .user-table td { text-align: left; padding: 10px 8px;
  border-bottom: 1px solid var(--line); font-size: 14px; }
.user-table th { color: var(--muted); font-weight: 600; }
.badge { font-size: 11px; padding: 2px 8px; border-radius: 999px;
  background: var(--panel2); border: 1px solid var(--line); }
.badge.admin { color: var(--accent); border-color: var(--accent); }
.badge.invited { color: var(--blue); }

/* Composite preview modal (two angles, beep-aligned) */
.cpv { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center;
  justify-content: center; padding: 12px;
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
  background: rgba(8,10,13,.86); }
.cpv-box { width: 100%; max-width: 640px; display: flex; flex-direction: column; gap: 10px; }
.cpv-top { display: flex; align-items: center; justify-content: space-between; color: var(--text); }
.cpv-title { font-weight: 700; }
.cpv-x { background: none; border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; width: 36px; height: 36px; cursor: pointer; }
.cpv-stage { position: relative; width: 100%; max-height: 64dvh; background: #000;
  border-radius: 12px; overflow: hidden; display: flex; }
.cpv-stage video { background: #000; display: block; }
.cpv-stage #cpv-canvas { position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; }
.cpv-stage.lay-horizontal { flex-direction: row; }
.cpv-stage.lay-horizontal video { width: 50%; height: auto; object-fit: contain; }
.cpv-stage.lay-vertical { flex-direction: column; }
.cpv-stage.lay-vertical video { width: 100%; height: 50%; object-fit: contain; }
.cpv-stage.lay-square { aspect-ratio: 1 / 1; }
.cpv-stage.lay-square.base-v { flex-direction: column; }
.cpv-stage.lay-square video { flex: 1; min-height: 0; min-width: 0; object-fit: contain; }
.cpv-stage.lay-pip #cpv-main { width: 100%; height: 100%; object-fit: contain; }
.cpv-stage.lay-pip #cpv-sec { position: absolute; width: 30%; object-fit: contain;
  border: 1px solid rgba(255,255,255,.5); border-radius: 6px; }
.cpv-stage.lay-pip.pipsz-s #cpv-sec { width: 22%; }
.cpv-stage.lay-pip.pipsz-l #cpv-sec { width: 38%; }
.cpv-stage.lay-pip.pip-tl #cpv-sec { top: 4%; left: 4%; }
.cpv-stage.lay-pip.pip-tr #cpv-sec { top: 4%; right: 4%; }
.cpv-stage.lay-pip.pip-bl #cpv-sec { bottom: 4%; left: 4%; }
.cpv-stage.lay-pip.pip-br #cpv-sec { bottom: 4%; right: 4%; }
.cpv-ctl { display: flex; align-items: center; gap: 10px; }
.cpv-ctl #cpv-seek { flex: 1; }
.cpv-time { color: var(--muted); font-size: 13px; min-width: 42px; text-align: right; }
.cpv-cap { color: var(--muted); font-size: 12px; text-align: center; margin: 0; }

.jobs-head-acts { display: flex; gap: 8px; }
.run-form { padding: 4px 2px calc(24px + var(--tabbar-clearance) + env(safe-area-inset-bottom)); }
.run-form label { display: block; margin: 10px 0 0; color: var(--muted); font-size: 13px; }
.run-form input, .run-form select { width: 100%; margin-top: 4px; min-height: 44px;
  background: var(--panel2); color: var(--text); border: 1px solid var(--line2);
  border-radius: 10px; padding: 10px; box-sizing: border-box; }
.run-form .row { display: flex; gap: 10px; }
.run-form .row label { flex: 1; }
.attach-banner { background: var(--panel2); border: 1px solid var(--accent);
  color: var(--text); border-radius: 10px; padding: 10px 12px; margin: 0 0 12px;
  font-weight: 600; text-align: center; }

.attach-summary { margin: 8px 0 4px; padding: 12px; background: var(--panel2);
  border: 1px solid var(--accent); border-radius: 12px; }
.attach-sum-text { color: var(--text); font-size: 14px; line-height: 1.5;
  margin-bottom: 10px; }
.attach-sum-text .muted { color: var(--muted); }

.attach-time { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 10px; }
.seg-toggle { display: inline-flex; background: var(--panel);
  border: 1px solid var(--line2); border-radius: 10px; overflow: hidden; }
.seg-toggle .seg { background: none; border: none; color: var(--muted);
  padding: 8px 12px; font-weight: 700; min-height: 40px; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.seg-toggle .seg.on { background: var(--accent); color: #1a1606; }

.lg-pick { position: fixed; inset: 0; background: rgba(0,0,0,.6);
  display: flex; align-items: flex-end; justify-content: center; z-index: 50; }
.lg-pick.hidden { display: none; }
.lg-pick-box { background: var(--panel); border: 1px solid var(--line);
  border-radius: 14px 14px 0 0; width: 100%; max-width: 520px; max-height: 85vh;
  overflow-y: auto; padding: 14px; }
.lg-pick-head { display: flex; justify-content: space-between; align-items: center;
  font-weight: 700; margin-bottom: 8px; }
.lg-pick label { display: block; color: var(--muted); font-size: 13px; margin: 8px 0 0; }
.lg-pick select { width: 100%; min-height: 44px; margin-top: 4px; background: var(--panel2);
  color: var(--text); border: 1px solid var(--line2); border-radius: 10px; padding: 10px; }
.lg-pick-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.lg-pick-list button { text-align: left; background: var(--panel2); color: var(--text);
  border: 1px solid var(--line); border-radius: 10px; padding: 10px; min-height: 44px; cursor: pointer; }

/* Multi-string marking bar */
.strings-bar { display:flex; gap:8px; flex-wrap:wrap; margin:8px 0; }
.strings-bar .chip { min-height:40px; padding:6px 12px; border-radius:10px;
  background:var(--panel2); color:var(--text); border:1px solid var(--line);
  cursor:pointer; font-size:13px; font-weight:650; line-height:1.2;
  -webkit-tap-highlight-color:transparent; }
.strings-bar .chip.active { border-color:var(--accent); color:var(--accent); }
.strings-bar .chip-sub { display:block; font-size:11px; color:var(--muted); }
.strings-bar .chip.active .chip-sub { color:var(--accent); opacity:.7; }
.strings-bar .chip.add { border-style:dashed; }
/* Dimmed markers for inactive strings on the waveform */
.marker.dim { opacity:0.35; pointer-events:none; }

/* ══ Identity layer ═══════════════════════════════════════════════════════
   Applied last so it wins ties with the base component styles above. */

/* Equipment voice: headings, buttons, tabs and labels in Chakra Petch. */
h1, h2, h3, .brand, .ed-title, .dz-title,
.btn, .btn-primary, .btn-ghost, .qm-btn, .seg, .lay, .tab,
.appnav__item, .seg-label, .sc-lab, .badge--pb, .pill--hf, .label {
  font-family: var(--display);
  letter-spacing: .01em;
}
.seg-label, .label { text-transform: uppercase; letter-spacing: .09em;
  font-size: 11px; color: var(--muted); }

/* The LCD: hero time values render as true seven-segment digits inside the
   timer's window — unlit segments ghosted behind ("888.88" residue). */
.lcd {
  font-family: var(--lcd-font);
  font-weight: 700;
  color: var(--lcd-ink);
  background: var(--lcd-bg);
  border: 1px solid #232b21;
  border-radius: 10px;
  box-shadow: inset 0 2px 10px rgba(0,0,0,.55);
  padding: 10px 14px 8px;
  display: inline-block;
  line-height: 1.05;
  text-shadow: 0 0 8px rgba(203,230,171,.28);
  position: relative;
}
.lcd span { /* the unit ("s") steps off the LCD glyphs */
  font-family: var(--display);
  font-weight: 600;
  color: var(--lcd-ink);
  opacity: .55;
}
.home-time.lcd { font-size: 34px; margin: 8px 0 4px; }
.home-time.lcd span { font-size: 14px; }
.result-time.lcd { font-size: 38px; margin: 6px 0; }
.result-time.lcd span { font-size: 15px; }
.pbval.lcd { font-size: 17px; padding: 7px 9px 5px; margin-top: 4px; }
.pbval.lcd span { font-size: 11px; }

/* Numbers elsewhere: tabular, equipment-flavored. */
#stats b, .rank-no, .recent-row b, .board td {
  font-family: var(--display); font-variant-numeric: tabular-nums;
}

/* ── Feels-like-an-app pass ──────────────────────────────────────────────── */
html { overscroll-behavior: none; }
html, body { -webkit-tap-highlight-color: transparent; }
.topbar, .tabbar, .appnav, .ed-header, button, .seg, .lay, .tab, .qm-btn {
  -webkit-user-select: none; user-select: none;
}
button, .btn, .btn-primary, .btn-ghost, a.linkish, .seg, .lay, .tab, .qm-btn {
  touch-action: manipulation;
}
/* press feedback — the whole UI answers your finger */
.btn:active, .btn-primary:active, .btn-ghost:active, .qm-btn:active,
.seg:active, .lay:active, .tab:active, .appnav__item:active,
.stat-tile:active, .jobs-list li:active {
  transform: scale(.97);
  transition: transform .06s ease;
}
/* chrome (no backdrop blur — sticky/fixed bars re-composite on every scroll
   frame and blur janks low-end phones; see .appnav) */
.topbar {
  background: rgba(15,18,22,.95);
  border-bottom: 1px solid var(--line);
}
.brand { white-space: nowrap; }
.topbar { flex-wrap: nowrap; }
.expiry { white-space: nowrap; }
.tabbar {
  background: rgba(15,18,22,.96);
  border-top: 1px solid var(--line);
  padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
}
.tabbar .tab { position: relative; }
/* Journey-step badge: the number in the corner turns into a green ✓ when
   that step is done (marks confirmed / run saved / video rendered).
   data-step sits on the icon span (see the tabbar comment in index.html);
   the badge positions against the .tab, which is position:relative. */
.tabbar .tab .i[data-step]::after { content: attr(data-step);
  position: absolute; top: 3px; right: 16%;
  width: 14px; height: 14px; line-height: 13px; text-align: center;
  font-size: 9px; font-weight: 800; color: var(--muted);
  border: 1px solid var(--line); border-radius: 50%; box-sizing: border-box; }
.tabbar .tab.done .i[data-step]::after { content: "✓";
  color: #10120f; background: var(--green); border-color: transparent; }
.tabbar .tab.on::after {
  content: ""; position: absolute; top: 0; left: 22%; right: 22%; height: 3px;
  border-radius: 0 0 3px 3px; background: var(--accent-grad);
}
/* the Add button = the timer's round amber side button */
.appnav__add .appnav__ic {
  border-radius: 50%;
  box-shadow: 0 6px 16px rgba(245,185,33,.42),
              inset 0 -2px 4px rgba(0,0,0,.28),
              inset 0 2px 3px rgba(255,255,255,.45);
}
/* tab switches glide instead of popping */
@keyframes panel-in { from { opacity: 0; transform: translateY(5px); }
                      to { opacity: 1; transform: none; } }
.tab-panel.active { animation: panel-in .18s ease both; }
.view:not(.hidden) { animation: panel-in .18s ease both; }
@media (prefers-reduced-motion: reduce) {
  .tab-panel.active, .view:not(.hidden) { animation: none; }
  .btn:active, .btn-primary:active, .btn-ghost:active { transform: none; }
}
/* keyboard users get a clear amber ring */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* hide scrollbars on touch layouts; content still scrolls */
@media (max-width: 719px) {
  ::-webkit-scrollbar { width: 0; height: 0; }
  * { scrollbar-width: none; }
}

/* ── audio-alignment lane (Layout tab) ───────────────────────────────────── */
.align-lane { position: relative; height: 76px; margin: 8px 0 4px;
  background: var(--bg2); border: 1px solid var(--line); border-radius: 10px;
  overflow: hidden; }
.align-lane img { position: absolute; height: 36px; object-fit: fill;
  image-rendering: auto; }
#al-main { top: 1px; }
#al-sec { bottom: 1px; }
.al-shade { position: absolute; top: 0; bottom: 0; width: 0;
  background: rgba(5,7,9,.72); pointer-events: none; }
/* ── post-upload equipment sheet + target-type picker (same bottom-sheet chrome) ── */
/* z-index 70, ABOVE the toast's 60: they used to tie at 60 and the toast
   ("How did detection do?" 👍👎) painted over the sheet's Done/Skip row on
   phones, swallowing the taps that dismiss the dialog — with the dialog
   stuck, everything behind it read as broken. dvh, not vh: Safari's vh is
   the LARGE viewport, so 70vh overshot the visible area under the address
   bar and pushed the buttons off-screen. */
#equip-sheet, #target-type-sheet { position: fixed; inset: 0; background: rgba(0,0,0,.55);
  z-index: 70; display: flex; align-items: flex-end; justify-content: center; }
#equip-sheet.hidden, #target-type-sheet.hidden { display: none; }
#equip-sheet .sheet-card, #target-type-sheet .sheet-card { width: min(560px, calc(100% - 16px));
  margin: 0 8px 8px; max-height: 70dvh; overflow-y: auto; }
/* Done/Skip pinned to the card's bottom edge while the content scrolls —
   scrollbars are hidden under 720px, so buttons below the fold gave no sign
   they existed at all. */
#equip-sheet .sheet-actions { position: sticky; bottom: 0;
  background: var(--panel); padding: 8px 0 2px; margin-bottom: 0; }
/* This sheet's fields never picked up the app's usual form styling (they sit
   outside .tool-area/.run-form/.lg-pick) — labels and raw browser-default
   inputs overlapped instead of stacking. Match the rest of the app. */
#equip-sheet .row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
#equip-sheet .row label { flex: 1 1 140px; display: flex; flex-direction: column;
  gap: 4px; font-size: 13px; color: var(--muted); }
#equip-sheet input[type="text"] { background: var(--panel2); color: var(--text);
  border: 1px solid var(--line2); border-radius: 10px; padding: 10px;
  font-size: 16px; /* under 16px iOS focus-zooms the sheet */ min-height: 44px; }
#equip-sheet input[type="text"]:focus { outline: none; border-color: var(--blue);
  box-shadow: 0 0 0 3px rgba(90,169,236,.18); }
/* Saved-gun suggestion buttons: show which one(s) are already added. */
#eqs-guns { margin-bottom: 4px; }
#eqs-guns .btn[data-eqgun].on { border-color: var(--accent); color: var(--accent);
  background: rgba(232,200,74,.12); }
#eqs-guns .btn[data-eqgun].on::before { content: "✓ "; }
/* "What you've selected" — kept visible even when empty so it reads as a
   real section, not a gap that might just not have loaded yet. */
.eqs-selected-label { font-size: 11px; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); margin: 10px 0 4px; }
#eqs-chips:empty::after { content: "Nothing added yet — tap a saved gun above or type one below.";
  color: var(--muted); font-size: 13px; }

/* brand mark (topbar) */
.brand-pin { height: 20px; width: 20px; vertical-align: -4px; margin-right: 2px; }

/* A member's home-club logo, swapped in for the pin+"Shooting Local" mark
   (or a page's own title text) once /api/me reports one — see
   applyClubBrand() in topmenu.js / app.js. */

/* Indeterminate progress: slide a short segment while no percent is known. */
.progress .bar span.indet {
  width: 30% !important;
  animation: indet-slide 1.2s ease-in-out infinite alternate;
}
@keyframes indet-slide {
  from { margin-left: 0; }
  to   { margin-left: 70%; }
}

/* Share nudge on the render result card + home following block */
/* The share ask on the render result: one button, one owner-only choice. */
.share-ask { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
.share-ask .check { display: flex; align-items: center; gap: 8px;
  color: var(--muted); font-size: 13px; min-height: 32px; }
.share-ask .check input { width: 18px; height: 18px; accent-color: var(--accent); }
.share-nudge { margin-top: 12px; padding: 10px 12px; background: var(--panel2);
  border: 1px solid var(--line); border-radius: 10px; }
.share-nudge .label { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
.copy-row { display: flex; align-items: center; gap: 8px; }
.copy-row code { flex: 1; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: 12px; color: var(--accent2); }
.follow-block { padding: 8px 0; border-bottom: 1px solid var(--line); }
.follow-block:last-child { border-bottom: 0; }

/* Player buffering overlay ("thinking" while the video loads/seeks) */
.player-loading { position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center;
  background: rgba(15,18,22,.35); pointer-events: none; z-index: 5; }
.player-loading i { width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid var(--line2); border-top-color: var(--accent);
  animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Beep confirmation bar + playhead readout chip */
.beep-confirm { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--panel2); border: 1px solid var(--line);
  border-radius: 10px; padding: 8px 12px; margin: 6px 0; font-size: 14px; }
.beep-confirm b { color: var(--accent2); font-variant-numeric: tabular-nums; }
/* First run ever: one pulse to say "start here". Motion-sensitive users get
   the scroll but not the flash. */
.beep-confirm.first-run-pulse { animation: first-run-pulse 1.3s ease-in-out 2; }
@keyframes first-run-pulse {
  50% { border-color: var(--accent); box-shadow: 0 0 0 3px
        color-mix(in srgb, var(--accent) 30%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .beep-confirm.first-run-pulse { animation: none; }
}
.playhead-chip { position: absolute; top: 6px; left: 8px; z-index: 6;
  background: rgba(15,18,22,.75); border-radius: 6px; padding: 2px 8px;
  font-size: 12px; color: var(--accent2);
  font-variant-numeric: tabular-nums; pointer-events: none; }

/* Checkbox rows (my-shots-only, Fit toggle) */
.check-row { display: flex; align-items: center; gap: 8px; margin: 10px 2px;
  font-size: 14px; color: var(--text); cursor: pointer; }
.check-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); }

/* ── Club cards (colorful) — shared by the public landing + the home
   "Clubs" strip. Each club gets a distinct hue avatar (hashed from its
   name) so a directory of clubs reads at a glance. ── */
.club-card { display: flex; align-items: center; gap: 12px;
  background: linear-gradient(135deg, var(--panel), var(--panel2));
  border: 1px solid var(--line); border-left: 4px solid var(--club-hue, var(--accent));
  border-radius: 14px; padding: 12px 14px; margin-bottom: 10px;
  box-shadow: var(--shadow); text-decoration: none; color: var(--text);
  transition: transform .08s ease, border-color .12s ease; }
.club-card:hover { transform: translateY(-1px); border-color: var(--line2); }
.club-card:active { transform: translateY(0); opacity: .85; }
.club-av { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 12px;
  display: grid; place-items: center; font-weight: 800; font-size: 16px;
  color: #0d0f13; letter-spacing: .3px;
  background: linear-gradient(135deg, var(--club-hue, var(--accent)),
              var(--club-hue2, var(--accent2))); }
.club-card .club-body { flex: 1 1 auto; min-width: 0; }
.club-card .club-name { font-weight: 700; font-size: 15px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.club-card .club-meta { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
.club-card .club-go { flex: 0 0 auto; color: var(--club-hue, var(--accent));
  font-size: 1.3em; font-weight: 700; }
.club-chip { display: inline-block; font-size: 11px; font-weight: 700;
  padding: 1px 7px; border-radius: 999px; margin-left: 6px;
  background: rgba(255,255,255,.06);
  color: var(--club-hue, var(--accent)); vertical-align: middle; }

/* "Report a problem" — one shared affordance and its modal. */
.report-open { background: none; border: 0; color: var(--muted);
  font: inherit; font-size: 12px; cursor: pointer; padding: 4px 8px; }
.report-open:hover { color: var(--text); }
.report-modal { position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center; padding: 16px;
  background: rgba(0, 0, 0, .55); }
.report-modal.hidden { display: none; }
.report-card { background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px; width: 100%; max-width: 420px;
  box-shadow: var(--shadow); }
.report-card h3 { margin: 0 0 6px; font-size: 16px; }
.report-card textarea { width: 100%; background: var(--panel2);
  color: var(--text); border: 1px solid var(--line); border-radius: 10px;
  padding: 10px; font: inherit; resize: vertical; }
.report-hint { color: var(--muted); font-size: 12px; margin: 6px 0; }
.report-actions { display: flex; gap: 8px; justify-content: flex-end;
  margin-top: 10px; }

/* First-run funnel: non-dismissable verification banner, injected once by
   nav.js's shared chrome (index.html/stats.html/leagues.html). Sits right
   below header.topbar, sized to match <main>'s own content column. */
.verify-banner { width: calc(100% - 32px); max-width: 780px; box-sizing: border-box;
  margin: 10px auto 0; background: var(--panel2); border: 1px solid var(--accent);
  color: var(--text); border-radius: 10px; padding: 12px 14px; font-size: 13.5px;
  line-height: 1.5; }
.verify-banner__text { margin: 0 0 8px; }
.verify-banner__malformed { color: var(--red); font-weight: 600; margin: 0 0 8px; }
.verify-banner__actions { display: flex; gap: 8px; flex-wrap: wrap; }
.verify-banner__status { margin: 8px 0 0; font-size: 12.5px; min-height: 1em; }
.verify-banner__status.ok { color: var(--green); }
.verify-banner__status.err { color: var(--red); }
.verify-banner__correct { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.verify-banner__correct label { display: block; color: var(--muted); font-size: 12px;
  margin-bottom: 4px; }
.verify-banner__correct input { min-height: 40px; background: var(--panel);
  color: var(--text); border: 1px solid var(--line2); border-radius: 8px;
  padding: 8px 10px; box-sizing: border-box; }

/* Shared blocking-error rendering (nav.js renderBlockingError): an object
   `detail` {code, message, action?} from web/app.py's _blocked() gets its
   message plus, when action is present, a button wired by action.kind. A
   plain-string detail renders with no button and no layout change. */
.blocking-error { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.blocking-error-btn { align-self: flex-start;
  /* A form that renders this near the bottom of the page (e.g. the "log a
     run without a video" sheet) can land it right where the fixed .appnav
     bar sits — this tells scrollIntoView("nearest") (nav.js) how much
     clearance to leave so the button doesn't end up rendered but unreachable
     behind the bar. */
  scroll-margin-bottom: calc(var(--tabbar-clearance) + env(safe-area-inset-bottom)); }
.blocking-error-status { font-size: 12px; }
.blocking-error-status.ok { color: var(--green); }
.blocking-error-status.err { color: var(--red); }
