/* Court Replay — design system
 *
 * Adapted from the claude.ai/design "Court Replay" handoff bundle.
 * Light theme is :root default; [data-theme="dark"] flips to the dark
 * palette. The light palette is Court Green (prototypes/palette, frrx.3): the
 * accent is the brand and its call to act, and nothing else; teams, shot types
 * and status each take hues of their own, so a colour on screen means one thing.
 */

:root {
  /* Light palette (default) */
  --bg: #f6f7f5;
  --bg-2: #eef0ec;
  --surface: #ffffff;
  --surface-2: #f3f5f1;
  --surface-3: #e8ece6;
  --border: #e3e7df;
  --border-2: #d4dacf;
  --text: #0e1311;
  --text-2: #2a302c;
  --muted: #5d6862;
  --muted-2: #8a948e;

  /* Accent — Court Green: primary action, active/selected, focus, the wordmark
     dot, and the "ready" tag, told apart from the action by form (a tinted tag,
     never a filled one). Never a label or a team. */
  --accent: #2b6a48;
  --accent-ink: #ffffff;
  --accent-soft: rgba(43, 106, 72, 0.10);
  --accent-line: rgba(43, 106, 72, 0.30);

  /* Status, always beside a glyph and a word (DESIGN.md, Status). Status Blue
     is a match in flight — uploading, pending, the upload bar — and never a
     data mark; warn and danger collapse into each other under protan/deutan
     vision. Ready takes the accent wash (.status-tag.st-ready). */
  --info: #0b6a8a;
  --info-soft: rgba(11, 106, 138, 0.10);
  --info-line: rgba(11, 106, 138, 0.28);
  --warn: #94600a;
  --danger: #b3261e;

  /* Team colours — the two sides of a match (player analytics, gvtk.2). Blue
     and orange separate under every common colour-vision deficiency. Team B is
     3.9:1 on white: a mark, never text (see .fg.w1 for its text ink). */
  --team-a: #1157a4;
  --team-b: #db551d;

  /* Match-level charts with no team or shot meaning (the shots-per-rally
     histogram). */
  --data-ink: #46524b;

  /* Player DNA stroke-mix palette (sbx-619). Categorical, deliberately NOT the
     team hues: stroke type is a property of the shot, not the side, so it needs
     its own ramp that stays distinct on either team card. */
  --st-ground: #1d97a3;
  --st-volley: #9c76f3;
  --st-overhead: #7f3a72;
  --st-lob: #7e9a4c;

  /* Status glyphs, drawn in currentColor through a mask: the "ready" check
     (.dot.live and .status-tag.st-ready), then the rest of the status tag's
     (prototypes/palette/status-blue.html). */
  --icon-up: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 10V2.5M2.8 5.5 6 2.3l3.2 3.2'/%3E%3C/svg%3E");
  --icon-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round'%3E%3Ccircle cx='6' cy='6' r='4.6'/%3E%3Cpath d='M6 3.4V6l1.8 1.1'/%3E%3C/svg%3E");
  --icon-arc: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round'%3E%3Cpath d='M10.4 6A4.4 4.4 0 1 1 6 1.6'/%3E%3C/svg%3E");
  --icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 2.2 10.6 10H1.4L6 2.2ZM6 5.4v2.2M6 8.7v.1'/%3E%3C/svg%3E");
  --icon-pause: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M4.2 3v6M7.8 3v6'/%3E%3C/svg%3E");
  --icon-x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='m3 3 6 6M9 3l-6 6'/%3E%3C/svg%3E");
  --icon-arrow-right: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 6h7M6.5 3l3 3-3 3'/%3E%3C/svg%3E");
  --icon-retry: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13 8a5 5 0 1 1-1.5-3.5M13 3v3h-3'/%3E%3C/svg%3E");
  --icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.2'/%3E%3Cpath d='M8 7.2v4M8 4.8v.2'/%3E%3C/svg%3E");
  --icon-decline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6'%3E%3Ccircle cx='8' cy='8' r='6'/%3E%3Cpath d='M4 12l8-8'/%3E%3C/svg%3E");
  --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.2 4.2L19 7'/%3E%3C/svg%3E");

  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;
  --radius-xl: 20px;

  --shadow-1: 0 1px 0 rgba(255, 255, 255, 0.6) inset, 0 1px 2px rgba(20, 30, 20, 0.05);
  --shadow-2: 0 1px 0 rgba(255, 255, 255, 0.8) inset, 0 10px 30px rgba(20, 30, 20, 0.08);

  --font-sans: 'Geist', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-serif: 'Instrument Serif', ui-serif, Georgia, serif;

  /* ── Type scale (ADR-054) ────────────────────────────────────────────────
     Seven steps, each a step a reader can SEE. Counting every rule whose
     selector the upload screen's markup reaches, plus the sizes named in the
     template itself, the screen used to draw TWENTY distinct px: 10, 10.5,
     11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15, 16, 18, 19, 20, 22, 24, 28,
     36, 40. Eleven of those — everything from 10 to 15 — form an unbroken
     ladder in half-pixel steps, which is not a set of decisions but the
     residue of many. Nine were named inline in index.html, across seventeen
     `style="font-size:"` attributes, i.e. outside this file's reach, which is
     how they drifted apart in the first place.

     The roles, and they are the whole of it:
       --fs-eta     the countdown, mono
       --fs-title   the one heading an entry panel carries
       --fs-head    the card title, and a stage title
       --fs-body    prose the card speaks
       --fs-sub     a muted qualifier that is not a sentence
       --fs-meta    measured/mono lines — chip, sub line, figures line, hints
       --fs-label   eyebrows and uppercase labels, mono

     The seven own the card's own prose; they do not swallow all twenty. What
     is deliberately NOT on this scale, and why, is ADR-054's second half: the
     button primitive (`.btn`/`.btn-lg`/`.btn-sm`), which every page draws and
     which is sized against padding and hit target rather than against prose;
     `.pt-input`/`.up-sc` at 16px, which is an iOS Safari focus-zoom threshold
     and not a type choice (a fine-pointer override takes the pair to the body
     step where no focus zoom exists — see the rule beside .pt-input); the
     shared text input; the page's own `.h1` above the card; and the
     countdown's two derived ratios. All of them
     are pinned by name and value in tests/off-scale.js's OFF_SCALE, and the
     sweep in upload-layout.test.js fails on a card rule naming a px the table does not — so
     the list cannot grow without someone writing a line in it.

     NOTE the boundary that sweep has: it reaches a rule through the classes
     the card's markup AND its scripts apply, so a rule keyed on a class no
     card node ever carries is outside it. */
  --fs-eta: 40px;
  --fs-title: 19px;
  --fs-head: 15px;
  --fs-body: 13.5px;
  --fs-sub: 12.5px;
  --fs-meta: 11px;
  --fs-label: 10px;
}

/* Unshipped: no page sets data-theme="dark". It still carries the palette from
   before Court Green (prototypes/palette, frrx.3 scoped it out), so it needs its
   own pass before any page turns it on. */
[data-theme="dark"] {
  --bg: #0b0e0c;
  --bg-2: #0e1311;
  --surface: #131815;
  --surface-2: #181e1a;
  --surface-3: #1e2520;
  --border: #232a25;
  --border-2: #2c342e;
  --text: #eef2ef;
  --text-2: #c7cec9;
  --muted: #8a948e;
  --muted-2: #5f6a64;

  --accent: #9dd4b0;
  --accent-ink: #091410;
  --accent-soft: rgba(157, 212, 176, 0.10);
  --accent-line: rgba(157, 212, 176, 0.32);

  --warn: #f6c26b;
  --danger: #ef6b6b;
  --info: #7cb4ff;

  --team-a: #9dd4b0;
  --team-b: #7cb4ff;

  --st-ground: #5fb3a7;
  --st-volley: #8f95ef;
  --st-overhead: #e0a54e;
  --st-lob: #df7fa2;

  --shadow-1: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-2: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 10px 30px rgba(0, 0, 0, 0.35);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-feature-settings: "ss01", "cv11";
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body { min-height: 100vh; overflow-x: hidden; }

a { color: inherit; text-decoration: none; }
button { font-family: inherit; }
input, textarea, select { font-family: inherit; }

::selection { background: var(--accent); color: var(--accent-ink); }

/* ───────── App shell ───────── */
.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

.topnav {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 28px;
  background: color-mix(in oklab, var(--bg) 82%, transparent);
  backdrop-filter: blur(20px) saturate(140%);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  border-bottom: 1px solid var(--border);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 500;
  letter-spacing: -0.01em;
  font-size: 15px;
  color: var(--text);
}
/* The CR play-mark SVG (viewBox 700x385, ~1.82:1) — height sets the size,
   width follows the aspect ratio. */
.brand-mark {
  height: 26px;
  width: auto;
  display: inline-block;
}
.brand-dot { color: var(--accent); }

.nav-screens {
  display: flex;
  align-items: center;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px;
}
.nav-screens a, .nav-screens button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: 12.5px;
  font-weight: 500;
  /* A length, not `normal`: the line's height was the font's, so the bar was 64px
     on the fallback face and 66px once Geist landed, and everything sized from the
     bar (the match page's first-paint column, --mp2-col-w) moved with it (lsrn.48).
     The pill is 35px, the tallest thing on the bar's row, so this sets the bar's
     height: 14px padding twice, the pill, the border. */
  line-height: 15px;
  padding: 6px 12px;
  border-radius: 999px;
  cursor: pointer;
  letter-spacing: -0.005em;
  transition: color 120ms ease, background 120ms ease;
  text-decoration: none;
}
.nav-screens a:hover, .nav-screens button:hover { color: var(--text-2); }
.nav-screens a.active, .nav-screens button.active {
  background: var(--surface-3);
  color: var(--text);
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 10px;
}
/* One height for the controls in a bar's right group (frrx.18,
   prototypes/language-picker/; owner, 2026-10-21): the language picker, the
   balance chip and Sign out stand at 32px on a desktop; at 720px and under the
   picker, the chip and the 36px menu button stand at 36px. */
.topnav, .sr-topbar { --nav-control: 32px; }
.nav-logout-desktop { height: var(--nav-control); padding: 0 12px; border-radius: 8px; }
.sr-topbar .btn-sm { height: var(--nav-control); padding: 0 12px; border-radius: 8px; }

.nav-username {
  font-size: 12px;
  color: var(--muted);
}

/* Help and feedback (y700, prototypes/top-bar-feedback/): the icon-only opener of
   the support panel, after the language picker. Its unread badge is
   #support-unread (css/support.css). */
.nav-help {
  position: relative; appearance: none; flex: none;
  width: var(--nav-control); height: var(--nav-control); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid transparent; border-radius: 8px;
  color: var(--text-2); cursor: pointer; transition: background 120ms ease, color 120ms ease;
}
.nav-help:hover { background: var(--surface-2); color: var(--text); }
.nav-help[aria-expanded="true"] { background: var(--surface-3); color: var(--text); }
.nav-help:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nav-help svg { width: 18px; height: 18px; }

/* The credits balance (2vhw.16, prototypes/player-credits/): one chip in the nav,
   the status tag with a clock (.status-tag.st-balance, frrx.16), and the
   breakdown by source in a popover under it.
   The container is `hidden` until js/credits-nav.js knows the player is on credits. */
.nav-credits { position: relative; }
.nav-credits[hidden], .nav-credits [hidden] { display: none; }
/* In the bar the tag is a control: the bar's height, 8px control corners, a hover
   and the accent focus ring. Its tint, line, clock and type stay the tag's. */
.nav-credits .nav-credits-chip { cursor: pointer; height: var(--nav-control); padding: 0 10px; gap: 6px; border-radius: 8px; transition: background 120ms ease; }
.nav-credits .nav-credits-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (hover: hover) { .nav-credits .nav-credits-chip:hover { background: color-mix(in oklab, var(--st-wash), var(--st-ink) 7%); } }
.nav-credits-chip .nav-credits-chev { width: 10px; height: 10px; flex: none; opacity: 0.6; }
.nav-credits-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  width: 290px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(20, 30, 20, 0.05), 0 14px 36px rgba(20, 30, 20, 0.12);
  z-index: 60;
}
.nav-credits-pop h5 {
  margin: 0 0 2px;
  font: 500 var(--fs-meta) var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.nav-credits-total { font-size: 24px; letter-spacing: -0.02em; color: var(--text); font-variant-numeric: tabular-nums; }
.nav-credits-pop ul { list-style: none; margin: 10px 0 0; padding: 0; }
.nav-credits-pop li {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 0;
  border-top: 1px dashed var(--border-2);
  font-size: var(--fs-sub);
  color: var(--text-2);
}
.nav-credits-src { display: inline-flex; align-items: center; gap: 8px; }
.nav-credits-key { width: 8px; height: 8px; border-radius: 2px; background: var(--muted-2); flex: none; }
.nav-credits-key-free { background: var(--accent); }
.nav-credits-key-old { background: var(--muted); }
.nav-credits-key-new { background: var(--muted-2); }
.nav-credits-num { font-variant-numeric: tabular-nums; }
/* The chip while a purchase's sum resolves (8lu9.38, credits-nav.js
   beginCreditsNavSum): the old words and their +N laid over the new words, the
   cell as wide as what it shows at each step. */
.nav-credits-sum { position: relative; display: inline-block; white-space: nowrap; overflow: hidden; line-height: 16px; }
.nav-credits-was { position: absolute; left: 0; top: 0; }
.nav-credits-tok { display: inline-block; margin-left: 5px; font-style: normal; font-weight: 600; font-variant-numeric: tabular-nums; }
/* On a phone the bar has room for the mark, the language select, the chip and the
   menu button, not the wordmark too. The chip keeps its clock and chevron, so it
   still reads as a button that opens (owner, 2026-10-21), and drops the sentence
   for the bare time instead: "11 h 08 min", not "11 h 08 min left" or "Te quedan
   11 h 08 min". The sentence goes sr-only, not display:none, so the accessible
   name is the same at every width (the .pl-lbl-wide pattern). Measured on alpha,
   both glyphs with the full sentence wrapped the bar a row deeper at 360px. */
.nav-credits-short { display: none; }
@media (max-width: 520px) {
  .nav-credits-chip .nav-credits-long {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .nav-credits-chip .nav-credits-short { display: inline; }
  .topnav:has(.nav-credits:not([hidden])) .brand > span { display: none; }
}
/* Top up is a full-width primary button at the menu's foot (prototypes/hour-packs,
   nav-debt; owner, 2026-10-15), and the debt its own red row (8lu9.5). */
.nav-credits-foot .nav-credits-topup { width: 100%; box-sizing: border-box; }
.nav-credits-pop li.nav-credits-debt { color: var(--danger); }
.nav-credits-key-debt { background: var(--danger); }
.nav-credits-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: var(--fs-sub);
}

/* The confirm receipt (2vhw.17, prototypes/player-credits/ decision 2): balance
   now -> after, this video, one solid bar per source with the part this video
   takes in ink, a deduction row per source. On the one-file confirm card and
   under the order screen's clip list (js/upload-credits-confirm.js). The source
   colours are the mockup's working palette (README, Open): free minutes carry the
   accent, the first lot in spend order (grants before packs) the neutral data
   ink, every later one the lighter grey (the mockup's srcCls), the used part
   the text ink. */
.up-receipt { width: 100%; text-align: left; margin-top: 4px; }
.mp-arrange-wrap .up-receipt { margin-top: 18px; padding-top: 4px; border-top: 1px solid var(--border); }
.up-receipt-unknown { margin-top: 16px; width: 100%; }
/* Why a match under the minimum costs the minimum (2vhw.46): under the receipt, or under the short card's meter. */
.rcpt-minimum { margin-top: 16px; }
.rcpt-flow { margin-top: 16px; display: flex; align-items: flex-end; flex-wrap: wrap; gap: 6px 14px; }
.rcpt-fig { display: flex; flex-direction: column; gap: 6px; }
.rcpt-cap { font-size: var(--fs-sub); color: var(--muted); }
.rcpt-arr { color: var(--muted-2); font-size: var(--fs-title); line-height: 1; padding-bottom: 14px; }
.rcpt-qty { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text); }
.rcpt-qty .rcpt-d { font-weight: 600; }
.rcpt-qty .rcpt-u { color: var(--muted); font-weight: 400; letter-spacing: 0.02em; }
.rcpt-qty-xl { font-size: 32px; letter-spacing: -0.03em; line-height: 1; }
.rcpt-qty-md { font-size: var(--fs-title); letter-spacing: -0.02em; line-height: 1.1; }
.rcpt-qty-xl .rcpt-u { font-size: var(--fs-body); }
.rcpt-qty-md .rcpt-u { font-size: var(--fs-sub); }
.rcpt-qty.rcpt-dim .rcpt-d { color: var(--muted); font-weight: 500; }
.rcpt-line { display: flex; align-items: baseline; gap: 10px; margin-top: 22px; }
.rcpt-label { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-body); color: var(--text-2); }
.rcpt-label i { width: 10px; height: 10px; border-radius: 2px; background: var(--text); }
.rcpt-later { font-size: var(--fs-body); color: var(--muted); }
.rcpt-bar { display: flex; gap: 3px; height: 14px; margin-top: 8px; }
.rcpt-seg { position: relative; height: 100%; border-radius: 3px; overflow: hidden; min-width: 6px; background: var(--muted); }
.rcpt-used { position: absolute; left: 0; top: 0; bottom: 0; background: var(--text); }
.rcpt-src-free { background: var(--accent); }
.rcpt-src-old { background: var(--muted); }
.rcpt-src-new { background: var(--muted-2); }
.rcpt-rows { list-style: none; margin: 14px 0 0; padding: 0; }
.rcpt-rows li { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; padding: 8px 0; border-top: 1px dashed var(--border-2); }
.rcpt-rows li:first-child { border-top: 0; }
.rcpt-source { display: flex; align-items: flex-start; font-size: var(--fs-body); color: var(--text-2); min-width: 0; }
.rcpt-key { display: inline-block; width: 9px; height: 9px; border-radius: 2px; flex: none; margin: 4px 7px 0 0; }
.rcpt-left { display: block; color: var(--muted); font-size: var(--fs-meta); margin-top: 1px; }
.rcpt-untouched .rcpt-source { color: var(--muted); }
.rcpt-num { font-family: var(--font-mono); font-size: var(--fs-sub); font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }
.rcpt-num.rcpt-none { color: var(--muted); }
.up-confirm-cta { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.up-confirm-cta .btn-primary { min-width: 200px; }
.up-confirmed { margin-top: 6px; font-size: var(--fs-sub); color: var(--muted); }
@media (max-width: 520px) { .up-confirm-cta .btn { flex: 1 1 100%; } }

/* The upload gate on the confirm card (ADR-096, prototypes/upload-gate/,
   js/upload-gate.js): three rows of chips above the receipt, the stop branch
   under them, and the hard stop in their place. The chips are the mockup's
   `.seg button`: a pill, the accent when pressed. */
.up-gate { width: 100%; text-align: left; margin-top: 18px; }
.up-gate-title { font-size: var(--fs-body); font-weight: 500; color: var(--text); }
.up-gate-rows { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.up-gate-q { display: flex; flex-direction: column; gap: 7px; }
.up-gate-label { font-size: var(--fs-sub); font-weight: 500; color: var(--text-2); }
.up-gate-seg { display: flex; gap: 6px; flex-wrap: wrap; }
.up-gate-chip { font: inherit; font-size: var(--fs-sub); letter-spacing: -0.01em; padding: 7px 13px; border-radius: 999px; border: 1px solid var(--border-2); background: var(--surface); color: var(--text-2); cursor: pointer; }
.up-gate-chip:hover { border-color: var(--muted-2); }
.up-gate-chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 500; }
.up-gate-stop, .up-gate-blocked { width: 100%; text-align: left; margin-top: 18px; }
.up-gate-banner { width: 100%; }
.up-gate-sub { margin-top: 3px; font-size: var(--fs-sub); color: var(--text-2); }
/* Thanks, in green: the one banner that is not a warning. */
.banner-ok { background: color-mix(in oklab, var(--accent) 10%, var(--surface)); border: 1px solid color-mix(in oklab, var(--accent) 35%, var(--border)); }
.banner-ok > .banner-ico { color: var(--accent); }
.up-gate-ta { display: block; width: 100%; min-height: 84px; margin-top: 12px; padding: 10px 12px; border: 1px solid var(--border-2); border-radius: 10px; background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-body); resize: vertical; }
.up-gate-ta:focus { outline: none; border-color: var(--accent); }
.up-gate-cta { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.up-gate-err { margin: 8px 0 0; font-size: var(--fs-sub); color: var(--danger); }
/* The beat before the rows (owner, 2026-10-21): one line and a turning ring in
   their place; the host's receipt and Upload row wait with them. */
.up-gate-checking { display: flex; align-items: center; gap: 9px; color: var(--muted); }
.up-gate-spin { flex: none; width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--border-2); border-top-color: var(--accent); animation: spin 0.9s linear infinite; }
@media (prefers-reduced-motion: reduce) { .up-gate-spin { animation: none; } }
#dz-confirm.gate-checking .up-gate-title, #dz-multi-review.gate-checking .up-gate-title { display: none; }
#dz-confirm.gate-checking .up-receipt, #dz-confirm.gate-checking .up-receipt-unknown, #dz-confirm.gate-checking .up-confirm-cta { display: none; }
#dz-multi-review.gate-checking .up-receipt, #dz-multi-review.gate-checking .up-receipt-unknown, #dz-multi-review.gate-checking #mp-upload-row { display: none; }
/* The branch the rows chose: a stop hides what would upload, the hard stop the rows too. */
#dz-confirm.gate-stopped .up-receipt, #dz-confirm.gate-stopped .up-receipt-unknown, #dz-confirm.gate-stopped .up-confirm-cta { display: none; }
#dz-confirm.gate-blocked .up-gate { display: none; }
/* The same rows on the order screen (decision 8): under the clip list, above the receipt. */
.mp-arrange-wrap .up-gate { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); }
.mp-arrange-wrap .up-gate-stop, .mp-arrange-wrap .up-gate-blocked { margin-top: 14px; }
#dz-multi-review.gate-stopped .up-receipt, #dz-multi-review.gate-stopped .up-receipt-unknown, #dz-multi-review.gate-stopped #mp-upload-row { display: none; }
#dz-multi-review.gate-blocked .up-gate { display: none; }
/* The clip the check stopped on: an amber edge and a tag under its name. */
.mp-file-row-blocked { border-color: color-mix(in oklab, var(--warn) 45%, var(--border)); background: color-mix(in oklab, var(--warn) 6%, var(--surface)); }
.mp-row-mark { display: inline-block; margin-top: 4px; padding: 1px 7px; border-radius: 999px; font-size: var(--fs-meta); font-weight: 500; color: var(--warn); border: 1px solid color-mix(in oklab, var(--warn) 45%, var(--border)); }
@media (max-width: 520px) { .up-gate-chip { flex: 1 1 auto; text-align: center; } .up-gate-cta .btn { flex: 1 1 100%; } }

/* A balance that does not cover the match (2vhw.18, prototypes/player-credits/
   decisions 4 and 5): the fill meter, one piece wherever the balance is short
   (js/credits-fill-meter.js; the refused card, the waiting card and the library
   row of 2vhw.7 will reuse it, .fm-small being the library row's form), and the
   pack offer under it (js/upload-credits-short.js). */
.short-title { margin: 16px 0 0; font-size: var(--fs-head); font-weight: 500; letter-spacing: -0.014em; color: var(--text); }
.fm { margin-top: 16px; }
.fm-top { display: flex; align-items: baseline; justify-content: space-between; gap: 4px 10px; flex-wrap: wrap; }
.fm-qty { font-size: 22px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text); }
.fm-qty .fm-d { font-weight: 600; }
.fm-qty .fm-u { font-size: 12px; font-weight: 400; color: var(--muted); letter-spacing: 0.02em; }
.fm-of { font-size: var(--fs-body); color: var(--muted); }
.fm-needed { font-size: var(--fs-sub); color: var(--danger); font-variant-numeric: tabular-nums; }
.fm-track { height: 10px; border-radius: 999px; background: var(--surface-3); margin-top: 8px; overflow: hidden; }
.fm-track i { display: block; height: 100%; background: var(--warn); border-radius: 999px; }
.fm.fm-small { display: flex; align-items: center; gap: 8px; margin-top: 6px; font: var(--fs-meta) var(--font-mono); color: var(--muted); }
.fm-small .fm-track { flex: none; width: 120px; height: 5px; margin-top: 0; }
@media (max-width: 520px) { .fm.fm-small { white-space: nowrap; } .fm-small .fm-track { width: 64px; } }
.short-offer { margin-top: 18px; display: flex; align-items: center; gap: 12px 16px; flex-wrap: wrap; padding: 14px 16px; border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--radius); }
.short-offer-tx { flex: 1 1 220px; min-width: 0; }
.short-offer-tx b { display: block; font-weight: 500; font-size: var(--fs-body); color: var(--text); }
.short-offer-tx small { display: block; margin-top: 2px; font-size: var(--fs-sub); color: var(--text-2); }
.short-offer-price { font: 500 var(--fs-body) var(--font-mono); color: var(--text); white-space: nowrap; }
.short-offer .btn-ghost { background: var(--surface); }
.short-wait { margin: 10px 0 0; font-size: var(--fs-sub); color: var(--muted); line-height: 1.5; }

/* A refused hand-over on credits (2vhw.23, js/upload-credits-refused.js; spec
   prototypes/player-credits/, states refused, reconfirm, unknown-done): the card
   in place of the instrument, built from the .dz-stage pieces, the fill meter or
   the receipt, and the survey row under the actions (the mockup's .survey). */
.dz-refusal { padding-top: 20px; }
.dz-refusal .dz-stage-title { margin: 0; letter-spacing: -0.014em; }
/* The waiting card (2vhw.25) opens on its body: no title above it. */
.dz-refusal > .dz-stage-body:first-child { margin-top: 0; }
.dz-refusal .dz-stage-cta { margin-top: 18px; align-items: center; }
.refusal-clips { margin: 12px 0 0; padding: 0; list-style: none; }
.refusal-clips li { font-size: var(--fs-sub); color: var(--text); overflow-wrap: anywhere; }
.refusal-clips li + li { margin-top: 4px; }
.refusal-survey { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border); }
.refusal-survey-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.refusal-survey-row p { margin: 0; flex: 1 1 260px; font-size: var(--fs-sub); color: var(--text-2); line-height: 1.5; }
@media (max-width: 520px) { .dz-refusal .dz-stage-cta .btn { flex: 1 1 auto; } }

.nav-toggle {
  display: none;
  appearance: none;
  width: 36px;
  height: 36px;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
}
.nav-toggle:hover { background: var(--surface-2); }
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  display: block;
  width: 18px;
  height: 1.5px;
  background: var(--text);
  border-radius: 2px;
  transition: transform 160ms ease, opacity 160ms ease;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before { position: absolute; left: 0; top: -6px; }
.nav-toggle-bars::after  { position: absolute; left: 0; top:  6px; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars::after  { transform: translateY(-6px) rotate(-45deg); }

.nav-menu {
  display: none;
  position: absolute;
  top: 100%;
  right: 16px;
  margin-top: 8px;
  min-width: 220px;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
  z-index: 60;
}
.nav-menu.open { display: flex; }
.nav-menu a {
  color: var(--text-2);
  text-decoration: none;
  font-size: 14px;
  padding: 10px 12px;
  border-radius: 8px;
}
.nav-menu a:hover { background: var(--surface-2); color: var(--text); }
.nav-menu a.active { background: var(--surface-3); color: var(--text); }
.nav-menu-sep {
  height: 1px;
  background: var(--border);
  margin: 6px 4px;
}
.nav-menu-user {
  font-size: 12px;
  color: var(--muted);
  padding: 4px 12px 6px;
  word-break: break-all;
}
.nav-menu .btn { width: 100%; justify-content: center; margin-top: 4px; }

/* ───────── Buttons ───────── */
.btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid transparent;
  font-family: var(--font-sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  padding: 9px 14px;
  border-radius: 8px;
  cursor: pointer;
  transition: all 120ms ease;
  white-space: nowrap;
  text-decoration: none;
  line-height: 1.2;
}
.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
}
.btn-primary:hover { filter: brightness(1.05); transform: translateY(-1px); }
.btn-primary:disabled { opacity: 0.55; cursor: not-allowed; transform: none; filter: none; }
.btn-ghost {
  background: transparent;
  color: var(--text-2);
  border-color: var(--border);
}
.btn-ghost:hover { background: var(--surface); color: var(--text); }
/* A disabled ghost button (the match page's Invite before its player analytics
   answer, ws9c.1). Its own rule: only .btn-primary had a disabled look. */
.btn-ghost:disabled,
.btn-ghost:disabled:hover { opacity: 0.5; cursor: not-allowed; background: transparent; color: var(--text-2); }
.btn-quiet {
  background: transparent;
  color: var(--muted);
  padding: 6px 10px;
}
.btn-quiet:hover { color: var(--text); background: var(--surface); }
.btn-lg { padding: 13px 20px; font-size: 14.5px; border-radius: 10px; }
.btn-sm { padding: 6px 10px; font-size: 12px; border-radius: 6px; }
.btn-danger {
  background: transparent;
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 35%, var(--border));
}
.btn-danger:hover {
  background: color-mix(in oklab, var(--danger) 12%, transparent);
  color: var(--danger);
}

.icon-btn {
  appearance: none;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-2);
  border-radius: 8px;
  cursor: pointer;
  transition: all 120ms ease;
}
.icon-btn:hover { color: var(--text); border-color: var(--border-2); }
.icon-btn:disabled { opacity: 0.4; cursor: default; }
.icon-btn:disabled:hover { color: var(--text-2); border-color: var(--border); }

/* ───────── Surfaces & primitives ───────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border: 1px solid var(--border-2);
  border-radius: 4px;
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--muted);
  line-height: 1;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.serif { font-family: var(--font-serif); font-weight: 400; letter-spacing: -0.01em; }
.mono { font-family: var(--font-mono); }

.dot {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 999px;
  background: var(--muted);
  vertical-align: middle;
}
/* The quota fact chip's marks (index.html): a check in the chip's own ink
   while free videos are left, danger once none are. A match's status is the
   .status-tag, not a dot. */
.dot.live {
  width: 11px; height: 11px;
  border-radius: 0;
  background: currentColor;
  -webkit-mask: var(--icon-check) center / contain no-repeat;
  mask: var(--icon-check) center / contain no-repeat;
}
.dot.err { background: var(--danger); box-shadow: 0 0 0 4px color-mix(in oklab, var(--danger) 18%, transparent); }

.placeholder {
  background:
    repeating-linear-gradient(135deg,
      color-mix(in oklab, var(--surface-2) 100%, transparent) 0 8px,
      color-mix(in oklab, var(--surface-3) 100%, transparent) 8px 16px);
  color: var(--muted-2);
  font-family: var(--font-mono);
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}

/* Court visual (decorative placeholder for thumbnails) */
.court {
  --line: rgba(255,255,255,.72);
  --lw: 1.5px;
  position: relative;
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--accent) 22%, var(--bg)) 0%,
    color-mix(in oklab, var(--accent) 10%, var(--bg)) 100%);
  border-radius: var(--radius);
  overflow: hidden;
}
.court::before {
  content: "";
  position: absolute; inset: 0; margin: auto;
  height: 64%; aspect-ratio: 2 / 1;
  border: var(--lw) solid var(--line);
  background:
    /* net */
    linear-gradient(90deg, transparent calc(50% - 1px), rgba(255,255,255,.95) calc(50% - 1px) calc(50% + 1px), transparent 0),
    /* service lines */
    linear-gradient(90deg, transparent calc(15.25% - var(--lw)/2), var(--line) 0 calc(15.25% + var(--lw)/2), transparent 0),
    linear-gradient(90deg, transparent calc(84.75% - var(--lw)/2), var(--line) 0 calc(84.75% + var(--lw)/2), transparent 0),
    /* centre service line, between the service lines */
    linear-gradient(180deg, transparent calc(50% - var(--lw)/2), var(--line) 0 calc(50% + var(--lw)/2), transparent 0)
      center / 69.5% 100% no-repeat;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 999px; border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--muted-2); }

/* ───────── Layout helpers ───────── */
.page {
  padding: 32px 28px 80px;
  max-width: 1440px;
  margin: 0 auto;
}
.page-wide {
  padding: 24px 28px 80px;
  max-width: 1600px;
  margin: 0 auto;
}
.page-narrow {
  padding: 32px 28px 80px;
  max-width: 860px;
  margin: 0 auto;
}

/* Headings */
h1, h2, h3, h4 { margin: 0; letter-spacing: -0.02em; font-weight: 500; }
p { margin: 0; }

.h-display {
  font-size: clamp(40px, 6vw, 78px);
  line-height: 0.98;
  letter-spacing: -0.035em;
  font-weight: 500;
}
.h1 { font-size: 36px; line-height: 1.05; letter-spacing: -0.025em; font-weight: 500; }
.h2 { font-size: 24px; line-height: 1.2; letter-spacing: -0.02em; font-weight: 500; }
.h3 { font-size: 18px; line-height: 1.25; letter-spacing: -0.015em; font-weight: 500; }

.muted { color: var(--muted); }
.text-2 { color: var(--text-2); }

/* Chip */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 24px;
  padding: 0 9px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  /* ADR-054: the meta step, not 11.5px. The chip is shared, and this is the
     full list of what else draws it — the signed-out landing hero (three of
     them, the first thing a visitor sees), the account, clip-export,
     shared-match and video-detail screens. It is taken to the scale globally
     rather than overridden on the card, because one component at two sizes is
     the drift this change deletes. The box is a fixed 24px tall, so nothing
     above it moves on any of them. */
  font-size: var(--fs-meta);
  color: var(--text-2);
  font-family: var(--font-mono);
}
/* No list, no count: an emptied chip is a blank pill beside the card's title. */
#rallies-count-chip:empty { display: none; }
.chip-accent {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}
/* The beta tag (lsrn.50): the status tag's shape and type, without its glyph,
   since beta is not a status. Height is each context's own (coach head, door). */
.beta-tag {
  display: inline-flex;
  align-items: center;
  padding: 0 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
}
.chip-warn {
  background: color-mix(in oklab, var(--warn) 12%, var(--surface));
  border-color: color-mix(in oklab, var(--warn) 35%, var(--border));
  color: var(--warn);
}
.chip-danger {
  background: color-mix(in oklab, var(--danger) 12%, var(--surface-2));
  border-color: color-mix(in oklab, var(--danger) 35%, var(--border));
  color: var(--danger);
}

/* Status tag (G2: prototypes/palette/status-blue.html, prototypes/player-
   credits decision 12). The one chip that states a match's status, on the
   library, the match page, the upload card and admin, and the nav balance
   (.st-balance); count and fact chips keep
   the mono .chip above. A tinted tag in Geist, not mono, with a 12px glyph in
   its own ink: status never relies on colour alone. The state class picks the
   tint and the glyph together; processing is the neutral base, its arc
   turning. Every ink reaches 4.5:1 on its own wash over white (info 5.3,
   ready 5.6, warn 4.7, danger 5.8, processing 4.9). */
.status-tag {
  --st-ink: var(--muted);
  --st-wash: var(--surface-3);
  --st-line: var(--border-2);
  --st-glyph: var(--icon-arc);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 24px;
  padding: 0 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--st-line);
  background: var(--st-wash);
  color: var(--st-ink);
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
}
.status-tag::before {
  content: "";
  flex: none;
  width: 12px; height: 12px;
  background: currentColor;
  -webkit-mask: var(--st-glyph) center / contain no-repeat;
  mask: var(--st-glyph) center / contain no-repeat;
}
.status-tag.st-processing::before { animation: status-turn 1.2s linear infinite; }
@keyframes status-turn { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .status-tag::before { animation: none !important; }
}
.status-tag.st-uploading { --st-ink: var(--info); --st-wash: var(--info-soft); --st-line: var(--info-line); --st-glyph: var(--icon-up); }
.status-tag.st-pending { --st-ink: var(--info); --st-wash: var(--info-soft); --st-line: var(--info-line); --st-glyph: var(--icon-clock); }
.status-tag.st-ready { --st-ink: var(--accent); --st-wash: var(--accent-soft); --st-line: var(--accent-line); --st-glyph: var(--icon-check); }
/* Paused by the person: neutral like processing, but nothing is turning. */
.status-tag.st-paused { --st-glyph: var(--icon-pause); }
/* Not enough balance, and a partial upload ("2 of 3 uploaded"): attention. */
.status-tag.st-attention { --st-ink: var(--warn); --st-wash: rgba(148, 96, 10, 0.10); --st-line: rgba(148, 96, 10, 0.30); --st-glyph: var(--icon-alert); }
/* Failed, and every other dead end the upload card names (stopped, expired,
   discarded, not started). */
.status-tag.st-failed { --st-ink: var(--danger); --st-wash: rgba(179, 38, 30, 0.08); --st-line: rgba(179, 38, 30, 0.28); --st-glyph: var(--icon-x); }
/* The nav balance (frrx.16) takes the tint of its tone (ready, attention,
   failed) but always the clock: it counts time left, it is not a match's state. */
.status-tag.st-balance { --st-glyph: var(--icon-clock); }

.hr { height: 1px; background: var(--border); border: 0; margin: 0; }

/* Subtle grid background */
.bg-grid {
  background-image:
    linear-gradient(var(--border) 1px, transparent 1px),
    linear-gradient(90deg, var(--border) 1px, transparent 1px);
  background-size: 56px 56px;
  background-position: -1px -1px;
}

/* Form inputs */
.input,
.field input,
.field textarea,
.field select,
.ac-input {
  appearance: none;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 11px;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 13.5px;
  width: 100%;
}
.input:focus,
.field input:focus,
.field textarea:focus,
.field select:focus,
.ac-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Toast */
.toast {
  position: fixed;
  bottom: 16px;
  right: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 16px;
  font-size: 13px;
  color: var(--text);
  box-shadow: var(--shadow-2);
  z-index: 200;
  display: flex;
  align-items: center;
  gap: 10px;
}
.toast.hidden { display: none; }

/* Animations */
@keyframes pulse-soft {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 1; }
}
.pulse { animation: pulse-soft 2s ease-in-out infinite; }

@keyframes spin { to { transform: rotate(360deg); } }
.spin { animation: spin 1.2s linear infinite; }

/* Editorial mode (display headers) */
[data-direction="editorial"] .h-display {
  font-family: var(--font-serif);
  font-weight: 400;
  letter-spacing: -0.025em;
}

/* Utility */
.row { display: flex; align-items: center; gap: 12px; }
.col { display: flex; flex-direction: column; gap: 12px; }
.between { justify-content: space-between; }
.grow { flex: 1; }
.center { display: flex; align-items: center; justify-content: center; }
.hidden { display: none !important; }
/* Playtomic import controls stay out of the page unless the playtomic-import
   flag is on (app.js gatePlaytomicImport). Out-specifies .hidden, so the flag
   wins over whatever the page scripts toggle. */
html:not(.pt-import-on) [data-pt-import] { display: none !important; }

/* ───────────────────────────────────────────────────────────────────────── */
/* CTA card — the landing's closing section, and the sign-up card on the     */
/* shared match and rally pages. (The landing's own styles are landing.css;  */
/* the old landing that lived in styles.css went with ADR-058.)              */
/* ───────────────────────────────────────────────────────────────────────── */
.cta-section {
  padding: 60px 28px 100px;
  max-width: 1440px;
  margin: 0 auto;
  width: 100%;
}
.cta-card {
  display: grid;
  grid-template-columns: 1.4fr auto;
  gap: 40px;
  align-items: center;
  padding: 48px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  position: relative;
  overflow: hidden;
}
.cta-card::before {
  content: "";
  position: absolute;
  right: -8%; bottom: -40%;
  width: 50%;
  aspect-ratio: 2 / 1;
  background: radial-gradient(ellipse, var(--accent-soft) 0%, transparent 70%);
  z-index: 0;
}
.cta-card > * { position: relative; z-index: 1; }

/* ───────────────────────────────────────────────────────────────────────── */
/* Upload                                                                    */
/* ───────────────────────────────────────────────────────────────────────── */
/* 616px = the 560px card plus .page's own 28px of padding either side. Without
   a cap of its own this screen inherits .page's 1440px, which is why a card
   that measures 560 in the mockup sat in an open field on alpha (vyt.10).
   The cap is on the SCREEN rather than on the card so the head above it narrows
   with it — a 36px heading running to 1440 over a 560px card is the same
   mismatch one level up. */
.upload-screen { display: flex; flex-direction: column; gap: 32px; max-width: 616px; }
.up-head { margin-bottom: 0; }
/* The PAGE's sub line, not the card's — ADR-050 decision 1 settled this
   block's type and ADR-054 does not reopen it. All that changed is where the
   15px is written: it was a `style="font-size:"` on the <p>, and the card
   below it now has none, so leaving one here would have been the only inline
   size on the screen. Same step the card's title takes, same rendered size as
   before. */
.up-head-sub { font-size: var(--fs-head); }

/* THE CARD. One surface, and its own edge says whether you can drop on it
   (vyt.10, decision 3). There used to be two boxes here: a dashed .dropzone
   with the card 28px inside it, and `.dropzone.active` only swapped the outer
   border from dashed to solid — so from the first byte the screen drew a border
   around a border. The box is gone and the card took the border over.
   The `position: relative` that closed this rule went with .up-edge (pnot.1):
   it was the containing block for that one absolutely-positioned line and
   nothing else in the card is positioned. The sticky pin does not need it —
   a sticky element is offset against its scroll container, not against the
   nearest positioned ancestor. */
.up-card {
  max-width: 560px;
  margin: 0 auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
  transition: all 200ms ease;
  /* `clip`, not `hidden`, and the difference is load-bearing: a `hidden`
     ancestor is a scroll container, and `position: sticky` inside one is
     measured against THAT box rather than the viewport — so it never sticks.
     .up-card-pin sticks on narrow screens (see the responsive section);
     measured with `hidden` it scrolled clean off the top instead.
     Both values clip the same pixels at the rounded corner, which is all this
     was ever here for. The one other difference — `hidden` establishes a block
     formatting context and `clip` does not — is inert here only because the
     1px border already stops a child margin collapsing through, so do not
     remove the border without re-checking this.
     Safari < 16 does not know `clip` and drops the declaration entirely,
     falling back to `visible`: the sticky still works, the rounded corner
     stops clipping. Deliberately no `overflow: hidden` fallback ahead of it —
     that is the value that kills the sticky, and old Safari would take it. */
  overflow: clip;
}
/* Dashed while the card is still somewhere you can drop a file, solid from the
   moment there is one. The shadow goes with it: a surface you are being invited
   to drop onto should not also be lifted off the page.
   `target` rather than the old `active`, and it is the same fact read the right
   way round — the class now says what the card IS (a drop target) instead of
   what the upload is doing, which is why it can own the border. */
.up-card.target {
  border-style: dashed;
  border-color: var(--border-2);
  box-shadow: none;
}
.up-card.over {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.dz-idle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 52px 40px 46px;
}
/* The cap panel needs the same gutters, and until vyt.10 it got them by
   accident: it carries `max-width: 540px; margin: 0 auto` inline and no padding
   of its own, which inside a .dropzone running to .page's 1440px left it several
   hundred pixels of clear space either side. Capping the card at 560px leaves it
   NINE — measured on the rendered page in headless Chrome, card inner width
   558px — so the heading, the feedback textarea and the survey rows land against
   the card's border. With this rule the same reading is 50px. The panel is one
   of the two entry states, so it takes .dz-idle's padding rather than
   .dz-active's tighter 28px.

   Neither this panel nor the old multi-part panel (#dz-mp-active, gone since
   aymc.50) is in the mockup, which is why the cap reached them unmeasured: the four states drawn there are idle, uploading,
   paused and done. Both were re-measured at 390/768/1280/1680 when this rule
   went in, along with #dz-multi-review — no horizontal overflow in any of
   them. */
.dz-quota { padding: 52px 40px 46px; }
.dz-icon {
  width: 64px; height: 64px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.dz-hint {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 40px;
  padding: 8px 14px;
  background: var(--bg-2);
  border-radius: 999px;
  font-size: var(--fs-meta);
  color: var(--muted);
}

/* ── The card's type, by role (ADR-054) ──────────────────────────────────
   The five classes the card's own markup spends the scale through. They are
   classes and not `style="font-size:"` attributes for one reason: an inline
   size cannot be found by reading this file, and nine of them here drifted
   into the half-pixel ladder above before anybody noticed.

   Size only, apart from .dz-title (which replaced .h2 on this card and has
   to carry the rest of that heading's treatment) and .dz-name (whose
   ellipsis rides with it because the same two nodes want both). Everything
   else — colour, spacing, family — stays where it already was, on .muted,
   .mono and the elements' own rules. */
.dz-title {
  font-size: var(--fs-title);
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-weight: 500;
}
/* The card head's first line: the file, or the match. SANS, and that is the
   point of it — this line and the one under it were both mono, which is why
   the head read as a terminal window rather than as a title over its
   details. The details keep the mono (.dz-meta below, on a node that also
   carries .mono), because they are figures that should not shuffle. */
.dz-name {
  font-size: var(--fs-head);
  font-weight: 500;
  letter-spacing: -0.014em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dz-body { font-size: var(--fs-body); }
.dz-quiet { font-size: var(--fs-sub); }
.dz-meta { font-size: var(--fs-meta); }

.dz-active { padding: 28px 28px 30px; }
.dz-file {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--border);
}
.dz-thumb {
  position: relative;
  width: 84px;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-sm);
  overflow: hidden;
  flex-shrink: 0;
}
.dz-file-meta { flex: 1; min-width: 0; }
.dz-status { display: flex; }

/* What replaced the three-phase stepper (yow.10), down to two states in vyt.3.
   One at a time, in the column the instrument was in — so the card goes from
   "bytes moving" to its ending in place, without the layout jumping. Single
   column at every width: it is a heading and a paragraph, so it has none of the
   stepper's min-content floor and needs no breakpoint of its own. */
.dz-after { padding: 24px 0 4px; }
.dz-settled {
  font-size: var(--fs-meta);
  color: var(--muted);
  padding-bottom: 14px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.dz-stage-title { font-weight: 500; font-size: var(--fs-head); }
.dz-stage-body {
  color: var(--muted);
  font-size: var(--fs-body);
  line-height: 1.55;
  margin-top: 8px;
  max-width: 46ch;
}
/* The controls a card owns itself, as opposed to #dz-done-cta at the foot of
   the card: the refusal cards' (upload-credits-refused.js). No border-top and a
   smaller gap above than .dz-done-cta's, because this sits INSIDE the state it
   belongs to rather than closing the card off. A button row on the same terms
   as .dz-done-cta — flex with a gap, wrapping rather than shrinking, since .btn
   is nowrap and a squeezed label is worse than a second line. The 46ch above is
   .dz-stage-body's own cap and does not bound this row: it is as wide as the
   card's one column, so wrapping is a narrow-viewport behaviour. */
.dz-stage-cta { margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap; }

.dz-bar {
  height: 6px;
  background: var(--surface-3);
  border-radius: 999px;
  overflow: hidden;
}
.dz-bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 200ms ease;
}

/* The instrument (yow.7). Time remaining is the largest element on the card:
   40px against the 15px stage titles, the 15px filename and the 11px note, so
   nothing else on the card is close. The upload runs for ten to forty minutes
   with the tab held open, and that is the number the person is actually waiting
   on. (The 12px rate this list used to name went from this card in pnot.1 and
   from the multi-clip panel in pnot.4 — it is the third field of the 11px
   figures line below, which is also where the byte counter vyt.10 deleted came
   back. `.ins-meter`, `.ins-rate`, `.ins-trace` and `.ins-col` are deleted with
   it: nothing draws a separate rate or a sparkline on either card now.)

   `.ins-head` has one child on both cards as a result, which is why the row it
   still declares looks pointless — it is not, quite: the `margin-bottom` is the
   gap to the bar, and the narrow-screen block near the bottom of this file
   overrides both properties per breakpoint. Collapsing it into `.ins-primary`
   is a type-scale sweep's job, not this one's. */
/* The gap under the head's border, above the countdown. The multi-clip panel
   has always had one (an inline margin-bottom on its .dz-file); on this card
   the pin followed the border with nothing between, so 40px digits sat
   straight on the line. The same 14px, as PADDING, because the pin sticks on
   a phone and its own background has to cover this strip while content
   scrolls under it — a margin is a see-through slit there. It spends 14px of
   the narrow-screen height budget (see the responsive block); the worst
   measured state left ~115px of a 360px viewport clear, so this fits. */
.up-card-pin { padding-top: 14px; }
.ins-head {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin-bottom: 14px;
}
.ins-primary { min-width: 0; flex: 1; }
/* Mono and 40px with the units at 0.42em (pnot.1), replacing 36px sans at
   weight 300 with full-size units. The units are what the size is FOR: at one
   size "6m 22s" reads as a word, and dropping them to under half keeps the
   digits the largest thing on the card rather than the sentence around them.
   No font-weight — 300 was drawing a display face thin; the mono face is read
   at its own weight. */
.ins-eta {
  font-family: var(--font-mono);
  font-size: var(--fs-eta);
  line-height: 1;
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
  /* Holds the line's height while the slot is empty — which it is before a
     rate has been measured, and again the moment the link drops. A readout
     that collapses and reopens makes the whole card jump every time the
     connection flickers. */
  min-height: 40px;
}
/* `m` and `s`. letter-spacing back to 0: the -0.035em above is set for digit
   pairs and closes two letters into each other. */
.ins-eta-unit { font-size: 0.42em; color: var(--muted); letter-spacing: 0; }
/* A word in the countdown's slot — `Paused`, and nothing else the instrument
   writes. 40px of monospace saying a word is a number that turned out not to be
   one; 0.6 of the countdown is the 24px the spec draws it at, and the muted
   colour says the same thing the grey bar underneath does.

   A RATIO of the countdown rather than a step of its own (ADR-054): the
   countdown slot is one entry on the scale, and every size derived from it has
   to come down when it does. This is also what deleted the @720 override that
   used to sit beside the phone countdown — 14px of 22px was not the 24-of-40
   ratio its own comment claimed, and now there is no second place to keep in
   step.

   calc() OF THE TOKEN, NOT `em`, and the difference between the two is the
   whole of this rule. .ins-eta-unit above can say `em` because paintReadout
   builds it as a CHILD span, where `em` does resolve against the countdown.
   This class is not a child: upload-instrument.js toggles it onto the countdown
   ELEMENT ITSELF (`el.classList.toggle('ins-eta-word', …)`, where el is
   #progress-time, the node that carries .ins-eta). A font-size in `em`
   resolves against the PARENT's computed size, never against another font-size
   on the same element — and nothing from .ins-primary up to <html> sets one. So
   `0.6em` here drew 0.6 × the 16px default = 9.6px at EVERY width: smaller than
   the 11px figures line beneath it, and pinned to the one thing on this card
   the scale does not control. calc(var(--fs-eta) * 0.6) is 24px on the card and
   13.2px on a phone, and it tracks the token the way the paragraph above always
   claimed it did. upload-layout.test.js pins both resolved values in px rather
   than this declaration's text — asserting the string was what let 9.6px ship
   green. */
.ins-eta-word { font-size: calc(var(--fs-eta) * 0.6); color: var(--muted); letter-spacing: -0.02em; }
/* The card's one measured line: bytes of total, percent, rate. Mono and tabular
   so the figures do not shuffle sideways once a second, muted because the
   countdown above it is the headline. min-height for the same reason .ins-eta
   has one and not for the two-figure states — two figures and three are one
   line of the same type either way; what collapses the row is the EMPTY slot
   (`clips.length === 0`, i.e. before a selection and after reset()), and a row
   that collapses and reopens jumps the card under it. */
.ins-figures {
  font-size: var(--fs-meta);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin-top: 7px;
  min-height: 16px;
}
.ins-note {
  font-size: var(--fs-meta);
  color: var(--muted);
  min-height: 15px;
  margin-top: 2px;
}
/* One segment per clip, sized by byte length (flex-grow is set per segment in
   JS). A single file is the one-segment case.
   6px until pnot.1, and the HEIGHT is the only thing that changed: track and
   fill have both been on the full radius (below) since long before vyt.10 took
   this bar off the single-file card. They are named in the layout test anyway,
   because at 4px the radius is what stops the left end squaring off at low
   percentages — a regression guard on a pre-existing pair, not new work.
   ONE rule for every card: #up-bar came back to it in pnot.1, and the old
   multi-part panel's #mp-bar came down to 4px with it until that panel went
   (aymc.50). */
.ins-bar {
  display: flex;
  gap: 2px;
  height: 4px;
}
.ins-seg {
  flex-basis: 0;
  flex-shrink: 1;
  min-width: 2px;
  background: var(--surface-3);
  border-radius: 999px;
  overflow: hidden;
}
.ins-seg-fill {
  height: 100%;
  background: var(--info);
  border-radius: 999px;
  transition: width 200ms ease;
}
/* A failed clip, on the TRACK as well as the fill. Colouring only the fill
   makes the commonest failure invisible: a part that is refused a session, or
   dies on its first chunk, has a 0%-wide fill, so there is nothing for the
   danger colour to land on and the segment renders exactly like a clip that
   simply has not been reached yet. */
.ins-seg-failed { background: color-mix(in oklab, var(--danger) 40%, var(--surface-3)); }
.ins-seg-failed .ins-seg-fill { background: var(--danger); }
/* Held or interrupted: desaturated, so a stopped upload does not look like a
   running one. The colour change is the whole signal — the bar keeps its fill,
   because the bytes that got there are still there.
   `-off` rather than `-paused` since pnot.1: the class is written on paused,
   offline and reconnecting alike (upload-instrument.js, render), and the name
   had stopped saying which.
   ONE selector, on the segment: `filter` renders an element together with its
   descendants, so naming the fill as well applied saturate(0.1) to the fill and
   then again to the result — an effective saturate(0.01), which is the flat
   grey the rule above is written to avoid. Measured in Chrome. */
.ins-bar-off .ins-seg { filter: saturate(0.1); }

/* The bar's legend (prototypes/unified-upload, qwog.14): one row per clip, drawn
   by the unified driver only when there is something to list. Lifted from the
   mockup; its phone frame's two rules become the max-width query below. */
.clips { margin-top: 14px; display: flex; flex-direction: column; }
.clip { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; column-gap: 10px; row-gap: 4px; align-items: center; padding: 8px 0; border-top: 1px solid var(--border); }
.clip:first-child { border-top: 0; padding-top: 0; }
.clip-n { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--muted-2); font-variant-numeric: tabular-nums; }
.clip-name { font-size: var(--fs-sub); color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clip-state { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
.clip-state.ok { color: var(--accent); }
.clip-state.bad { color: var(--danger); }
.clip-note { grid-column: 2 / -1; font-size: var(--fs-sub); line-height: 1.45; color: var(--muted); }
.clip-note.bad { color: var(--danger); }
/* A per-clip action (aymc.15): Replace… on a clip that can't be read, Use it here
   on an unfinished clip when a picked file matched none. It is about THIS clip, so
   it sits on the row, after its state; a row without one keeps three columns. */
.clip.has-act { grid-template-columns: 16px minmax(0, 1fr) auto auto; }
.clip-act { justify-self: end; }
.clip.dim .clip-name, .clip.dim .clip-state { color: var(--muted-2); }
@media (max-width: 480px) {
    .clip { grid-template-columns: 16px minmax(0, 1fr); }
    .clip-state { grid-column: 2; justify-self: start; }
    /* Under the state on a phone: beside it, the state's words run under it. */
    .clip.has-act { grid-template-columns: 16px minmax(0, 1fr); }
    .clip-act { grid-column: 2; justify-self: start; }
}

/* No animation is introduced by the instrument — its readout is rewritten on a
   ticker, not tweened — but the segment fill declares a width transition, and a
   bar stepping itself along is motion to someone who asked for none. */
@media (prefers-reduced-motion: reduce) {
  .ins-seg-fill { transition: none; }
}

.dz-done-cta {
  display: flex;
  gap: 10px;
  margin-top: 28px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
}

/* The uploading card's body is ONE column at every width (vyt.10). It was a
   `minmax(0,1fr) minmax(0,360px)` grid — .up-card-cols, with .up-card-progress
   and .up-card-details as its two tracks — from yow.6 until the card was drawn
   in its page and the grid turned out to be the thing that made the card stop
   looking like the mockup: a 360px measure truncating the title placeholder, and
   the form folded away behind a button because a column needs something to hold
   while it is empty. In one column the form is simply the next thing down, and
   the rule below is what separates it from the instrument above.

   HAZARD INHERITED FROM THAT GRID, kept because it outlives it: a wide,
   min-content-floored child in a narrow track overflows rather than shrinking.
   The three-phase stepper's `1fr 30px 1fr 30px 1fr` had a floor of ~330px and
   was measured at 81.8px of overlap at 721px beside the 360px details column —
   phase 3 painted straight over the form. Both the stepper (yow.10) and the
   grid (here) are gone, so nothing can reproduce it today; it is written down
   because the next thing wide enough to have a min-content floor will meet it
   again the moment anything on this card is put beside anything else. */
.up-card-rule {
  height: 1px;
  background: var(--border);
  margin: 22px 0 18px;
}

/* The match-details form, in the upload card's one column, below
   .up-card-rule. A plain block, so the `margin: … auto` does its own centering; the
   `text-align: left` is what stops a form of labelled fields reading as a
   banner. */
.up-match-wrap { width: min(560px, 100%); margin: 18px auto 0; text-align: left; }
/* Read-only while another run, or none, holds the match (aymc.42): `inert`
   stops every edit, this says so at a glance. The note above it is not dimmed. */
.up-match-frozen .up-match { opacity: 0.55; }
.up-match { display: flex; flex-direction: column; gap: 14px; margin-top: 12px; }
/* A class, not an inline `display: flex` — an inline display outranks .hidden
   and only stays hideable because .hidden carries !important. */
.up-match-hand { display: flex; flex-direction: column; gap: 14px; }
/* The match page's foot (ADR-083): Save writes the typed fields and shows the
   board, Cancel shows it unchanged. The upload card has no foot — it saves as
   you type — so this rule has one reader. */
.up-foot { display: flex; gap: 8px; margin-top: 4px; }
/* A stored scoreline the boxes cannot hold (a set Playtomic reported for one
   side only): printed where the boxes would be, between the pairs, and the
   save leaves it standing (ADR-048's echo rule). Match page only. */
.up-score-locked { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; }
.up-score-locked-txt { font-size: var(--fs-body); color: var(--text-2); font-variant-numeric: tabular-nums; }
/* An import's start time, echoed in grey beside the folded date and never asked
   for (ADR-043). Only the match page ever fills it. The date's own step, so the
   two read as one line (the button is .up-datebtn, --fs-body). */
.up-date-time { color: var(--text-2); font-size: var(--fs-body); }
/* "+ player" and "+ set": text links inside a sentence-less row, so nothing
   around them sets a step and they inherited the page's base size. The sub
   step, which is what the Playtomic line's links get from their .pt-hint. */
.up-player-add, .up-scorebar .up-linkbtn { font-size: var(--fs-sub); }
/* The match form's status line (#dz-match-note).
   The SUB step and not the body step, because upload-match-draft.js swaps this
   node's own class by state — `up-match-note muted` normally, `up-match-note
   pt-error` when it complains — and .pt-error is --fs-sub. At --fs-body the two
   states drew the same line at two sizes (13.5 and 12.5, .pt-error winning on
   source order a thousand lines below), which is the drift this change exists
   to delete, wearing the change's own tokens. Both states are 12.5px now,
   whichever rule wins. It reads as a muted qualifier beside .pt-hint, which is
   the same step in the same form. */
.up-match-note { margin-top: 12px; font-size: var(--fs-sub); }
/* The "use the clip's own date instead" offer, inline in #up-date-live. A button
   rather than a link because it changes a field rather than going anywhere, and
   styled as text because it sits mid-sentence inside the complaint it answers. */
/* …and the three Playtomic lines on the upload card carry the same shape
   (vyt.5): the offer, the field's way out, and the line a row that has a match
   carries. Text buttons, because each one sits inside a sentence. The only
   difference is the inline margin, which only .pt-suggest needs — those three
   are spaced by their row's own gap or by a separator. */
.pt-suggest,
.up-linkbtn {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}
.pt-suggest { margin-left: 6px; }
.pt-suggest:hover,
.up-linkbtn:hover { color: var(--text); }
.up-pt-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
.up-you-save { margin-top: -2px; font-size: var(--fs-sub); }
.up-you-save-hint, .up-you-save-status { color: var(--muted); }
.up-you-save-hint::before { content: "· "; }

/* ---- The upload card's own match fields (vyt.4) ---------------------------
   The date, as one control rather than three. A value you can type on, not a
   link to somewhere: dashed underline rather than the solid one .pt-suggest
   and every real link on this page carry, because pressing it opens the field
   under it instead of going anywhere. */
.up-date-line { margin: 0; }
.up-datebtn {
  appearance: none;
  border: 0;
  border-bottom: 1px dashed var(--border-2);
  background: none;
  padding: 0 0 1px;
  font-family: inherit;
  font-size: var(--fs-body);
  color: var(--text-2);
  cursor: pointer;
}
.up-datebtn:hover { color: var(--accent); border-bottom-color: var(--accent-line); }
/* The cap, and ONLY the cap: a date is ten characters and a field the width of
   the card claims otherwise. The column override `.pt-input`'s `flex: 1 1 320px`
   needs is already carried by `.pt-field .pt-input` below, which matches this
   input through its wrapper — repeating it here did nothing, since `.pt-input`
   is later in the file and wins the specificity tie against `.up-date-input`. */
.up-date-input { max-width: 170px; }
/* One label over four boxes, two to a row: the row IS the pair, so the sides
   need no headings of their own. Grid rather than flex so both boxes in a row
   are the same width whatever their placeholder says.
   `min-width` because .pt-input floors at 200px for the full-width link box,
   which two-across would overflow rather than shrink. No `flex` reset: these
   are GRID items, so the shorthand would be inert — the stretch that sizes them
   is grid's own default. */
.up-players { display: flex; flex-direction: column; gap: 8px; }
.up-player-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.up-player-row .pt-input { width: 100%; min-width: 0; }
/* The score, between the two pairs (vyt.7). The digits sit DEAD CENTRE: the
   "Score" label and the "+ set" link take equal flexible cells either side of
   them, so the block of boxes lands on the card's centre line and the top row
   of digits reads as the pair above it, the bottom row as the pair below.
   Centring is what removes the need for team headings, so it is load-bearing
   rather than decorative.
   Both side cells keep their share whatever is in them — basis 0, not the
   `auto` a flex item takes by default — because "+ set" HIDES once the third
   set opens, and a cell sized by its (now empty) contents would collapse and
   slide the digits sideways under the fingers of whoever just pressed it. The
   longhand is written out because that basis is the load-bearing part; `flex: 1`
   is the identical declaration, and `flex: auto` is the one that breaks it. */
.up-scorebar { display: flex; align-items: center; gap: 10px; }
.up-scorebar > .up-score-side { flex: 1 1 0; min-width: 0; }
.up-scorebar > .up-score-side:first-child { text-align: right; }
/* Intrinsic width, never growing or shrinking: that is what makes the two equal
   side cells centre it. */
.up-sets { flex: 0 0 auto; display: flex; align-items: center; gap: 7px; }
/* A `.up-set` is one SET — the pair's own box above the other pair's — not one
   team. Stacking them is what puts each number next to the names it belongs
   to. */
.up-set { display: flex; flex-direction: column; gap: 4px; }
/* Not `.pt-input`: that one is a full-width field with a 200px floor and a 320px
   flex basis, and two digits need none of it. What it DOES carry over is the
   16px — anything smaller and iOS Safari zooms the page when the box takes
   focus, which on this row would jump the card sideways mid-score. The mockup's
   13px is a desktop drawing, and a fine-pointer override beside .pt-input now
   draws close to it (the body step) where no focus zoom exists; this 16px is
   the same box on a phone.

   Which makes it one of the two SIXTEEN-PIXEL exceptions ADR-054 leaves off the
   type scale on purpose (`.pt-input` is the other): 16px here is a browser
   threshold, not a step someone chose, and rounding it to --fs-body would trade
   a readable scale for a page that jumps. It is not one of only two exceptions
   overall — OFF_SCALE in tests/off-scale.js holds the list, and the
   type-scale block at the top of this file lists them. Named there, so it stays
   an exception somebody wrote down rather than one that crept back. */
.up-sc {
  width: 46px; text-align: center; padding: 8px 0;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font-size: 16px; font-variant-numeric: tabular-nums;
}
.up-sc:focus { outline: none; border-color: var(--accent-line); }

/* ---- Club, folded away under the players (vyt.9) --------------------------
   The rule is the whole reason "below the players" reads as a separate
   question instead of a fifth name box. */
.up-rule { height: 1px; background: var(--border); }
.up-club { display: flex; flex-direction: column; gap: 10px; }
.up-club-field { display: flex; flex-direction: column; gap: 10px; }
/* Same override, same reason, as `.pt-field .pt-input`: .pt-input carries
   `flex: 1 1 320px; min-width: 200px` for the full-width link box, which is a
   ROW. In this column that grow factor stretches the input's HEIGHT and the
   200px floor overflows a narrow card instead of shrinking. */
.up-club-field .pt-input { flex: 0 0 auto; width: 100%; min-width: 0; }
/* Dashed, not solid, and not a link: pressing it opens the field underneath
   rather than going anywhere — the same distinction .up-datebtn draws with the
   same dashed border. */
.up-addbtn {
  align-self: flex-start;
  appearance: none;
  background: none;
  border: 1px dashed var(--border-2);
  border-radius: 999px;
  color: var(--muted);
  font-family: inherit;
  font-size: var(--fs-sub);
  padding: 6px 13px;
  cursor: pointer;
}
.up-addbtn:hover { color: var(--text-2); border-color: var(--muted-2); }
/* Secondary to the field above them: quieter, smaller, and prefixed by a label
   that says what they are rather than asking for a decision. */
.up-picks { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.up-picks-lead {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted-2);
  margin-right: 1px;
}
.up-pick {
  appearance: none;
  font-family: inherit;
  font-size: var(--fs-body);
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 12px;
  cursor: pointer;
}
.up-pick:hover { border-color: var(--border-2); color: var(--text-2); }
.up-pick[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}

/* `.up-side` and `.up-privacy-list` stood here until ADR-054 and rendered
   nothing: no template, script or test referenced either name — they are the
   right-hand column ADR-050 deleted. They are gone rather than taken to the
   scale, because a dead rule holding a live-looking token is how a deleted
   size comes back: someone copies a selector that still exists. */

/* A notice with an icon and one line of text (vyt.1, pnot.3). Two tones:
   `banner-warn` for something that goes wrong if it is ignored, `banner-plain`
   for a state that is merely worth stating. The card's live states use it above
   the controls (#up-tabline in index.html).

   `.banner-sub` — the muted second line each notice used to stack under its
   lead — is DELETED rather than kept for a future user. pnot.3 collapsed all
   three notices to `<b>clause.</b> reason` in one div, and #up-tabline was the
   only thing in the repo that ever carried the class, so what was left was a
   rule with no element and a narrow-screen override to keep in step with it.
   The reason clause is now plain `.banner` text: the body step, `--text`, one
   line at 560px and two on a phone. upload-layout.test.js pins the absence on both
   sides — no `.banner-sub` in this file outside the comments that explain the
   deletion (the test strips those first), and inside the notice nothing but the
   `<b>` with a box of its own. (The old `.resume-banner`, which never folded
   into this, went with the multi-clip driver that drew it, aymc.50.) */
.banner {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  padding: 11px 13px;
  border-radius: var(--radius);
  /* ADR-054 takes this to the body step (13.5px, from 13px). Unlike .chip,
     `.banner` is NOT shared: the only elements carrying it are the card's own
     three notices, #up-tabline-open/-paused/-offline, and compress-worker.js
     never writes this class — its "banner" is prose in a comment. So this is a card-local rule taking a card step, and
     it needs none of the cross-surface argument .chip needs. The narrow-screen
     step down to --fs-sub is unchanged and still stops at 720px. Re-measured:
     the 560px card still gives each of the three notices one line. */
  font-size: var(--fs-body);
  color: var(--text);
}
.banner-warn {
  background: color-mix(in oklab, var(--warn) 12%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--warn) 35%, var(--border));
}
.banner-plain {
  background: var(--surface-2);
  border: 1px solid var(--border);
}
/* Something the person did that did not work (a file that is none of the clips),
   where every amber banner is about the connection. aymc.13. */
.banner-bad {
  background: color-mix(in oklab, var(--danger) 10%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--danger) 30%, var(--border));
}
.banner b { font-weight: 500; }
.banner-ico { flex-shrink: 0; margin-top: 1px; }
.banner-warn > .banner-ico { color: var(--warn); }
.banner-plain > .banner-ico { color: var(--muted); }
.banner-bad > .banner-ico { color: var(--danger); }

/* The slot the three live-state notices share, above the pause/cancel row. The
   margin lives here rather than on `.banner`, so the three states cannot drift
   apart by one of them carrying a different one. */
.up-tabline { margin-top: 16px; }

/* ───────────────────────────────────────────────────────────────────────── */
/* Library                                                                   */
/* ───────────────────────────────────────────────────────────────────────── */
/* One prefix for this screen. These rules are ported from
   docs/mockups/library-v2.html, which was written to be lifted; the .lib-* set
   they replace is deleted rather than left alongside them. */
.lb-screen { display: flex; flex-direction: column; gap: 18px; }
.lb-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.lb-shell {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 22px;
  align-items: start;
}

.lb-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 80px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.lb-empty-court {
  position: relative;
  width: 320px;
  aspect-ratio: 2 / 1;
  opacity: 0.6;
}

/* The rail — one date bucket per line, with its count */
.lb-rail { display: flex; flex-direction: column; gap: 18px; }
.lb-rail-group { display: flex; flex-direction: column; gap: 1px; }
.lb-rail-label {
  padding: 0 10px 6px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-2);
}
.lb-rail-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.lb-rail-item:hover { background: var(--surface-2); color: var(--text-2); }
.lb-rail-item.active {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 7px 9px;
  color: var(--text);
  font-weight: 500;
  box-shadow: var(--shadow-1);
}
.lb-rail-count {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--muted-2);
  flex-shrink: 0;
}
.lb-rail-item.active .lb-rail-count { color: var(--muted); }

/* Search + the grouped list */
.lb-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.lb-search {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--muted);
}
.lb-search input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 13.5px;
}
.lb-search input::placeholder { color: var(--muted); }

.lb-groups { display: flex; flex-direction: column; gap: 22px; }
.lb-group { display: flex; flex-direction: column; gap: 8px; }
.lb-group-head { font-size: 13px; font-weight: 600; letter-spacing: -0.01em; margin: 0; }

/* One match */
.lb-card {
  position: relative;
  display: grid;
  grid-template-columns: 190px 1fr auto;
  align-items: center;
  gap: 20px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 120ms ease;
}
.lb-card:hover { border-color: var(--border-2); box-shadow: var(--shadow-1); }
.lb-card.working { opacity: 0.85; }
/* A stretched link, not a wrapper: the whole card opens the match while the
   kebab's buttons stay SIBLINGS of the anchor. A <button> inside an <a> is
   invalid HTML and gives the button two conflicting Enter behaviours, which is
   the shape .lib-row had. The anchor keeps the match title as its text, moved
   off-screen by text-indent, so it is not a link with no accessible name. */
.lb-card-link {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  text-indent: -9999px;
  overflow: hidden;
  white-space: nowrap;
}
.lb-card:focus-within { border-color: var(--accent-line); }
.lb-card-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Above the stretched link, or the kebab is unclickable — but ONLY the kebab.
   Raising the whole .lb-side put the status pill and the when-line above the
   link too, so clicking either did nothing: a dead patch of card, and a wide
   one in the phone layout below, where .lb-side is the card's full last row. */
.lb-card > .lb-side .lb-menu-wrap { z-index: 2; }
/* A match waiting for credits (2vhw.25; prototypes/player-credits, state library):
   not dimmed like a working row, ringed in the warning tint so it reads as the
   row that wants something, and its "Top up to process" above the stretched link
   as the kebab is — only the button, so the chip and the need still open the card. */
.lb-card.waiting { border-color: var(--border-2); box-shadow: 0 0 0 3px color-mix(in oklab, var(--warn) 15%, transparent); }
.lb-card > .lb-side .lb-waiting-status .btn { position: relative; z-index: 2; }
.lb-waiting-meter:empty { display: none; }
.lb-waiting-meter .fm.fm-small { margin-top: 0; }
/* And above the FOLLOWING cards while the menu is down. The rule above only
   reaches the page from a card that is not its own stacking context, and
   .lb-card.working is one — opacity < 1 makes it one — so it is clamped inside
   exactly the cards a stuck upload produces. The menu hangs past the card's
   bottom edge (5px here; ~36px in the phone layout below, where .lb-side is
   the last row), and the next card painted over it: a tap on Delete opened
   that card's match instead. Raising the card fixes both layouts in one place,
   and stays under .topnav's 50. */
.lb-card.menu-open { z-index: 3; }

.lb-thumb {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 7px;
  overflow: hidden;
  background: #000;
}
.lb-thumb-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.lb-thumb-play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(180deg, transparent 45%, rgba(0, 0, 0, 0.45) 100%);
  color: rgba(255, 255, 255, 0.95);
  opacity: 0;
  transition: opacity 120ms ease;
}
.lb-card:hover .lb-thumb-play { opacity: 1; }
/* The thumbnail of a working card carries NOTHING over the court — no bar, no
   shimmer, no spinner. The rules that used to sit here dressed a fill
   hardcoded at 35% (padel-website-vyt.2): nothing records a processing
   sub-stage, so any quantity here is invented, and one that never moves
   reports a match handed over ten seconds ago exactly like one wedged for six
   hours. The status pill says which state the card is in, and
   .lb-card.working dims it. */

.lb-body { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.lb-title {
  font-size: 15.5px;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lb-lineup {
  font-size: 14px;
  color: var(--text-2);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lb-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--muted);
}
.lb-meta-item { display: inline-flex; align-items: center; gap: 5px; }
.lb-meta svg { flex-shrink: 0; opacity: 0.8; }
.lb-sep { color: var(--muted-2); }

/* What happened in the match, as numbers, on the bottom line of the middle
   column — below the club-and-time meta, never in .lb-side with the status.
   The status pill answers "is it done yet" and is round; these answer "what
   was this match" and are square, so the two never read as the same kind of
   thing. Ported from docs/mockups/library-v2.html with its --mono swapped for
   this sheet's --font-mono, and without the mockup's dashed .unbacked variant:
   that marked a number no column held yet, and this one does. */
.lb-pills { display: flex; gap: 6px; flex-wrap: wrap; }
.lb-pill {
  padding: 3px 9px;
  border-radius: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  font-size: 11.5px;
  font-family: var(--font-mono);
  color: var(--text-2);
  white-space: nowrap;
}

.lb-side { display: flex; align-items: center; gap: 12px; padding-left: 4px; }
.lb-side-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  text-align: right;
}
.lb-when { font-size: 12px; color: var(--muted); white-space: nowrap; }
/* An unfinished upload's resume deadline carries its exact instant as a title
   (aymc.14, prototypes/unified-upload): hint that there is one to hover. */
.lb-when[title] { text-decoration: underline dotted var(--border-2); text-underline-offset: 3px; cursor: help; }

/* Delete lives here and nowhere else: two deliberate steps, on the screen
   people scan fastest. A finished match also gets its two share items above
   it (rdxv.1). */
.lb-menu-wrap { position: relative; }
.lb-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 150px;
  padding: 5px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: 9px;
  box-shadow: var(--shadow-2);
  display: flex;
  flex-direction: column;
}
/* Manage sharing is a link (it navigates to the match page), dressed as
   the buttons beside it. */
.lb-menu button,
.lb-menu a {
  display: block;
  box-sizing: border-box;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-sans);
  font-size: 13px;
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}
.lb-menu button:hover,
.lb-menu a:hover { background: var(--surface-2); color: var(--text); }
.lb-menu button.danger { color: var(--danger); }

.lb-blank {
  padding: 48px 20px;
  text-align: center;
  color: var(--muted);
  font-size: 13.5px;
  background: var(--surface);
  border: 1px dashed var(--border-2);
  border-radius: var(--radius);
}

/* ── Loading in place (padel-website-frrx.17) ──────────────────────────────
   prototypes/library-skeletons/: the page holds its shape from first paint
   and the data fills it in. One prefix, .sk-. A skeleton block is the shape
   of the text it stands for, in surface-3, inside a box of the real line
   height, so a card does not change height when the words replace the bars
   (124.3px desktop, 232.3px phone, both states). */
.sk-line { display: flex; align-items: center; }
.sk-bar {
  display: block;
  width: var(--w, 100%);
  height: 0.62em;
  border-radius: 4px;
  background: var(--surface-3);
}
.lb-title.sk-line { height: calc(15.5px * 1.3); }
.lb-lineup.sk-line { height: calc(14px * 1.3); }
.lb-meta.sk-line { height: 16px; }
.lb-meta.sk-line .sk-bar + .sk-bar { display: none; }
.lb-when.sk-line { height: 16px; justify-content: flex-end; }
/* On a phone the club, time and length wrap to a second line on most
   matches (the body column is ~200px), so the skeleton draws two. */
@media (max-width: 720px) {
  .lb-meta.sk-line { height: auto; flex-direction: column; align-items: flex-start; gap: 8px; }
  .lb-meta.sk-line .sk-bar { height: 8px; margin: 4px 0; }
  .lb-meta.sk-line .sk-bar + .sk-bar { display: block; }
}
.sk-pill {
  width: var(--w, 64px);
  height: 23px;
  border-radius: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.sk-tag {
  display: inline-block;
  width: var(--w, 62px);
  height: 24px;
  border-radius: 6px;
  background: var(--surface-3);
  border: 1px solid var(--border);
}
.sk-kebab { display: block; width: 32px; height: 32px; }

/* The skeleton card is the real card with no link, no hover and no menu.
   Its thumbnail is the court every card already draws under its poster,
   so the thumb does not change at all when the match arrives. */
.lb-card.sk-card { pointer-events: none; }
.lb-group-head.sk-line { height: 17px; }

.sk-rail-item { padding: 8px 10px; display: flex; justify-content: space-between; align-items: center; height: 33px; box-sizing: border-box; }
.sk-rail-item .sk-bar { height: 8px; }
.lb-rail-count .sk-bar { display: inline-block; width: 10px; height: 8px; vertical-align: middle; }
/* On a phone the rail is a row of chips, so its skeleton is too. */
@media (max-width: 720px) {
  .sk-rail-item { width: var(--w); border: 1px solid var(--border); border-radius: 7px; background: var(--surface); }
  .sk-rail-item .sk-bar:first-child { width: 70% !important; }
}
/* In em, so it follows the phone h1. */
.sk-h1 { display: flex; align-items: center; height: 1.05em; }
.sk-h1 .sk-bar { height: 0.72em; border-radius: 6px; }

/* Screen readers hear one sentence, not a pile of empty boxes. */
/* The negative margin keeps base.html's #sk-slow-status, which sits after the
   100vh .app, from adding a pixel of page scroll. */
.sk-sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* Every block breathes together, one slow breath (the owner's pick, A). */
.sk-bar, .sk-pill, .sk-tag, .sk-icon { animation: sk-breath 1.8s ease-in-out infinite; }
@keyframes sk-breath { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

/* The list arrives in place: same boxes, the words fade up from an already
   visible state. One moment for the whole list, no stagger. */
.sk-arrive { animation: sk-arrive 220ms cubic-bezier(.16, 1, .3, 1) both; }
@keyframes sk-arrive { from { opacity: 0.35; } to { opacity: 1; } }
/* Posters fade in over the court they replace, instead of popping. */
.lb-thumb .lb-thumb-img { opacity: 0; transition: opacity 260ms cubic-bezier(.16, 1, .3, 1); }
.lb-thumb .lb-thumb-img.is-loaded { opacity: 1; }

/* A video stage that waits on its clip (frrx.23): the court under it, the
   poster over the court when there is one, and the video fades in over both
   once it can play. The stage is never a black box. Decorative blocks only
   are aria-hidden; the video itself keeps its controls from the moment it
   plays. The poster has the clip's shape, so it is contained as the video is:
   a portrait clip's bars are the poster's black, and the poster stays under the
   video until a frame comes. */
.sk-stage { position: relative; background: var(--bg-2); overflow: hidden; }
.sk-stage > .court { position: absolute; inset: 0; border-radius: 0; }
.sk-stage > .sk-poster {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; display: block;
  opacity: 0; transition: opacity 260ms cubic-bezier(.16, 1, .3, 1);
}
.sk-stage > .sk-poster.is-loaded { opacity: 1; }
.sk-stage > video { transition: opacity 260ms cubic-bezier(.16, 1, .3, 1); }
.sk-stage.is-waiting > video { opacity: 0; pointer-events: none; }
.sk-stage > .sk-slow {
  position: absolute; left: 12px; right: 12px; bottom: 10px; margin: 0;
  text-align: center; color: var(--text-2);
}
/* The share modal's members list answers on its own read: while the body is
   still the skeleton (which draws a members row of its own), it waits. */
#share-loading:not(.hidden) ~ #share-members { display: none; }
/* The modals' bodies (frrx.23): the same gap as .modal-body, the real
   link field with a bar for the link and a box for Copy, and rows of bars. */
.sk-modal { display: flex; flex-direction: column; gap: 14px; }
.sk-modal .sk-bar { max-width: 100%; }
.sk-p { margin: 0; line-height: 1.6; }
.sk-p .sk-bar { display: inline-block; vertical-align: middle; }
.sk-field { height: 50px; box-sizing: border-box; justify-content: space-between; }
.sk-field .sk-bar { flex: 0 1 auto; height: 8px; }
.sk-btn-box { flex: none; width: 56px; height: 32px; border-radius: 8px; background: var(--surface-3); animation: sk-breath 1.8s ease-in-out infinite; }
.sk-btn-bar { height: 12px; }
.sk-members li { height: 40px; box-sizing: border-box; }
.sk-members .sk-bar { height: 8px; }
.sk-textarea { height: 62px; box-sizing: border-box; background: var(--surface-3); border-color: transparent; animation: sk-breath 1.8s ease-in-out infinite; }
.sk-target { height: 40px; background: var(--surface-3); border-color: transparent; pointer-events: none; animation: sk-breath 1.8s ease-in-out infinite; }
/* The account page's beta rows: a bar where the On / Off word will be. */
.ac-row .sk-bar { display: inline-block; vertical-align: middle; }

/* After 4 s still loading: say so, quietly, under the skeleton. */
.sk-slow {
  margin: 4px 2px 0;
  font-size: var(--fs-sub);
  color: var(--muted);
  opacity: 0;
  transition: opacity 400ms ease;
}
.sk-slow.is-shown { opacity: 1; }

/* A failed read: one quiet panel where the list would be, with a retry. */
.sk-error {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.sk-error p { margin: 0; font-size: var(--fs-body); color: var(--text-2); }
.sk-error .muted { font-size: var(--fs-sub); margin-top: 3px; color: var(--muted); }

/* The nav slot: the balance tag's own box, held empty until the read
   answers. Only ever rendered for a player the server knows is on credits. */
.nav-credits-slot {
  display: flex;
  align-items: center;
  height: var(--nav-control);
  /* If credits-nav.js never runs (its module failed to load on a bad
     connection), the slot must not breathe forever: past any read's timeout
     it goes still and invisible, keeping its box so nothing moves. Not
     motion, so reduced motion keeps it. */
  animation: sk-slot-expire 0s 15s forwards;
}
@keyframes sk-slot-expire { to { visibility: hidden; opacity: 0; } }
/* The chip's own box in the bar (frrx.18): its height and corners, and 6px wider
   than the 24px tag for the bar's 10px padding and 6px gaps. */
.nav-credits-slot .sk-tag { width: 137px; height: var(--nav-control); border-radius: 8px; }
@media (max-width: 520px) { .nav-credits-slot .sk-tag { width: 105px; } }

/* The match page (frrx.19) in the same vocabulary. A bar standing in for a
   line of text sits in that text's own line box: the zero-width space the
   template puts after it gives the box the real font's line height, so the
   words that replace it cannot make the line taller or shorter. */
.sk-text .sk-bar { display: inline-block; vertical-align: middle; }
/* The status tag's box in the meta line. */
.sk-status { --w: 76px; border-radius: 999px; }
/* The winners' "Won" chip on the details board (.pt-win's box). */
.sk-win { --w: 40px; height: 19px; margin-left: 8px; vertical-align: middle; border-radius: 999px; }
/* The row's icon buttons. */
.sk-icon {
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.rl-row.sk-row, .view-mode.sk-mode { pointer-events: none; cursor: default; }
/* The player's box is held black, as the stage is before a rally plays; the
   empty video element and its controls stay out of it until the rallies land. */
#rallies-card[aria-busy="true"] #rally-player { visibility: hidden; }
/* The quiet line on the mode description's own line, in place of its bar. */
#view-mode-desc { position: relative; }
#view-mode-desc .sk-slow {
  position: absolute;
  left: 0;
  top: 0;
  margin: 0;
  white-space: nowrap;
}
#view-mode-desc:has(.sk-slow.is-shown) .sk-text { visibility: hidden; }
/* The late blocks above the rallies card (frrx.20), held only when the server
   knows they come: the same real boxes, bars for what the reads bring. */
.sk-eta { --w: 118px; border-radius: 999px; }
/* The claim row's "This is me": the small button's own box (its padding,
   border and the font's line box), so the card does not grow when it lands. */
.btn.sk-claim {
  width: 82px;
  background: var(--surface-3);
  border-color: transparent;
  pointer-events: none;
}
.btn.sk-claim, .hbar .bar.sk-hbar, .zbar.sk-zbar, .sbar.sk-zbar, .team-badge .sw.sk-bar {
  background: var(--surface-3);
  opacity: 1;
  animation: sk-breath 1.8s ease-in-out infinite;
}
.team-badge .sw.sk-bar { display: inline-block; }

/* Reduced motion: a still skeleton, and the words and posters simply appear. */
@media (prefers-reduced-motion: reduce) {
  .sk-bar, .sk-pill, .sk-tag, .sk-icon, .sk-arrive,
  .btn.sk-claim, .hbar .bar.sk-hbar, .zbar.sk-zbar, .sbar.sk-zbar,
  .sk-btn-box, .sk-textarea, .sk-target { animation: none; }
  .lb-thumb .lb-thumb-img, .sk-slow, .sk-stage > video, .sk-stage > .sk-poster { transition: none; }
}

/* ───────────────────────────────────────────────────────────────────────── */
/* Match detail                                                              */
/* ───────────────────────────────────────────────────────────────────────── */
.match-detail { display: flex; flex-direction: column; gap: 20px; }

/* Match page v2 (lsrn.2, prototypes/match-page-v2/): the coach in an always-open
   420px column fixed flush to the window's right edge (lsrn.21: it runs to the
   window's bottom at every scroll position), a border on its left, full
   height under the top bar (and the email strip while it shows), its own
   scroll. The server renders .mp2 and #coach-rail with #ai-coach inside it
   (lsrn.17); the columns are keyed on the viewport, above 1100px, so the page
   is never drawn with a rail that a script then takes away, and below it the
   rail is not there (the phone's bar opens the coach page). match-page-v2.js
   sets --mp2-top to the top bar plus the email strip. The rail shows while the
   card does, or while it is held for a card on its way (data-coach-pending:
   the `ai-coach-chat` flag is on, ai-coach.js has not answered), and only then
   does the page give up its max-width and right padding. */
.mp2-rail { display: none; }
.mp2-coach-btn { display: none; }
@media (min-width: 1101px) {
  .match-detail.mp2 { display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 20px 28px; align-items: start; }
  .match-detail.mp2.mp2-coach-open:has(> .mp2-rail[data-coach-pending]),
  .match-detail.mp2.mp2-coach-open:has(> .mp2-rail > #ai-coach:not(.hidden)) { max-width: none; padding-right: 0; }
  .match-detail.mp2 > #content { grid-column: 1; }
  .mp2-coach-open > .mp2-rail[data-coach-pending], .mp2-coach-open > .mp2-rail:has(> #ai-coach:not(.hidden)) {
    grid-column: 2; grid-row: 1;
    position: fixed; top: var(--mp2-top, 64px); right: 0; bottom: 0; width: 420px;
    z-index: 5;
    display: flex; flex-direction: column; min-height: 0;
    background: var(--surface); border-left: 1px solid var(--border);
  }
  /* The email strip is in the page flow: over the first --mp2-strip pixels of
     scroll it slides under the top bar, and the rail's top rises with it, so
     no gap opens under the bar. Declarative (a scroll timeline); a browser
     without one keeps the top under bar + strip. */
  @supports (animation-timeline: scroll()) {
    .mp2-coach-open > .mp2-rail[data-coach-pending], .mp2-coach-open > .mp2-rail:has(> #ai-coach:not(.hidden)) {
      animation: mp2-rail-rise linear both;
      animation-timeline: scroll(root block);
      animation-range: 0px var(--mp2-strip, 0px);
    }
  }
  /* The rail is out of flow (fixed), so the grid's second column only reserves
     its width; the footer keeps the same offset and runs under the match column. */
  .app:has(> main > .match-detail.mp2.mp2-coach-open > .mp2-rail[data-coach-pending]) > .legal-links,
  .app:has(> main > .match-detail.mp2.mp2-coach-open > .mp2-rail > #ai-coach:not(.hidden)) > .legal-links { margin-right: 420px; }
  /* Coach hidden (the default, lsrn.22) or no coach card: today's column, full
     width (the grid's align-items: start would shrink the main column to its
     content). The rail is display: none until the page carries mp2-coach-open. */
  .match-detail.mp2:not(.mp2-coach-open),
  .match-detail.mp2:not(:has(> .mp2-rail[data-coach-pending])):not(:has(> .mp2-rail > #ai-coach:not(.hidden))) { display: flex; align-items: stretch; }
  /* The name row's "AI coach" (lsrn.22, prototypes/coach-launcher/ 2): the
     row's one filled button, a toggle on the rail. Shown while the coach is
     held or drawn, so a match without one has no door to nothing. */
  .mp2-coach-btn {
    display: none; box-sizing: border-box; align-items: center; gap: 8px; height: 36px; padding: 0 14px 0 6px; border: 0; border-radius: 999px;
    background: var(--accent); color: #fff; font-family: var(--font-sans); font-size: 13.5px; font-weight: 500; white-space: nowrap; cursor: pointer;
  }
  .match-detail:has(> .mp2-rail[data-coach-pending]) .mp2-coach-btn,
  .match-detail:has(> .mp2-rail > #ai-coach:not(.hidden)) .mp2-coach-btn { display: inline-flex; }
  .mp2-coach-btn:hover { background: #245a3d; }
  .mp2-coach-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .mp2-coach-btn-mark { width: 24px; height: 24px; border-radius: 50%; background: #fff; display: grid; place-items: center; flex: none; }
  .mp2-coach-btn-mark::before { content: ""; width: 10px; height: 10px; background: var(--accent); clip-path: polygon(28% 16%, 28% 84%, 84% 50%); }
  .mp2-coach-btn .beta-tag { height: 18px; background: rgba(0,0,0,.2); color: #fff; border-color: transparent; }
  .mp2-coach-btn[aria-pressed="true"] { background: var(--surface-3); color: var(--text); }
  .mp2-coach-btn[aria-pressed="true"]:hover { background: color-mix(in srgb, var(--surface-3) 93%, var(--text)); }
  .mp2-coach-btn[aria-pressed="true"] .beta-tag { background: var(--accent-soft, var(--surface-2)); color: var(--accent); }
  .mp2-coach-btn[aria-pressed="true"] .mp2-coach-btn-mark { background: var(--accent); }
  .mp2-coach-btn[aria-pressed="true"] .mp2-coach-btn-mark::before { background: #fff; }
}
/* The rail's Hide button (lsrn.22): at the head line's right end. Only the
   match page's rail has a coach to hide; the coach page's head is not shown. */
.coach-hide { display: none; margin-left: auto; flex: none; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--muted); place-items: center; cursor: pointer; }
.mp2-rail .coach-hide { display: grid; }
.coach-hide:hover { background: var(--surface-2); color: var(--text); }
.coach-hide:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.mp2-rail #ai-coach { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.mp2-rail .coach-card { flex: 1; min-height: 0; overflow: hidden; border: 0; border-radius: 0; background: transparent; }
.mp2-rail .coach-thread { flex: 1; min-height: 0; max-height: none; }
@keyframes mp2-rail-rise {
  from { top: var(--mp2-top, 64px); }
  to { top: calc(var(--mp2-top, 64px) - var(--mp2-strip, 0px)); }
}

/* Match page v2, the rally strip (lsrn.3, prototypes/match-page-v2/): the
   Rallies card's toolbar, mode pills and list go and the player block is one
   dark surface, the frame with a caption over it, a thin progress line, the
   control row and the strip of clips. match-page-v2-strip.js builds the .mp2-*
   chrome and moves the page's own buttons into it. Only under .mp2-player
   (flag on, at every width since lsrn.14: the server draws it with .mp2 since
   lsrn.17, and match-page-v2.js takes it off if the strip cannot attach) and
   only once the card has a player to show; the empty and failed states keep
   the card as it was. What differs at 1100px and below is in the 1100px rules
   after this block. */
.match-detail.mp2-player {
  --pl: #0b0f0d; --pl-2: rgba(255,255,255,.07); --pl-3: rgba(255,255,255,.12); --pl-line: rgba(255,255,255,.16);
  --pl-text: #e6ebe8; --pl-muted: #9aa39e; --pl-faint: #7f8a84; --pl-accent: #9dd4b0;
  --pl-a: #3d8bdd; --pl-b: #f07a45; --pl-u: #5f6a64;
}
.match-detail.mp2-player #rallies-card:has(> .sbs-main:not(.hidden)) {
  display: block; padding: 0; border: 0; background: none; box-shadow: none;
}
.match-detail.mp2-player #rallies-card:has(> .sbs-main:not(.hidden)) > .rallies-pin > .md-toolbar,
.match-detail.mp2-player #rallies-card:has(> .sbs-main:not(.hidden)) #rallies-player-wrap,
.match-detail.mp2-player #rallies-card:has(> .sbs-main:not(.hidden)) > .rally-list-wrap,
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-now,
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-kbd-hints,
.match-detail.mp2-player #rallies-card > .sbs-main .rally-report-trigger { display: none; }
.match-detail.mp2-player #rallies-card > .sbs-main {
  display: block; position: relative; overflow: visible;
  background: var(--pl); border-radius: 14px; color: var(--pl-text);
}
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-stage { border-radius: 14px 14px 0 0 !important; overflow: hidden; }
.match-detail.mp2-player .mp2-cap {
  display: flex; align-items: flex-start; gap: 10px; position: absolute; left: 0; right: 0; top: 0; z-index: 3;
  padding: 14px 16px 30px; background: linear-gradient(rgba(0,0,0,.6), transparent); pointer-events: none;
}
.match-detail.mp2-player .mp2-cap > * { pointer-events: auto; }
.match-detail.mp2-player .mp2-cap-titles { min-width: 0; pointer-events: none; }
.match-detail.mp2-player .mp2-cap-title { margin: 0; font-size: 15px; font-weight: 500; color: #fff; font-feature-settings: "tnum"; }
.match-detail.mp2-player .mp2-cap-meta { margin: 0; font: 11.5px var(--font-mono); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-cap-tools { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.match-detail.mp2-player .mp2-cap-tools :is(.icon-btn, .mp2-gb) {
  height: 30px; min-width: 30px; width: auto; padding: 0 10px; gap: 6px; border: 0; border-radius: 8px;
  background: var(--pl-3); color: var(--pl-text); display: inline-flex; align-items: center; justify-content: center;
}
.match-detail.mp2-player .mp2-cap-tools :is(.mp2-gb, .mp2-lbl) { font: inherit; font-size: 12.5px; }
.match-detail.mp2-player .mp2-cap-tools :is(.icon-btn, .mp2-gb):hover { background: rgba(255,255,255,.2); color: var(--pl-text); }
.match-detail.mp2-player .mp2-cap-tools :is(.icon-btn.on, .icon-btn[aria-pressed="true"], .mp2-gb.on) { color: var(--pl-accent); }
.match-detail.mp2-player .mp2-more { position: relative; }
.match-detail.mp2-player .mp2-menu {
  display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 5; min-width: 180px; padding: 4px;
  border-radius: 10px; background: #1a211d; box-shadow: 0 0 0 1px var(--pl-line), 0 18px 40px -12px rgba(0,0,0,.6);
}
.match-detail.mp2-player .mp2-menu.open { display: block; }
.match-detail.mp2-player .mp2-menu-item {
  width: 100%; height: 32px; padding: 0 10px 0 8px; border: 0; border-radius: 6px; background: none; color: var(--pl-text);
  font: inherit; font-size: 13px; text-align: left; cursor: pointer; white-space: nowrap;
  display: flex; align-items: center; gap: 8px;
}
/* Every row keeps the tick's 14px slot, so the labels line up; only a checked
   Auto-play next shows it. */
.match-detail.mp2-player .mp2-menu-item .mp2-tick { flex: none; visibility: hidden; color: var(--pl-accent); }
.match-detail.mp2-player .mp2-menu-item[aria-checked="true"] .mp2-tick { visibility: visible; }
.match-detail.mp2-player .mp2-menu-item:hover, .match-detail.mp2-player .mp2-menu-item:focus-visible { background: var(--pl-3); outline: 0; }
.match-detail.mp2-player .mp2-prog {
  height: 3px; background: var(--pl-line); position: relative; z-index: 2; cursor: pointer; touch-action: none; outline: 0;
}
/* The seek bar draws 3px and takes the pointer 8px above and below it (19px). */
.match-detail.mp2-player .mp2-prog::before { content: ""; position: absolute; left: 0; right: 0; top: -8px; bottom: -8px; }
.match-detail.mp2-player .mp2-prog > u { display: none; }
.match-detail.mp2-player .mp2-prog > .mp2-tip { display: none; }
.match-detail.mp2-player .mp2-prog > i { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--pl-accent); pointer-events: none; }
.match-detail.mp2-player .mp2-prog > b {
  position: absolute; top: 50%; left: 0; width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px; border-radius: 50%;
  background: var(--pl-accent); opacity: 0; transition: opacity 120ms ease; pointer-events: none;
}
.match-detail.mp2-player .mp2-prog:is(:hover, :focus-visible, .drag) > b { opacity: 1; }
.match-detail.mp2-player .mp2-prog:focus-visible > b { box-shadow: 0 0 0 3px rgba(157,212,176,.35); }
.match-detail.mp2-player .mp2-ctl {
  display: flex; align-items: center; gap: 6px; padding: 8px 12px; font: 12px var(--font-mono); color: #c7cec9;
}
.match-detail.mp2-player .mp2-ctl :is(.mp2-ib, .icon-btn) {
  width: 30px; height: 30px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--pl-text);
  display: inline-grid; place-items: center; cursor: pointer;
}
.match-detail.mp2-player .mp2-ctl :is(.mp2-ib, .icon-btn):hover:not(:disabled) { background: var(--pl-3); color: var(--pl-text); }
.match-detail.mp2-player .mp2-ctl .icon-btn:disabled { opacity: .35; }
.match-detail.mp2-player .mp2-time { margin: 0 8px 0 4px; }
.match-detail.mp2-player .mp2-sp { margin-left: auto; }
.match-detail.mp2-player .mp2-strip {
  display: flex; gap: 6px; overflow-x: auto; padding: 10px 14px 14px; border-top: 1px solid var(--pl-line);
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.2) transparent;
}
.match-detail.mp2-player .mp2-clip {
  flex: none; width: 112px; height: 75px; padding: 9px 10px 8px; border: 0; border-radius: 6px;
  background: var(--pl-2); color: var(--pl-text); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u));
  display: flex; flex-direction: column; justify-content: center; gap: 4px; text-align: left; white-space: nowrap;
  cursor: pointer; transition: background 120ms ease; font: inherit;
}
.match-detail.mp2-player .mp2-clip[data-call="a"] { --edge: var(--pl-a); }
.match-detail.mp2-player .mp2-clip[data-call="b"] { --edge: var(--pl-b); }
.match-detail.mp2-player .mp2-clip:hover { background: var(--pl-3); }
.match-detail.mp2-player .mp2-clip b { font: 500 13.5px/20px var(--font-sans); font-feature-settings: "tnum"; }
.match-detail.mp2-player .mp2-clip em { font: normal 10.5px var(--font-mono); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-clip.on { background: rgba(157,212,176,.16); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)), 0 0 0 1.5px var(--pl-accent); }
.match-detail.mp2-player .mp2-clip:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
/* The strip grouped by game (lsrn.4, README "Grouping and the Score checkbox"):
   the strip bar over it holds the Set and Game dropdowns and
   the Score checkbox with its "estimated" note. A grouped clip is 50px under a
   25px label line (14px text, 4px under it, its 1px hairline, the 6px gap), so
   the 75px plain clip takes the same height and Score never moves the page. */
.match-detail.mp2-player .mp2-strip { position: relative; }
.match-detail.mp2-player .mp2-strip.mp2-under-bar { padding-top: 0; border-top: 0; }
.match-detail.mp2-player .mp2-strip.grouped { gap: 14px; }
.match-detail.mp2-player .mp2-grp { flex: none; display: flex; flex-direction: column; gap: 6px; }
.match-detail.mp2-player .mp2-gl {
  font: 10.5px/14px var(--font-mono); color: var(--pl-muted); text-transform: uppercase; letter-spacing: .04em;
  white-space: nowrap; padding-bottom: 4px; border-bottom: 1px solid var(--pl-line);
}
.match-detail.mp2-player .mp2-gl > span { position: sticky; left: 0; display: inline-block; padding-right: 10px; background: var(--pl); }
.match-detail.mp2-player .mp2-gi { display: flex; gap: 6px; }
.match-detail.mp2-player .mp2-strip.grouped .mp2-clip { height: 50px; gap: 1px; }
.match-detail.mp2-player .mp2-clip b.sc { display: flex; justify-content: space-between; align-items: baseline; height: 20px; }
.match-detail.mp2-player .mp2-clip b.sc sup { font: 10.5px var(--font-mono); color: var(--pl-faint); vertical-align: baseline; }
.match-detail.mp2-player .mp2-clip b.n { font: 12px/20px var(--font-mono); color: #c7cec9; }
.match-detail.mp2-player .mp2-sbar {
  display: flex; align-items: center; gap: 8px; min-height: 45px; padding: 6px 14px 10px; border-top: 1px solid var(--pl-line);
  font-size: 12.5px; color: var(--pl-text);
}
.match-detail.mp2-player .mp2-dds { display: flex; flex: none; gap: 6px; }   /* never squeezed onto two lines (Edit games beside it, f2pe.4) */
.match-detail.mp2-player .mp2-dd { position: relative; }
.match-detail.mp2-player .mp2-dd-btn {
  height: 28px; padding: 0 10px 0 11px; border: 0; border-radius: 8px; background: var(--pl-3); color: var(--pl-text);
  font: inherit; font-size: 12.5px; display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
}
.match-detail.mp2-player .mp2-dd-btn:hover,
.match-detail.mp2-player .mp2-dd.open > .mp2-dd-btn { background: rgba(255,255,255,.2); }
.match-detail.mp2-player .mp2-chev { display: inline-flex; transform: rotate(180deg); transition: transform 160ms var(--ease, ease-out); }
.match-detail.mp2-player .mp2-dd.open .mp2-chev { transform: none; }
.match-detail.mp2-player .mp2-dd-menu {
  position: absolute; left: 0; bottom: calc(100% + 6px); z-index: 5; min-width: 240px; white-space: nowrap;
  max-height: 280px; overflow: auto; padding: 4px; border-radius: 10px; background: #1a211d;
  box-shadow: 0 0 0 1px var(--pl-line), 0 18px 40px -12px rgba(0,0,0,.6); display: none;
}
.match-detail.mp2-player .mp2-dd.open > .mp2-dd-menu { display: block; }
.match-detail.mp2-player .mp2-dd-row {
  width: 100%; display: grid; grid-template-columns: 14px 1fr auto; align-items: center; gap: 8px; height: 32px;
  padding: 0 10px 0 8px; border: 0; border-radius: 6px; background: none; color: var(--pl-text); font: inherit; font-size: 13px;
  text-align: left; cursor: pointer;
}
.match-detail.mp2-player .mp2-dd-row::before { content: ""; }
.match-detail.mp2-player .mp2-dd-row.on { color: #fff; font-weight: 500; }
.match-detail.mp2-player .mp2-dd-row.on::before { content: "✓"; color: var(--pl-accent); font-size: 12px; }
.match-detail.mp2-player .mp2-dd-row:is(:hover, :focus-visible) { background: var(--pl-3); outline: 0; }
.match-detail.mp2-player .mp2-dd-row em { font: normal 10.5px var(--font-mono); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-score { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; white-space: nowrap; }
.match-detail.mp2-player .mp2-score input {
  appearance: none; width: 16px; height: 16px; margin: 0; border-radius: 4px; border: 1.5px solid rgba(255,255,255,.45);
  display: grid; place-items: center; cursor: pointer;
}
.match-detail.mp2-player .mp2-score input:checked { background: var(--pl-accent); border-color: var(--pl-accent); }
.match-detail.mp2-player .mp2-score input:checked::after {
  content: ""; width: 8px; height: 4px; border: 2px solid #0b0f0d; border-top: 0; border-right: 0; transform: translateY(-1px) rotate(-45deg);
}
.match-detail.mp2-player .mp2-score input:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
.match-detail.mp2-player .mp2-est { font: 10.5px var(--font-mono); color: var(--pl-muted); padding-left: 6px; border-left: 1px solid var(--pl-line); }
/* Fixing a call while watching (f2pe.3, prototypes/match-edit/ callControl):
   "Won by" A · B · ? and Not a point for the rally in the player. On desktop it
   sits in the strip bar after a hairline and Hidden points after Score; at 1100px
   and below, and on a narrow column above it (lsrn.51), both are on the call line under the bar (lsrn.42). A and B carry their team's colour as a top edge,
   the chosen one fills. A rally the owner took out shows dimmed, and a clip
   whose call the owner changed carries an amber dot top-right. */
.match-detail.mp2-player .mp2-vsep { flex: none; width: 1px; height: 18px; margin: 0 4px; background: var(--pl-line); }
.match-detail.mp2-player .mp2-callc { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; font: 12px var(--font-sans); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-callc-g { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--pl-2); }
.match-detail.mp2-player .mp2-callc-g button {
  height: 24px; min-width: 28px; padding: 0 9px; border: 0; border-radius: 6px; background: none; color: var(--pl-text);
  font: 500 11.5px var(--font-mono); cursor: pointer;
}
.match-detail.mp2-player .mp2-callc-g button:hover { background: var(--pl-3); }
.match-detail.mp2-player .mp2-callc-g button:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 1px; }
.match-detail.mp2-player .mp2-callc-g button:disabled { opacity: .45; cursor: default; pointer-events: none; }   /* Edit games saving, or its 409 */
.match-detail.mp2-player .mp2-callc-g .mp2-callc-ta { box-shadow: inset 0 3px 0 var(--pl-a); }
.match-detail.mp2-player .mp2-callc-g .mp2-callc-tb { box-shadow: inset 0 3px 0 var(--pl-b); }
.match-detail.mp2-player .mp2-callc-g .mp2-callc-ta.on { box-shadow: none; background: var(--pl-a); color: #06121f; }
.match-detail.mp2-player .mp2-callc-g .mp2-callc-tb.on { box-shadow: none; background: var(--pl-b); color: #1f0b02; }
.match-detail.mp2-player .mp2-callc-g .mp2-callc-u.on { background: var(--pl-u); color: #fff; }
.match-detail.mp2-player .mp2-callc-g .mp2-callc-no { color: var(--pl-muted); font: 400 11.5px var(--font-sans); }
.match-detail.mp2-player .mp2-callc-g .mp2-callc-no.on { background: var(--pl-3); color: var(--pl-text); }
/* In the strip bar and on the call line the control sits on the shelf, white above
   1100px (lsrn.46): it reads from the shelf's palette, as the clips do, and the dark
   block's in full screen. Team colours stay the player's. */
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc { color: var(--sh-mute); }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g { background: var(--sh-clip); }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g button { color: var(--sh-ink); }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g button:hover:not(.on) { background: var(--sh-clip-h); }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g button:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g .mp2-callc-ta.on { color: #06121f; }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g .mp2-callc-tb.on { color: #1f0b02; }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g .mp2-callc-u.on { color: #fff; }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g .mp2-callc-no { color: var(--sh-mute); }
.match-detail.mp2.mp2-player :is(.mp2-sbar, .mp2-callline) .mp2-callc-g .mp2-callc-no.on { background: var(--sh-btn); box-shadow: 0 0 0 1px var(--sh-line); color: var(--sh-ink); }   /* no team colour to fill with: a chip, prototypes/match-edit/ */
.match-detail.mp2-player .mp2-hidden { display: inline-flex; align-items: center; gap: 8px; margin-left: 14px; cursor: pointer; user-select: none; white-space: nowrap; }
.match-detail.mp2-player .mp2-hidden input {
  appearance: none; width: 16px; height: 16px; margin: 0; border-radius: 4px; border: 1.5px solid rgba(255,255,255,.45);
  display: grid; place-items: center; cursor: pointer;
}
.match-detail.mp2-player .mp2-hidden input:checked { background: var(--pl-accent); border-color: var(--pl-accent); }
.match-detail.mp2-player .mp2-hidden input:checked::after {
  content: ""; width: 8px; height: 4px; border: 2px solid #0b0f0d; border-top: 0; border-right: 0; transform: translateY(-1px) rotate(-45deg);
}
.match-detail.mp2-player .mp2-hidden input:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
/* The call line (lsrn.42, lsrn.51 above 1100px): under the strip bar at 1100px and below, never in the control row, so the idle fade does not reach it. */
.match-detail.mp2-player .mp2-callline { display: none; flex-wrap: wrap; align-items: center; gap: 4px; padding: 0 0 8px; color: var(--sh-mute); font: 12px var(--font-sans); }
@media (max-width: 720px) { .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-callline { padding-inline: 12px; } }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen > .mp2-callline { padding-inline: 14px; }
.match-detail.mp2-player .mp2-callline .mp2-hidden { gap: 6px; margin-left: auto; color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-callline .mp2-hidden input { border-color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-callline .mp2-hidden input:checked { background: var(--sh-acc); border-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-callline .mp2-hidden input:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-callline .mp2-hidden em { color: var(--sh-mute); }
.match-detail.mp2-player .mp2-hidden em, .match-detail.mp2-player .mp2-menu-item em { font: normal 10.5px var(--font-mono); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-menu-item em { margin-left: auto; padding-left: 12px; }
.match-detail.mp2-player .mp2-clip { position: relative; }
.match-detail.mp2-player .mp2-clip.out { opacity: .5; }
.match-detail.mp2-player .mp2-clip.out.on { opacity: .8; }
.match-detail.mp2-player .mp2-clip .mp2-dot { position: absolute; top: 8px; right: 8px; width: 7px; height: 7px; border-radius: 50%; background: #f2b134; }
/* A ready vertical clip (4y44): a 9:16 frame after the score or number. */
.match-detail.mp2-player .mp2-clip .mp2-v { display: inline-block; color: var(--pl-accent); margin-left: 6px; width: 7px; height: 12px; border: 1.5px solid currentColor; border-radius: 2px; vertical-align: -1px; background: color-mix(in srgb, currentColor 30%, transparent); }
.match-detail.mp2.mp2-player .mp2-clip .mp2-v { color: var(--sh-acc); }
/* Edit games (f2pe.4, prototypes/match-edit/ "Two ways in" and "The map"): the
   strip bar's opener, and while the map is open its head (Editing sets and games
   · N changes, ! N games look wrong ›), the call control, Hidden points and Back
   to detected · Cancel · Done, the dropdowns and Score gone. The
   strip becomes the map: a 48px tile per rally (the score over its number, the
   top edge the point winner's), under a label per game whose line takes the game
   winner's colour, amber and dashed when nobody can tell who won. The phone's
   bar is f2pe.12's, in the phone block below (720px and under). */
.match-detail.mp2-player :is(.mp2-pb, .mp2-edit-open, .mp2-chk) {
  flex: none; height: 28px; padding: 0 11px; border: 0; border-radius: 8px; background: var(--pl-3); color: var(--pl-text);
  font: inherit; font-size: 12.5px; display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; cursor: pointer;
}
.match-detail.mp2-player :is(.mp2-pb, .mp2-edit-open):hover:not(:disabled) { background: rgba(255,255,255,.2); }
.match-detail.mp2-player :is(.mp2-pb, .mp2-edit-open, .mp2-chk):focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
.match-detail.mp2-player :is(.mp2-pb, .mp2-edit-open):disabled { opacity: .45; cursor: default; }
.match-detail.mp2-player .mp2-pb.q { background: none; color: var(--pl-muted); }
.match-detail.mp2-player .mp2-pb.q:hover:not(:disabled) { background: var(--pl-2); color: var(--pl-text); }
.match-detail.mp2-player .mp2-pb.go { background: var(--pl-accent); color: #0b0f0d; font-weight: 500; }
.match-detail.mp2-player .mp2-pb.go:hover:not(:disabled) { background: #b6e2c4; }
.match-detail.mp2-player .mp2-edit-open { margin-left: auto; }
.match-detail.mp2-player .mp2-edit-open:not(.hidden) ~ .mp2-score { margin-left: 6px; }
.match-detail.mp2-player :is(.mp2-edit-head, .mp2-edit-tail, .mp2-conflict) { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; }
.match-detail.mp2-player .mp2-edit-tail { gap: 6px; }
.match-detail.mp2-player :is(.mp2-elab, .mp2-eshort) {
  display: inline-flex; align-items: center; gap: 8px; font: 500 10.5px var(--font-mono); letter-spacing: .08em;
  text-transform: uppercase; color: var(--pl-accent);
}
.match-detail.mp2-player :is(.mp2-elab, .mp2-eshort)::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--pl-accent); }
.match-detail.mp2-player .mp2-cnt { font: 11px var(--font-mono); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-chk { background: none; color: #f2b134; box-shadow: inset 0 0 0 1px rgba(242,177,52,.55); }
.match-detail.mp2-player .mp2-chk:hover { background: var(--pl-2); }
.match-detail.mp2-player .mp2-conflict { color: #f2b134; }
.match-detail.mp2-player .mp2-sbar.mp2-editing { flex-wrap: wrap; row-gap: 6px; }
.match-detail.mp2-player .mp2-sbar.mp2-editing .mp2-hidden { margin-left: auto; }
.match-detail.mp2-player .mp2-sbar.mp2-editing .mp2-edit-tail { margin-left: auto; }
.match-detail.mp2-player .mp2-strip.mp2-map { gap: 0; padding-top: 4px; }
.match-detail.mp2-player .mp2-map .mp2-grp { gap: 5px; }
.match-detail.mp2-player .mp2-map .mp2-gi { gap: 0; padding: 0 12px; }   /* the 8px between tiles is the gap's Insert (.mp2-seam) */
.match-detail.mp2-player .mp2-tl {
  width: 0; min-width: calc(100% - 24px); height: 20px; margin-inline: 12px; padding-bottom: 3px; overflow: clip;
  display: flex; align-items: center; border-bottom: 2px solid var(--gw, var(--pl-line));
  font: 10.5px var(--font-mono); color: var(--pl-muted); text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.match-detail.mp2-player .mp2-tl[data-won="a"] { --gw: var(--pl-a); }
.match-detail.mp2-player .mp2-tl[data-won="b"] { --gw: var(--pl-b); }
.match-detail.mp2-player .mp2-tl.open { --gw: #f2b134; border-bottom-style: dashed; }
.match-detail.mp2-player .mp2-tlb { position: sticky; left: 0; z-index: 1; display: inline-flex; align-items: center; gap: 5px; height: 18px; padding-right: 6px; background: var(--pl); }
.match-detail.mp2-player .mp2-tlb.warn { color: #f2b134; }
.match-detail.mp2-player .mp2-bang {
  display: inline-grid; place-items: center; width: 13px; height: 13px; border-radius: 3px; background: #f2b134; color: #1d1404;
  font: 700 10px/1 var(--font-mono);
}
.match-detail.mp2-player .mp2-srv { display: inline-flex; align-items: center; gap: 4px; font-weight: 500; }
.match-detail.mp2-player .mp2-srv.a { color: var(--pl-a); }
.match-detail.mp2-player .mp2-srv.b { color: var(--pl-b); }
.match-detail.mp2-player .mp2-srv i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.match-detail.mp2-player .mp2-gsc { margin-left: 3px; color: var(--pl-text); font-feature-settings: "tnum"; letter-spacing: .02em; }
.match-detail.mp2-player .mp2-tile {
  position: relative; flex: none; width: 48px; height: 44px; padding: 4px 6px 0; border: 0; border-radius: 5px;
  background: var(--pl-2); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)); display: flex; flex-direction: column;
  align-items: flex-start; justify-content: center; gap: 1px; font: 10px var(--font-mono); color: var(--pl-muted);
  white-space: nowrap; cursor: pointer; transition: background 120ms ease;
}
.match-detail.mp2-player .mp2-tile[data-call="a"] { --edge: var(--pl-a); }
.match-detail.mp2-player .mp2-tile[data-call="b"] { --edge: var(--pl-b); }
.match-detail.mp2-player .mp2-tile b { font: 500 11.5px/15px var(--font-sans); color: var(--pl-text); font-feature-settings: "tnum"; }
.match-detail.mp2-player .mp2-tile b.n { font: 400 11px/15px var(--font-mono); color: #c7cec9; }
.match-detail.mp2-player .mp2-tile small { font: 9.5px var(--font-mono); color: var(--pl-faint); }
.match-detail.mp2-player .mp2-tile:hover { background: var(--pl-3); }
.match-detail.mp2-player .mp2-tile.on { background: rgba(157,212,176,.18); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)), 0 0 0 1.5px var(--pl-accent); }
.match-detail.mp2-player .mp2-tile:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
.match-detail.mp2-player .mp2-tile.out {
  background: repeating-linear-gradient(135deg, transparent 0 5px, rgba(255,255,255,.05) 5px 10px);
  box-shadow: inset 0 0 0 1px var(--pl-line); opacity: .75;
}
.match-detail.mp2-player .mp2-tile.out.on { box-shadow: inset 0 0 0 1px var(--pl-line), 0 0 0 1.5px var(--pl-accent); opacity: 1; }
.match-detail.mp2-player .mp2-tile .mp2-dot { position: absolute; top: 8px; right: 4px; width: 5px; height: 5px; border-radius: 50%; background: #f2b134; }
/* The divider between two games (f2pe.10): `+`, or SET on a set start, opens
   its menu; a boundary the owner moved carries an amber ring. */
.match-detail.mp2-player .mp2-bnd {
  flex: none; position: relative; width: 20px; align-self: stretch; display: grid; place-items: center;
  padding: 0; border: 0; border-radius: 6px; background: none; color: var(--pl-muted); font: inherit; cursor: pointer;
}
.match-detail.mp2-player .mp2-bnd:disabled { cursor: default; }
.match-detail.mp2-player .mp2-bnd:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: -2px; }
.match-detail.mp2-player .mp2-bnd::after { content: ""; position: absolute; left: 50%; top: 0; bottom: 2px; border-left: 1.5px solid var(--pl-line); transform: translateX(-50%); }
.match-detail.mp2-player .mp2-bnd.set { width: 40px; }
.match-detail.mp2-player .mp2-bnd.set::after { border-left: 3px solid var(--pl-text); }
.match-detail.mp2-player .mp2-bnd i {
  position: relative; z-index: 1; width: 16px; height: 16px; border-radius: 50%; background: #1a211d; box-shadow: 0 0 0 1px var(--pl-line);
  display: grid; place-items: center; font: 600 9px/1 var(--font-mono); font-style: normal; color: var(--pl-muted);
  transition: background 120ms ease, color 120ms ease;
}
.match-detail.mp2-player .mp2-bnd.set i {
  width: auto; height: 18px; padding: 0 5px; border-radius: 4px; background: var(--pl-text); color: #0b0f0d; box-shadow: none;
  font: 600 9px/18px var(--font-mono);
}
.match-detail.mp2-player .mp2-bnd:is(:hover, [aria-expanded="true"]):not(:disabled) i { background: var(--pl-accent); color: #0b0f0d; }
.match-detail.mp2-player .mp2-bnd.mine i { box-shadow: 0 0 0 1.5px #f2b134; }
/* The gap between two tiles of a game: Insert, a dashed line and its tip on hover. */
.match-detail.mp2-player .mp2-seam {
  flex: none; position: relative; width: 8px; align-self: stretch; padding: 0; border: 0; border-radius: 4px; background: none; cursor: pointer;
}
.match-detail.mp2-player .mp2-seam:disabled { cursor: default; }
.match-detail.mp2-player .mp2-seam:focus-visible { outline: 0; }
.match-detail.mp2-player .mp2-seam::after {
  content: ""; position: absolute; left: 50%; top: 6px; bottom: 6px; border-left: 1.5px dashed transparent; transform: translateX(-50%);
}
.match-detail.mp2-player .mp2-seam:is(:hover, :focus-visible, [aria-expanded="true"]):not(:disabled)::after { border-color: var(--pl-accent); }
.match-detail.mp2-player .mp2-seam:is(:hover, :focus-visible):not(:disabled)::before {
  content: attr(data-tip); position: absolute; left: 50%; top: -20px; z-index: 2; transform: translateX(-50%); padding: 1px 6px;
  border-radius: 4px; background: var(--pl-accent); color: #0b0f0d; font: 500 10px var(--font-mono); white-space: nowrap;
}
/* A game's first and last gap sit in the row's padding, so the tiles do not move. */
.match-detail.mp2-player .mp2-seam.mp2-edge { width: 12px; }
.match-detail.mp2-player .mp2-seam.mp2-edge.before { margin-left: -12px; }
.match-detail.mp2-player .mp2-seam.mp2-edge.after { margin-right: -12px; }
.match-detail.mp2-player .mp2-spacer { flex: none; width: 8px; }
/* A point the video missed (f2pe.11): spaced like a rally, dashed, no clip. */
.match-detail.mp2-player .mp2-miss {
  position: relative; flex: none; width: 48px; height: 44px; padding: 4px 2px 0; border: 0; border-radius: 5px;
  background: color-mix(in srgb, var(--pl-2) 45%, transparent); outline: 1.5px dashed var(--edge, var(--pl-u)); outline-offset: -1.5px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  font: 10px var(--font-mono); color: var(--pl-muted); white-space: nowrap; cursor: pointer; transition: background 120ms ease;
}
.match-detail.mp2-player .mp2-miss[data-call="a"] { --edge: var(--pl-a); }
.match-detail.mp2-player .mp2-miss[data-call="b"] { --edge: var(--pl-b); }
.match-detail.mp2-player .mp2-miss:hover:not(:disabled) { background: var(--pl-2); }
.match-detail.mp2-player .mp2-miss:disabled { cursor: default; }
.match-detail.mp2-player .mp2-miss:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
.match-detail.mp2-player .mp2-miss b { font: 500 11.5px/15px var(--font-sans); color: var(--pl-muted); font-feature-settings: "tnum"; }
.match-detail.mp2-player .mp2-miss b.n { font: 400 11px/15px var(--font-mono); color: #f2b134; }
.match-detail.mp2-player .mp2-miss small { font: 9.5px var(--font-mono); color: var(--pl-faint); }
.match-detail.mp2-player .mp2-fmenu > .mp2-callc { display: flex; margin: 2px 6px 6px; }
.match-detail.mp2-player .mp2-rm-short { display: none; }
/* The map's menus: over the strip, opened from a gap or a divider. */
.match-detail.mp2-player .mp2-fmenu {
  position: absolute; z-index: 6; min-width: 230px; padding: 4px; border-radius: 10px; white-space: nowrap;
  background: #1a211d; box-shadow: 0 0 0 1px var(--pl-line), 0 18px 40px -12px rgba(0,0,0,.6);
}
.match-detail.mp2-player .mp2-fmenu-hd {
  padding: 6px 10px 4px; font: 10.5px var(--font-mono); letter-spacing: .06em; text-transform: uppercase; color: var(--pl-muted);
}
/* A game's label opens its panel (f2pe.5): the label is a button that looks the same. */
.match-detail.mp2-player button.mp2-tlb { border: 0; padding-left: 0; font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; cursor: pointer; border-radius: 4px; }
.match-detail.mp2-player button.mp2-tlb:disabled { cursor: default; }
.match-detail.mp2-player button.mp2-tlb:is(:hover, [aria-expanded="true"]):not(:disabled) { color: var(--pl-text); }
.match-detail.mp2-player button.mp2-tlb:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 1px; }
/* The game panel: one row per thing it sets, each a segmented choice; the notes
   on what stops the score above them (prototypes/match-edit/, "The game panel"). */
.match-detail.mp2-player .mp2-gpan { width: 344px; padding: 4px 4px 6px; overflow-y: auto; white-space: normal; }
.match-detail.mp2-player .mp2-gpan .mp2-fmenu-hd { padding: 8px 10px 6px; }
.match-detail.mp2-player .mp2-grow { display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: start; gap: 10px; padding: 10px; }
.match-detail.mp2-player .mp2-grow + .mp2-grow { border-top: 1px solid var(--pl-line); }
.match-detail.mp2-player .mp2-grow > span { font-size: 12.5px; line-height: 30px; color: var(--pl-muted); }
.match-detail.mp2-player .mp2-seg { display: flex; width: 100%; border-radius: 7px; box-shadow: inset 0 0 0 1px var(--pl-line); }
.match-detail.mp2-player .mp2-seg button {
  flex: 1; height: 30px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 8px; border: 0;
  border-radius: 7px; background: none; color: var(--pl-muted); font: 12.5px var(--font-sans); white-space: nowrap; cursor: pointer;
}
.match-detail.mp2-player .mp2-seg button:hover:not(:disabled) { color: var(--pl-text); }
.match-detail.mp2-player .mp2-seg button.on { background: var(--pl-3); color: var(--pl-text); font-weight: 500; }
.match-detail.mp2-player .mp2-seg button:disabled { opacity: .4; cursor: default; }
.match-detail.mp2-player .mp2-seg button:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: -2px; }
.match-detail.mp2-player .mp2-sq { width: 8px; height: 8px; border-radius: 2px; }
.match-detail.mp2-player .mp2-sq.a { background: var(--pl-a); }
.match-detail.mp2-player .mp2-sq.b { background: var(--pl-b); }
.match-detail.mp2-player .mp2-gnote { margin: 6px 0 0; font: 11px/1.4 var(--font-mono); color: var(--pl-muted); }
.match-detail.mp2-player .mp2-gnote.warn { color: #f2b134; }
.match-detail.mp2-player .mp2-giss { display: grid; gap: 8px; margin: 0 6px 4px; padding: 10px; border-radius: 8px; background: rgba(242,177,52,.09); }
.match-detail.mp2-player .mp2-gi1 p { margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--pl-text); }
.match-detail.mp2-player .mp2-gi1.mp2-gstop p::before {
  content: "!"; display: inline-grid; place-items: center; width: 13px; height: 13px; margin-right: 6px; border-radius: 3px;
  background: #f2b134; color: #1d1404; font-family: var(--font-mono); font-size: 10px; font-weight: 700; line-height: 1; vertical-align: 1px;
}
.match-detail.mp2-player .mp2-gi1 > div { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.match-detail.mp2-player .mp2-gi1 .mp2-pb { height: 26px; padding: 0 10px; font-size: 12px; }
/* A pair answer while its PUT is out: aria-disabled, so the focus stays on it. */
.match-detail.mp2-player .mp2-gi1 .mp2-pb[aria-disabled="true"] { opacity: .45; cursor: default; }
/* The strip bar on a phone (f2pe.12; prototypes/match-edit/README.md, "Phone"):
   Set, Game, Edit games, Score and an info button, which carries the "estimated"
   note's explanation on tap (tablets keep the text). In the map the head says
   "Editing" alone, Back to detected is gone, and the tiles stand further apart
   (12px, 16px at a game's edge, against 8px and 12px). */
.match-detail.mp2-player .mp2-info, .match-detail.mp2-player .mp2-eshort, .match-detail.mp2-player .mp2-why { display: none; }
@media (max-width: 720px) {
  .match-detail.mp2.mp2-player .mp2-sbar { position: relative; }
  .match-detail.mp2.mp2-player .mp2-est, .match-detail.mp2.mp2-player .mp2-est.hidden { display: none !important; }
  .match-detail.mp2.mp2-player .mp2-info:not(.hidden), .match-detail.mp2.mp2-player .mp2-score:not(.hidden) + .mp2-info {
    flex: none; display: inline-grid; place-items: center; width: 28px; height: 28px; margin: 0 -4px; padding: 0; border: 0;
    border-radius: 8px; background: none; color: var(--sh-mute); cursor: pointer;
  }
  /* Score off takes the button away and leaves its slot, so Score stays under the
     finger (lsrn.41, as "estimated" on a tablet). */
  .match-detail.mp2.mp2-player .mp2-score:not(.hidden) + .mp2-info.hidden { display: inline-grid !important; visibility: hidden; }
  .match-detail.mp2.mp2-player .mp2-sbar .mp2-info:is(:hover, [aria-expanded="true"]) { color: var(--sh-ink); }
  .match-detail.mp2.mp2-player .mp2-info:focus-visible { outline: 2px solid var(--sh-acc); outline-offset: 2px; }
  .match-detail.mp2.mp2-player .mp2-why {
    position: absolute; right: 12px; top: calc(100% - 6px); z-index: 3; width: min(280px, calc(100% - 24px)); padding: 9px 11px;
    border-radius: 8px; background: var(--surface); box-shadow: 0 6px 18px rgba(14,19,17,.22), inset 0 0 0 1px var(--border-2);
    font-size: 12px; line-height: 1.4; color: var(--text);
  }
  .match-detail.mp2.mp2-player .mp2-why:not(.hidden) { display: block; }
  .match-detail.mp2.mp2-player .mp2-edit-head .mp2-elab { display: none; }
  .match-detail.mp2.mp2-player .mp2-edit-head .mp2-eshort { display: inline-flex; order: -1; }
  .match-detail.mp2.mp2-player .mp2-edit-tail .mp2-pb.q { display: none; }
  .match-detail.mp2.mp2-player .mp2-map .mp2-gi { gap: 12px; padding: 0 16px; }
  /* The row's 12px gap beside the edge gap is taken back too: it sits in the 16px. */
  .match-detail.mp2.mp2-player .mp2-seam.mp2-edge { width: 16px; }
  .match-detail.mp2.mp2-player .mp2-seam.mp2-edge.before { margin: 0 -12px 0 -16px; }
  .match-detail.mp2.mp2-player .mp2-seam.mp2-edge.after { margin: 0 -16px 0 -12px; }
  /* The row's own 12px gap already spaces a missing point. */
  .match-detail.mp2.mp2-player .mp2-spacer { display: none; }
  .match-detail.mp2.mp2-player .mp2-rm-full { display: none; }
  .match-detail.mp2.mp2-player .mp2-rm-short { display: inline; }
  .match-detail.mp2.mp2-player .mp2-tl { min-width: calc(100% - 32px); margin-inline: 16px; }
}
/* The v1 sets-and-games editor is retired from v2 (lsrn.16): Edit games is the
   only structure editor, at every width. The page keeps the button for v1 (flag
   off); under v2 it never shows, so adjust mode cannot be entered. */
.match-detail.mp2 #ms-adjust-btn { display: none !important; }
/* The comment field opens from the caption's Comment; the problem report from
   its "…" menu. Both sit under the strip, on the dark surface. */
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-comment { display: none; margin: 0; padding: 0 14px 14px; }
.match-detail.mp2-player #rallies-card > .sbs-main.mp2-comment-open > .rally-comment { display: flex; }
.match-detail.mp2-player .rally-comment textarea { background: var(--pl-2); border-color: var(--pl-line); color: var(--pl-text); }
.match-detail.mp2-player .rally-comment #rally-comment-status { color: var(--pl-muted); }
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-report { margin: 0; padding: 0 14px; color: var(--pl-muted); }
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-report .rally-report-panel { padding-bottom: 14px; }
.match-detail.mp2-player #rallies-card > .sbs-main > .rally-report .rally-report-desc { color: var(--pl-muted); }
.match-detail.mp2-player #rallies-card > .sbs-main:fullscreen { display: flex; flex-direction: column; border-radius: 0; overflow: auto; }
.match-detail.mp2-player #rallies-card > .sbs-main:fullscreen > .rally-stage { flex: 1; min-height: 0; aspect-ratio: auto !important; border-radius: 0 !important; }
@media (prefers-reduced-motion: reduce) { .match-detail.mp2-player :is(.mp2-clip, .mp2-prog > b) { transition: none; } }
/* First paint of match page v2 (lsrn.18, prototypes/match-page-v2-polish/
   section 2): the server draws the final layout and every piece of data that
   arrives later has a stand-in at the size the real one takes, so nothing moves
   when it lands. Held while the Rallies card's player block is inert (video-
   detail.js lets it go with the list). The blocks breathe as the page's other
   skeletons do (sk-breath) and stand still for reduced motion. On the dark
   video they are translucent white, not the page's grey. */
.mp2-sk { display: block; flex: none; border-radius: 6px; background: rgba(255,255,255,.14); animation: sk-breath 1.8s ease-in-out infinite; }
/* The frame: the poster dimmed under a 28px spinner on a dark disc, until the
   player fades in over both. */
.match-detail.mp2-player .sk-stage.is-waiting > .sk-poster { filter: brightness(.72) saturate(.9); }
.mp2-spin { display: none; }
.match-detail.mp2-player .sk-stage.is-waiting > .mp2-spin { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.mp2-spin i {
  width: 28px; height: 28px; border-radius: 50%; box-sizing: border-box;
  border: 2.5px solid rgba(255,255,255,.22); border-top-color: #fff;
  background: rgba(10,14,12,.45); box-shadow: 0 0 0 10px rgba(10,14,12,.45);
  animation: mp2-rot 1s linear infinite;
}
@keyframes mp2-rot { to { transform: rotate(360deg); } }
/* The control row's buttons are drawn, inactive, until the rallies load; the
   time is a bar until a rally is in the player. */
.match-detail.mp2-player .sbs-main[inert] .mp2-ctl :is(.mp2-ib, .icon-btn) { opacity: .35; pointer-events: none; }
.match-detail.mp2-player .sbs-main[inert] .mp2-time:empty { display: block; height: 12px; border-radius: 6px; background: rgba(255,255,255,.14); animation: sk-breath 1.8s ease-in-out infinite; }
.match-detail.mp2-player .sbs-main[inert] .mp2-time:empty { width: 84px; margin: 0 10px 0 6px; }
/* The strip bar: the Set and Game dropdowns and the Score box, in the real
   bar's box (padding, 45px floor, hairline). It sits in front of the strip and
   goes when the real bar shows or the list is in. */
.match-detail.mp2-player .mp2-sk-bar { display: none; }
.match-detail.mp2-player .sbs-main[inert] .mp2-sk-bar { display: flex; align-items: center; gap: 8px; box-sizing: border-box; min-height: 45px; padding: 6px 14px 10px; border-top: 1px solid var(--pl-line); }
.match-detail.mp2-player .mp2-sk-bar > .mp2-sk { height: 28px; border-radius: 8px; }
.match-detail.mp2-player .mp2-sk-bar > .mp2-sk-score { width: 96px; margin-left: auto; }
.match-detail.mp2-player .mp2-sbar:not(.hidden) ~ .mp2-sk-bar { display: none; }
/* A structure still out past the budget (video-detail.js) brings the bar after
   the strip: above 1100px its box stays held, so the frame, and with it the
   column (lsrn.47), does not open wide and narrow when it lands. */
@media (min-width: 1101px) {
  .match-detail.mp2-player .sbs-main.mp2-bar-late > .mp2-sbar.hidden ~ .mp2-strip { padding-top: 0; border-top: 0; scrollbar-width: none; }
  .match-detail.mp2-player .sbs-main.mp2-bar-late:has(> .mp2-sbar.hidden) > .mp2-sk-bar { display: flex; align-items: center; gap: 8px; box-sizing: border-box; min-height: 46px; padding: 6px 14px 10px; border-top: 1px solid var(--pl-line); }
}
/* The call line (lsrn.51): on a page that will have the control where one bar row will not fit, its
   36px are held with the bar's. The same step as the column's first-paint rule, as
   a padding, since only that resolves a percentage against the block's width. fit()
   then says which. */
.match-detail.mp2-player .mp2-sk-line { display: none; }
@media (min-width: 1101px) {
  .match-detail.mp2.mp2-player .sbs-main[inert] > .mp2-sk-line,
  .match-detail.mp2.mp2-player .sbs-main.mp2-bar-late > .mp2-sk-line {
    display: block; position: relative; box-sizing: border-box; height: 0; overflow: hidden;
    padding-top: clamp(0px, (var(--mp2-bar-need) - 100%) * 1000, 36px);
  }
  .match-detail.mp2.mp2-player.mp2-fitted .sbs-main > .mp2-sk-line { padding-top: 0; }
  .match-detail.mp2.mp2-player.mp2-fitted.mp2-line .sbs-main > .mp2-sk-line { padding-top: 36px; }
  .match-detail.mp2.mp2-player .sbs-main:not(.mp2-bar-late) > .mp2-sbar:not(.hidden) ~ .mp2-sk-line,
  .match-detail.mp2.mp2-player .sbs-main > .mp2-callline:not(.hidden) ~ .mp2-sk-line { display: none; }
  .match-detail.mp2-player .mp2-sk-line > .mp2-sk { position: absolute; left: 8px; top: 0; width: 240px; height: 28px; border-radius: 8px; }
}
/* The strip: four games of four clips in the grouped strip's boxes (a 19px
   label line, 50px clips). The first draw replaces them. */
.match-detail.mp2-player .mp2-sk-strip { display: none; }
.match-detail.mp2-player .sbs-main[inert] .mp2-sk-strip { display: flex; gap: 14px; overflow: hidden; padding: 0 14px 14px; }
.match-detail.mp2-player .mp2-sk-grp { flex: none; display: flex; flex-direction: column; gap: 6px; }
.match-detail.mp2-player .mp2-sk-gl { height: 19px; box-sizing: border-box; padding: 2px 0 4px; border-bottom: 1px solid var(--pl-line); }
.match-detail.mp2-player .mp2-sk-gl > .mp2-sk { width: 150px; height: 10px; }
.match-detail.mp2-player .mp2-sk-gi { display: flex; gap: 6px; }
.match-detail.mp2-player .mp2-sk-clip { width: 112px; height: 50px; }
/* The real strip waits behind it, laid out (the script measures it) but with no height. */
.match-detail.mp2-player .sbs-main[inert] .mp2-strip { height: 0; padding: 0; border: 0; overflow: hidden; visibility: hidden; }
/* The coach's rail, held for a card on its way: the stand-in fills it and goes
   when the card shows (or the rail is let go: no data-coach-pending). */
.mp2-coach-sk { display: none; }
.mp2-rail[data-coach-pending] > #ai-coach.hidden ~ .mp2-coach-sk { flex: 1; min-height: 0; display: flex; flex-direction: column; pointer-events: none; }
.mp2-coach-sk .coach-head { gap: 12px; }
.mp2-coach-sk .coach-head .sk-bar { height: 14px; }
.mp2-coach-sk .mp2-sk-lines { display: flex; flex-direction: column; gap: 9px; padding-top: 7px; }
.mp2-coach-sk .mp2-sk-lines .sk-bar { height: 12px; border-radius: 6px; }
.mp2-coach-sk .coach-input { height: 32px; box-sizing: border-box; }
.mp2-coach-sk .coach-tools .sk-bar { height: 10px; }
.mp2-coach-sk .coach-foot { display: flex; flex-direction: column; gap: 8px; }
.mp2-coach-sk .coach-foot .sk-bar { height: 12px; border-radius: 5px; }
/* The composer's rally chip lands in a row that is already there: 26px of it
   is held (a 118px stand-in) while the row is empty, so the composer does not
   grow when the chip comes (the row is hidden while empty everywhere else).
   The stand-in is drawn only while the rallies read is out: a match whose
   rallies failed or are none has no rally to suggest, and the row stays empty. */
.mp2-rail .coach-att:empty, .mp2-coach-sk .coach-att { display: flex; min-height: 26px; }
.match-detail:has(#rallies-card[aria-busy="true"]) .mp2-rail .coach-att:empty::before, .mp2-coach-sk .coach-att::before { content: ""; width: 118px; height: 26px; border-radius: 6px; background: var(--surface-3); animation: sk-breath 1.8s ease-in-out infinite; }
/* Reduced motion: the blocks stand still and the spinner is a still ring. */
@media (prefers-reduced-motion: reduce) {
  .mp2-sk, .mp2-spin i, .match-detail.mp2-player .sbs-main[inert] .mp2-time:empty,
  .match-detail:has(#rallies-card[aria-busy="true"]) .mp2-rail .coach-att:empty::before, .mp2-coach-sk .coach-att::before { animation: none; }
}
/* The strip on a phone or tablet (lsrn.14; README "Where the phone layout ends:
   1100px", and the mockup's phone): the chrome above, with what the phone
   drops. The caption keeps only the star and "…" (Share and Comment become
   rows of "…", decided 2026-10-27), and the control row loses the breadcrumb.
   The phone's own Rallies card rules (the pinned box and the
   sticky player, sbx-6fgy.4) are undone for the player: one dark block in the
   page flow, as on desktop, so a rally list under it would stick its header
   under the top bar alone (--rl-stick, as .pin-overflow does). At 720px and
   below it is full-bleed and square, as drawn. Keyed on the viewport and the
   server's .mp2 (lsrn.17), not on the .mp2-phone match-page-v2.js sets, so the
   first paint is already the phone's block; .mp2-phone keys only what that
   script builds (the menu rows here, the bar). */
.match-detail.mp2-player:not(.mp2-phone) .mp2-menu-item.mp2-phone-row { display: none; }
@media (max-width: 1100px) {
  .match-detail.mp2.mp2-player .mp2-cap { padding: 10px 12px 24px; }
  .match-detail.mp2.mp2-player .mp2-cap-title { font-size: 14px; }
  .match-detail.mp2.mp2-player .mp2-cap-tools > :is(#rally-now-share, .mp2-comment) { display: none; }
  /* The call control and Hidden points leave the strip bar for the call line
     under it (lsrn.42, prototypes/player-controls-phone/ section 7). */
  .match-detail.mp2.mp2-player .mp2-sbar :is(.mp2-callc, .mp2-vsep, .mp2-hidden) { display: none; }
  .match-detail.mp2.mp2-player .mp2-callline:not(.hidden) { display: flex; }
  .match-detail.mp2.mp2-player .mp2-sbar { padding: 6px 12px 8px; }
  .match-detail.mp2.mp2-player .mp2-strip { padding-left: 12px; padding-right: 12px; padding-bottom: 12px; overscroll-behavior-x: contain; }
  .match-detail.mp2.mp2-player .mp2-dd-menu { max-width: calc(100vw - 24px); }
  .match-detail.mp2.mp2-player #rallies-card:has(> .sbs-main:not(.hidden)) > .rallies-pin { display: contents; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main { position: relative; top: auto; z-index: auto; margin-top: 0; padding: 0; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main::before,
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main::after { content: none; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-report { display: block; }
  @media (max-width: 720px) {
    .match-detail.mp2.mp2-player #rallies-card { --rl-stick: 61px; }
    .match-detail.mp2.mp2-player #rallies-card > .sbs-main { margin: 0 -16px; border-radius: 0; }
    .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage { border-radius: 0 !important; }
  }
}
@media (max-width: 720px) {
  .match-detail.mp2.mp2-player .mp2-ctl { padding: 6px 8px; }
  /* The desktop row's pills (lsrn.24) dissolve into this one row: the groups
     draw nothing, their buttons keep the order they had (mute and full screen
     last), and what the phone has elsewhere (the rally's icons over the frame,
     Auto-play next and Report in "…") is not drawn here. The rally's pill
     hides only the flag: a focused star still in it as the width crosses
     720px stays focusable until the script moves it over the frame. */
  .match-detail.mp2.mp2-player .mp2-cg { display: contents; }
  .match-detail.mp2.mp2-player :is(.mp2-report, .mp2-dl, .mp2-ap, .mp2-skip, .mp2-prog > u, .mp2-tip) { display: none; }
  .match-detail.mp2.mp2-player .mp2-g-mute > *, .match-detail.mp2.mp2-player .mp2-g-end > .mp2-full { order: 1; }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-time { margin: 0 2px; white-space: nowrap; font-size: 11px; }
  .match-detail.mp2.mp2-player .mp2-ctl { gap: 2px; }
}
/* The phone's player, 720px and below (lsrn.38, prototypes/player-controls-phone/
   2, 3 and 5): the caption's star and "…" are 38px glass circles, previous /
   play / next sit in the frame's middle (the script moves the transport into
   .mp2-mid), the time and full screen are glass pills 10px in from the frame's
   foot, and the seek line runs above them with a 31px touch band, a knob that is
   always drawn and a time chip over the finger while it is dragged. The row
   itself (.mp2-ctl) keeps its place under the frame and holds only the owner's
   call control until lsrn.42 gives that a line of its own; the pills hang from
   its top edge. The tablet (721-1100px) takes the desktop row (lsrn.40, below). */
.match-detail.mp2-player .mp2-mid { display: none; }
.match-detail.mp2-player .mp2-menu-item.mp2-mute-row { display: none; }
@media (max-width: 720px) {
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage { position: relative; overflow: visible; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 104px; z-index: 1; pointer-events: none;
    background: linear-gradient(transparent, rgba(6,10,8,.34));
  }
  .match-detail.mp2.mp2-player .mp2-cap {
    z-index: 7; gap: 8px; padding: 10px 12px 30px;
    background: linear-gradient(rgba(6,10,8,.74) 0%, rgba(6,10,8,.42) 60%, transparent 100%);
  }
  .match-detail.mp2.mp2-player .mp2-cap-titles { flex: 1; }
  .match-detail.mp2.mp2-player :is(.mp2-cap-title, .mp2-cap-meta) {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 2px rgba(0,0,0,.5);
  }
  .match-detail.mp2.mp2-player .mp2-cap-meta { color: rgba(255,255,255,.84); }
  .match-detail.mp2.mp2-player .mp2-cap-tools { flex: none; margin: -4px -4px 0 0; }
  .match-detail.mp2.mp2-player .mp2-cap-tools :is(.icon-btn, .mp2-gb) {
    width: 38px; min-width: 38px; height: 38px; padding: 0; border-radius: 50%; color: #fff;
    background: rgba(16,20,18,.46); box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
    -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  }
  .match-detail.mp2.mp2-player .mp2-cap-tools :is(.icon-btn.on, .icon-btn[aria-pressed="true"]) { color: var(--pl-accent); }
  /* Previous, play, next: 44px glass circles 34px apart, play/pause 58px. */
  .match-detail.mp2.mp2-player .mp2-mid {
    display: flex; position: absolute; left: 0; right: 0; top: 50%; z-index: 4; justify-content: center;
    transform: translateY(-50%); pointer-events: none;
  }
  .match-detail.mp2.mp2-player .mp2-mid .mp2-cg { display: flex; align-items: center; gap: 34px; }
  .match-detail.mp2.mp2-player .mp2-mid .mp2-cg > :is(.icon-btn, .mp2-play) {
    width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; color: #fff; pointer-events: auto;
    display: grid; place-items: center; cursor: pointer; background: rgba(16,20,18,.42);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  }
  .match-detail.mp2.mp2-player .mp2-mid .mp2-cg > .mp2-play { width: 58px; height: 58px; }
  .match-detail.mp2.mp2-player .mp2-mid .mp2-cg > :is(.icon-btn, .mp2-play) > svg:not(.mp2-skip) { width: 22px; height: 22px; }
  .match-detail.mp2.mp2-player .mp2-mid .mp2-cg > .mp2-play > svg { width: 28px; height: 28px; }
  .match-detail.mp2.mp2-player .mp2-mid .icon-btn:disabled { opacity: .35; }
  .match-detail.mp2.mp2-player .mp2-mid :is(button):focus-visible { outline: 2px solid var(--pl-accent); outline-offset: 2px; }
  .match-detail.mp2.mp2-player .sbs-main[inert] .mp2-mid :is(.mp2-play, .icon-btn) { opacity: .35; pointer-events: none; }
  /* The seek line: its 3px end 59px up from the foot (62px to its top), 10px in
     from the sides; the row below is back where it was. */
  .match-detail.mp2.mp2-player .mp2-prog {
    margin: -62px 10px 59px; height: 3px; border-radius: 2px; background: rgba(255,255,255,.28);
    transition: height 120ms ease, margin 120ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-prog::before { left: -10px; right: -10px; top: -14px; bottom: -14px; }
  .match-detail.mp2.mp2-player .mp2-prog.drag { height: 5px; margin-top: -63px; margin-bottom: 58px; }
  .match-detail.mp2.mp2-player .mp2-prog > :is(i, u) { border-radius: 2px; }
  .match-detail.mp2.mp2-player .mp2-prog > u { display: block; position: absolute; inset: 0 auto 0 0; width: 0; background: rgba(255,255,255,.34); pointer-events: none; }
  .match-detail.mp2.mp2-player .mp2-prog > b {
    width: 14px; height: 14px; margin: -7px 0 0 -7px; opacity: 1; box-shadow: 0 0 0 2px rgba(6,10,8,.2); transition: transform 120ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-prog.drag > b { transform: scale(1.35); }
  /* The knob's shadow above would hide the desktop's focus ring (same weight, later). */
  .match-detail.mp2.mp2-player .mp2-prog:focus-visible > b { box-shadow: 0 0 0 3px rgba(157,212,176,.35); }
  .match-detail.mp2.mp2-player .mp2-prog > .mp2-tip {
    display: block; position: absolute; bottom: 22px; left: 0; transform: translateX(-50%); padding: 4px 9px; border-radius: 7px;
    background: rgba(16,20,18,.86); color: #fff; font: 13px var(--font-sans); font-feature-settings: "tnum"; white-space: nowrap;
    opacity: 0; pointer-events: none; transition: opacity 120ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-prog.drag > .mp2-tip { opacity: 1; }
  /* The row under the frame is empty. The time
     and full screen hang from its top edge, over the frame's foot. */
  .match-detail.mp2.mp2-player .mp2-ctl { position: relative; padding: 0; gap: 0; }
  .match-detail.mp2.mp2-player .mp2-ctl > .mp2-sp, .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-mute { display: none; }
  .match-detail.mp2.mp2-player .mp2-ctl :is(.mp2-g-time, .mp2-g-end) {
    display: inline-flex; align-items: center; position: absolute; bottom: calc(100% + 10px); z-index: 3; height: 40px; padding: 0 2px;
    border-radius: 999px; color: #fff; background: rgba(16,20,18,.46); box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
    -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3);
  }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-time { left: 10px; padding: 0 12px; }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-end { right: 10px; }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-time { margin: 0; font: 13px var(--font-sans); font-feature-settings: "tnum"; letter-spacing: .01em; }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-time > span { color: rgba(255,255,255,.72); }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-end .mp2-ib {
    width: 36px; height: 36px; padding: 0; border: 0; border-radius: 50%; background: none; color: #fff;
    display: inline-grid; place-items: center; cursor: pointer;
  }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-end .mp2-ib > svg { width: 20px; height: 20px; }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-end .mp2-ib:hover:not(:disabled) { background: rgba(255,255,255,.16); }
  /* "…": a menu from the button, 44px rows, which may run past the frame's foot. */
  .match-detail.mp2.mp2-player .mp2-menu {
    z-index: 8; min-width: 230px; padding: 6px; border-radius: 12px; background: var(--surface); color: var(--text);
    box-shadow: 0 16px 40px -12px rgba(14,19,17,.35), inset 0 0 0 1px var(--border-2);
    /* The menu sits on the page's tokens, not the picture's (lsrn.45). */
    --pl-text: var(--text); --pl-muted: var(--muted); --pl-accent: var(--accent); --pl-3: var(--surface-2);
  }
  .match-detail.mp2.mp2-player .mp2-menu-item { height: auto; min-height: 44px; padding: 0 12px; gap: 12px; border-radius: 8px; font-size: 14px; }
  .match-detail.mp2.mp2-player .mp2-menu-item .mp2-tick { display: none; }
  .match-detail.mp2.mp2-player .mp2-menu-item.mp2-mute-row { display: flex; }
  /* Auto-play next is a switch row, its state the page's #rallies-autoadvance. */
  .match-detail.mp2.mp2-player .mp2-menu-item.mp2-sw { position: relative; padding-right: 58px; }
  .match-detail.mp2.mp2-player .mp2-menu-item.mp2-sw::before {
    content: ""; position: absolute; right: 12px; top: 50%; width: 34px; height: 20px; margin-top: -10px; border-radius: 999px;
    background: var(--border-2); transition: background 150ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-menu-item.mp2-sw::after {
    content: ""; position: absolute; left: calc(100% - 44px); top: 50%; width: 16px; height: 16px; margin-top: -8px; border-radius: 50%;
    background: #fff; transition: left 150ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-menu-item.mp2-sw[aria-checked="true"]::before { background: var(--pl-accent); }
  .match-detail.mp2.mp2-player .mp2-menu-item.mp2-sw[aria-checked="true"]::after { left: calc(100% - 30px); }
  /* A row answers the finger, and a keyboard's focus shows on the light ground,
     where the hover tint alone is too faint to read. */
  .match-detail.mp2.mp2-player .mp2-menu-item:active { background: var(--pl-3); }
  .match-detail.mp2.mp2-player .mp2-menu-item:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: -2px; }
  /* Full screen is the dark block's: the menu and the note go back to the picture's
     palette there (lsrn.45 is the page's light, and full screen has no page). */
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen .mp2-menu {
    background: rgba(18,22,20,.96); color: inherit;
    -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
    box-shadow: 0 16px 40px -12px rgba(0,0,0,.6), inset 0 0 0 1px rgba(255,255,255,.08);
    --pl-text: inherit; --pl-muted: inherit; --pl-accent: inherit; --pl-3: inherit;
  }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen .mp2-menu-item.mp2-sw:not([aria-checked="true"])::before { background: rgba(255,255,255,.22); }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen .mp2-why {
    background: #263029; box-shadow: 0 0 0 1px var(--pl-line), 0 6px 18px rgba(0,0,0,.35); color: var(--pl-text);
  }
}
@media (max-width: 720px) and (prefers-reduced-motion: reduce) {
  .match-detail.mp2.mp2-player :is(.mp2-prog, .mp2-prog > b, .mp2-prog > .mp2-tip, .mp2-menu-item.mp2-sw::before, .mp2-menu-item.mp2-sw::after) { transition: none; }
}
/* Match page v2, the player shelf (lsrn.19, prototypes/rally-panel/ 1-3 and
   prototypes/match-page-v2-polish/ 6-8), above 1100px. The control row moves
   onto the video at the frame's foot, over a fade; the frame, the strip bar and
   the clips sit on one white card, the frame lifted by a shadow that falls down
   only. The --sh-* tokens are the card's palette: page tokens on the page, the
   dark block's in full screen, where the card is the whole screen. The
   palette and its colours apply at every width, after this block (lsrn.41). The
   strip's edges (fade, snap) are lsrn.23, the row's new contents lsrn.24. */
@media (min-width: 1101px) {
  /* The strip's edges (lsrn.23, prototypes/rally-panel/ 4): it runs to the
     card's edge (the card's 6px padding undone), a clip scrolling out fades over
     44px, only on a side with more to scroll (data-l / data-r, set by the
     script), and a viewer's scroll settles with a clip 14px in. The grouped
     strip's sticky game label sits at the left edge for good, so the label's
     18px band is exempt from the left fade: a second mask layer keeps it opaque
     up to the right-hand fade (prototypes/player-controls-phone/ 9). */
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) :is(.mp2-strip, .mp2-sk-strip) { margin: 0 -6px; }
  .match-detail.mp2.mp2-player .mp2-strip {
    --fl: 0px; --fr: 0px; --lb: 0px; padding-inline: 14px; scroll-padding-inline: 14px; scroll-snap-type: x proximity;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - var(--fr)), transparent 100%), linear-gradient(90deg, transparent 0, #000 var(--fl), #000 calc(100% - var(--fr)), transparent 100%);
    mask-image: linear-gradient(90deg, #000 calc(100% - var(--fr)), transparent 100%), linear-gradient(90deg, transparent 0, #000 var(--fl), #000 calc(100% - var(--fr)), transparent 100%);
    -webkit-mask-size: 100% var(--lb), 100% 100%; mask-size: 100% var(--lb), 100% 100%;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  }
  .match-detail.mp2.mp2-player .mp2-strip.grouped { --lb: 18px; }
  .match-detail.mp2.mp2-player .mp2-strip[data-l] { --fl: 44px; }
  .match-detail.mp2.mp2-player .mp2-strip[data-r] { --fr: 44px; }
  .match-detail.mp2.mp2-player .mp2-clip { scroll-snap-align: start; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main {
    box-sizing: border-box; padding: 6px 6px 0; border: 1px solid var(--border); border-radius: 18px;
    background: var(--sh-bg); color: var(--sh-ink); box-shadow: 0 1px 2px rgba(14,19,17,.06);
  }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen {
    padding: 0; border: 0; border-radius: 0; box-shadow: none;
  }
  /* "estimated" stays with Score off (polish 8): lsrn.19 overrides the script's
     .hidden toggle here; the toggle can go with its next edit. Not at 1100px and
     below, where the info button holds the note (f2pe.12). */
  .match-detail.mp2.mp2-player .mp2-est.hidden { display: inline !important; }
  /* The strip bar 10px under the frame, 14px in from the card's edge (6 + 8). */
  .match-detail.mp2.mp2-player .mp2-sbar,
  .match-detail.mp2.mp2-player .sbs-main[inert] .mp2-sk-bar { min-height: 46px; padding: 10px 8px 8px; border-top: 0; color: var(--sh-ink); }
  /* The strip on the shelf: its inline 14px is lsrn.23's, above. */
  .match-detail.mp2.mp2-player .mp2-strip { padding-block: 10px 14px; border-top: 0; scrollbar-color: var(--sh-line) transparent; }
  .match-detail.mp2.mp2-player .mp2-strip.mp2-under-bar { padding-top: 0; }
  .match-detail.mp2.mp2-player .sbs-main[inert] .mp2-sk-strip { padding: 0 14px 14px; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-comment { padding: 0 8px 14px; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-report { padding: 0 8px; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen :is(.mp2-sbar, .mp2-strip, .rally-comment, .rally-report) { padding-inline: 14px; }
  .match-detail.mp2.mp2-player .mp2-strip.mp2-map { --lb: 22px; }
}
/* The shelf's palette and the colours drawn from it, at every width (lsrn.19,
   lsrn.41). Above 1100px the shelf is the white card (--sh-bg its surface); at
   1100px and below there is no card: the bar and the clips sit on the page's own
   ground (prototypes/player-controls-phone/ 6, "Light"), so the same rules read
   from the page's tokens with white buttons and clips on the ground. In full
   screen the palette is the dark block's, at every width. */
.match-detail.mp2.mp2-player #rallies-card > .sbs-main {
  --sh-bg: var(--surface); --sh-ink: var(--text); --sh-ink2: var(--text-2); --sh-mute: var(--muted); --sh-faint: var(--muted-2);
  --sh-line: var(--border-2); --sh-ring: var(--border); --sh-clip: var(--surface-2); --sh-clip-h: var(--surface-3);
  --sh-btn: var(--surface); --sh-btn-h: var(--surface-2); --sh-on: var(--accent-soft); --sh-acc: var(--accent);
  --sh-sk: rgba(14,19,17,.07); --sh-tick: #fff;
}
@media (max-width: 1100px) {
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main {
    --sh-bg: var(--bg); --sh-clip: var(--surface); --sh-clip-h: var(--surface-2);
  }
  /* A menu stands off the ground: back to the card's white, so a pressed row in the
     game panel keeps its contrast. Not in full screen, whose palette is dark. */
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) :is(.mp2-dd-menu, .mp2-fmenu) {
    --sh-bg: var(--surface); --sh-clip: var(--surface-2); --sh-clip-h: var(--surface-3);
  }
}
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen {
  --sh-bg: var(--pl); --sh-ink: var(--pl-text); --sh-ink2: #c7cec9; --sh-mute: var(--pl-muted); --sh-faint: var(--pl-faint);
  --sh-line: var(--pl-line); --sh-ring: transparent; --sh-clip: var(--pl-2); --sh-clip-h: var(--pl-3);
  --sh-btn: var(--pl-3); --sh-btn-h: rgba(255,255,255,.2); --sh-on: rgba(157,212,176,.16); --sh-acc: var(--pl-accent);
  --sh-sk: rgba(255,255,255,.14); --sh-tick: #0b0f0d;
}
.match-detail.mp2.mp2-player .mp2-dd-btn {
  box-shadow: inset 0 0 0 1px var(--sh-line); background: var(--sh-btn); color: var(--sh-ink2);
}
.match-detail.mp2.mp2-player .mp2-dd-btn:hover,
.match-detail.mp2.mp2-player .mp2-dd.open > .mp2-dd-btn { background: var(--sh-btn-h); }
.match-detail.mp2.mp2-player .mp2-dd-menu { background: var(--sh-bg); box-shadow: 0 0 0 1px var(--sh-line), 0 18px 40px -12px rgba(14,19,17,.35); }
.match-detail.mp2.mp2-player .mp2-dd-row { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-dd-row.on { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-dd-row.on::before { color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-dd-row:is(:hover, :focus-visible) { background: var(--sh-clip); }
.match-detail.mp2.mp2-player .mp2-dd-row em { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-score { color: var(--sh-ink2); }
.match-detail.mp2.mp2-player .mp2-score input { border-color: var(--sh-faint); }
.match-detail.mp2.mp2-player .mp2-score input:checked { background: var(--sh-acc); border-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-score input:checked::after { border-color: var(--sh-tick); border-top: 0; border-right: 0; }
.match-detail.mp2.mp2-player .mp2-score input:focus-visible { outline-color: var(--sh-acc); }
/* Hidden points in the strip bar reads from the shelf like Score does (lsrn.64); its
   base border is the dark bar's white at 45%, pale on the white shelf. Not in full
   screen: that is the dark bar, which keeps the base rules. The call line has its
   own copy (.mp2-callline above). */
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar .mp2-hidden { color: var(--sh-ink2); }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar .mp2-hidden input { border-color: var(--sh-mute); }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar .mp2-hidden input:checked { background: var(--sh-acc); border-color: var(--sh-acc); }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar .mp2-hidden input:checked::after { border-color: var(--sh-tick); border-top: 0; border-right: 0; }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar .mp2-hidden input:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar .mp2-hidden em { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-est { color: var(--sh-mute); border-left-color: var(--sh-line); }
.match-detail.mp2.mp2-player .mp2-clip {
  background: var(--sh-clip); color: var(--sh-ink); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)), 0 0 0 1px var(--sh-ring);
}
.match-detail.mp2.mp2-player .mp2-clip:hover { background: var(--sh-clip-h); }
.match-detail.mp2.mp2-player .mp2-clip em { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-clip.on { background: var(--sh-on); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)), 0 0 0 1.5px var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-clip:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-clip b.sc sup { color: var(--sh-faint); }
.match-detail.mp2.mp2-player .mp2-clip b.n { color: var(--sh-ink2); }
.match-detail.mp2.mp2-player .mp2-gl { color: var(--sh-mute); border-bottom-color: var(--sh-line); }
.match-detail.mp2.mp2-player .mp2-gl > span { background: var(--sh-bg); }
/* First-paint stand-ins, on the shelf's ground and at the same boxes. */
.match-detail.mp2.mp2-player :is(.mp2-sk-bar, .mp2-sk-strip) .mp2-sk { background: var(--sh-sk); }
.match-detail.mp2.mp2-player .mp2-sk-gl { border-bottom-color: var(--sh-line); }
.match-detail.mp2.mp2-player .rally-comment textarea { background: var(--sh-clip); border-color: var(--sh-line); color: var(--sh-ink); }
.match-detail.mp2.mp2-player .rally-comment #rally-comment-status,
.match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-report,
.match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-report .rally-report-desc { color: var(--sh-mute); }
/* Edit games on the shelf (f2pe.4): the bar's buttons and the map take the
   card's palette, as the clips do; amber is the page's --warn on the light
   card and the player's in full screen. The map's label band is kept clear of
   the left fade, like the grouped strip's. */
.match-detail.mp2.mp2-player #rallies-card > .sbs-main { --sh-warn: var(--warn); }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen { --sh-warn: #f2b134; }
.match-detail.mp2.mp2-player :is(.mp2-pb, .mp2-edit-open) { background: var(--sh-btn); box-shadow: inset 0 0 0 1px var(--sh-line); color: var(--sh-ink2); }
.match-detail.mp2.mp2-player :is(.mp2-pb, .mp2-edit-open):hover:not(:disabled) { background: var(--sh-btn-h); }
.match-detail.mp2.mp2-player :is(.mp2-pb, .mp2-edit-open, .mp2-chk, .mp2-tile):focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-pb.q { background: none; box-shadow: none; color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-pb.q:hover:not(:disabled) { background: var(--sh-btn-h); color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-pb.go { background: var(--sh-acc); box-shadow: none; color: var(--sh-tick); }
.match-detail.mp2.mp2-player .mp2-pb.go:hover:not(:disabled) { background: var(--sh-acc); filter: brightness(1.1); }
.match-detail.mp2.mp2-player :is(.mp2-elab, .mp2-eshort) { color: var(--sh-acc); }
.match-detail.mp2.mp2-player :is(.mp2-elab, .mp2-eshort)::before { background: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-cnt { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-chk { color: var(--sh-warn); box-shadow: inset 0 0 0 1px var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-chk:hover { background: var(--sh-clip); }
.match-detail.mp2.mp2-player .mp2-conflict { color: var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-tl { color: var(--sh-mute); border-bottom-color: var(--gw, var(--sh-line)); }
.match-detail.mp2.mp2-player .mp2-tl.open { --gw: var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-tlb { background: var(--sh-bg); }
.match-detail.mp2.mp2-player .mp2-tlb.warn { color: var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-bang { background: var(--sh-warn); color: var(--sh-tick); }
.match-detail.mp2.mp2-player .mp2-gsc { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-tile {
  scroll-snap-align: start; background: var(--sh-clip); color: var(--sh-mute);
  box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)), 0 0 0 1px var(--sh-ring);
}
.match-detail.mp2.mp2-player .mp2-tile:hover { background: var(--sh-clip-h); }
.match-detail.mp2.mp2-player .mp2-tile b { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-tile b.n { color: var(--sh-ink2); }
.match-detail.mp2.mp2-player .mp2-tile small { color: var(--sh-faint); }
.match-detail.mp2.mp2-player .mp2-tile.on { background: var(--sh-on); box-shadow: inset 0 3px 0 var(--edge, var(--pl-u)), 0 0 0 1.5px var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-tile.out {
  background: repeating-linear-gradient(135deg, transparent 0 5px, var(--sh-clip) 5px 10px); box-shadow: inset 0 0 0 1px var(--sh-line);
}
.match-detail.mp2.mp2-player .mp2-tile.out.on { box-shadow: inset 0 0 0 1px var(--sh-line), 0 0 0 1.5px var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-tile .mp2-dot { background: var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-fmenu .mp2-callc { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-fmenu .mp2-callc-g button:not(.on) { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-fmenu .mp2-callc-g .mp2-callc-no { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-miss { background: transparent; }
.match-detail.mp2.mp2-player .mp2-miss:hover:not(:disabled) { background: var(--sh-clip-h); }
.match-detail.mp2.mp2-player .mp2-miss:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-miss b { color: var(--sh-ink2); }
.match-detail.mp2.mp2-player .mp2-miss b.n { color: var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-miss small { color: var(--sh-mute); }   /* the word says what the tile is: not the rally number's faint */
.match-detail.mp2.mp2-player .mp2-bnd::after { border-left-color: var(--sh-line); }
.match-detail.mp2.mp2-player .mp2-bnd.set::after { border-left-color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-bnd i { background: var(--sh-bg); box-shadow: 0 0 0 1px var(--sh-line); color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-bnd.set i { background: var(--sh-ink); color: var(--sh-bg); box-shadow: none; }
.match-detail.mp2.mp2-player .mp2-bnd:is(:hover, [aria-expanded="true"]):not(:disabled) i { background: var(--sh-acc); color: var(--sh-tick); }
.match-detail.mp2.mp2-player .mp2-bnd.mine i { box-shadow: 0 0 0 1.5px var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-bnd:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-seam:is(:hover, :focus-visible, [aria-expanded="true"]):not(:disabled)::after { border-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-seam:is(:hover, :focus-visible):not(:disabled)::before { background: var(--sh-acc); color: var(--sh-tick); }
.match-detail.mp2.mp2-player .mp2-fmenu { background: var(--sh-bg); box-shadow: 0 0 0 1px var(--sh-line), 0 18px 40px -12px rgba(14,19,17,.35); }
.match-detail.mp2.mp2-player .mp2-fmenu-hd { color: var(--sh-mute); }
.match-detail.mp2.mp2-player button.mp2-tlb:is(:hover, [aria-expanded="true"]):not(:disabled) { color: var(--sh-ink); }
.match-detail.mp2.mp2-player button.mp2-tlb:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-grow + .mp2-grow { border-top-color: var(--sh-line); }
.match-detail.mp2.mp2-player .mp2-grow > span { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-seg { box-shadow: inset 0 0 0 1px var(--sh-ring); }
.match-detail.mp2.mp2-player .mp2-seg button { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-seg button:hover:not(:disabled) { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-seg button.on { background: var(--sh-clip-h); color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-seg button:focus-visible { outline-color: var(--sh-acc); }
.match-detail.mp2.mp2-player .mp2-gnote { color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-gnote.warn { color: var(--sh-warn); }
.match-detail.mp2.mp2-player .mp2-giss { background: color-mix(in srgb, var(--sh-warn) 9%, transparent); }
.match-detail.mp2.mp2-player .mp2-gi1 p { color: var(--sh-ink); }
.match-detail.mp2.mp2-player .mp2-gi1.mp2-gstop p::before { background: var(--sh-warn); color: var(--sh-tick); }
/* The game panel as a bottom sheet (f2pe.14, prototypes/match-edit-phone-sheet/):
   the editor sets .mp2-sheet at 720px and below. The scrim covers the page's nav
   and bars. The rules in .mp2-gpan's rows and tokens stand; only the sizes differ. */
body.mp2-sheet-lock { overflow: hidden; }
/* A flick of the map still coasting when the sheet opens stops here; script can still
   scroll it, so the check button's own glide lands (lsrn.75). */
body.mp2-sheet-lock .match-detail.mp2.mp2-player .mp2-strip { overflow-x: hidden; }
.match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen:has(> .mp2-gscrim:not(.hidden)) { overflow: hidden; }
.match-detail.mp2.mp2-player .mp2-gscrim {
  position: fixed; inset: 0; z-index: 70; background: rgba(8,12,10,.42); touch-action: none;
  animation: mp2-scrim-in .22s ease-out;
}
/* Still takes presses while it fades: a double tap must not land on the page under it. */
.match-detail.mp2.mp2-player .mp2-gscrim.mp2-out { animation: mp2-scrim-out .22s ease-out forwards; }
.match-detail.mp2.mp2-player .mp2-gpan.mp2-sheet {
  position: fixed; left: 0; right: 0; top: auto; bottom: 0; z-index: 71; display: flex; flex-direction: column;
  width: auto; min-width: 0; max-height: calc(100vh - 56px); max-height: calc(100dvh - 56px); padding: 0; overflow: hidden;
  border-radius: 18px 18px 0 0; box-shadow: 0 0 0 1px var(--sh-line), 0 -12px 32px -12px rgba(14,19,17,.35);
  animation: mp2-sheet-in .3s cubic-bezier(.32,.72,0,1);
}
.match-detail.mp2.mp2-player .mp2-gpan.mp2-sheet.mp2-out { animation: mp2-sheet-out .3s cubic-bezier(.32,.72,0,1) forwards; }
@keyframes mp2-scrim-in { from { opacity: 0; } }
@keyframes mp2-scrim-out { to { opacity: 0; } }
@keyframes mp2-sheet-in { from { transform: translateY(105%); } }
@keyframes mp2-sheet-out { to { transform: translateY(105%); } }
.match-detail.mp2.mp2-player .mp2-sh-top { flex: none; position: relative; padding: 8px 52px 8px 16px; touch-action: none; cursor: grab; }
.match-detail.mp2.mp2-player .mp2-sh-top::before {
  content: ""; position: absolute; top: 6px; left: 50%; width: 36px; height: 4px; margin-left: -18px; border-radius: 2px; background: var(--sh-line);
}
.match-detail.mp2.mp2-player .mp2-sheet .mp2-sh-top .mp2-fmenu-hd { margin-top: 10px; padding: 0; line-height: 18px; }
.match-detail.mp2.mp2-player .mp2-sh-x {
  position: absolute; top: 6px; right: 6px; width: 44px; height: 44px; display: grid; place-items: center; padding: 0; border: 0;
  border-radius: 50%; background: none; color: var(--sh-mute); font-size: 14px; cursor: pointer;
}
.match-detail.mp2.mp2-player .mp2-sh-x span { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--sh-clip); }
.match-detail.mp2.mp2-player .mp2-sh-x:focus-visible { outline: 2px solid var(--sh-acc); outline-offset: -4px; }
.match-detail.mp2.mp2-player .mp2-sh-tiles { flex: none; position: relative; display: flex; gap: 12px; overflow-x: auto; padding: 2px 16px 10px; scrollbar-width: none; touch-action: pan-x; }
.match-detail.mp2.mp2-player .mp2-sh-tiles::-webkit-scrollbar { display: none; }
.match-detail.mp2.mp2-player .mp2-sh-tiles .mp2-tile.mp2-flash { animation: mp2-tile-flash .9s ease-out; }
@keyframes mp2-tile-flash { 0%, 45% { background: color-mix(in srgb, var(--sh-warn) 26%, var(--sh-clip)); } }
.match-detail.mp2.mp2-player .mp2-sh-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 8px calc(34px + env(safe-area-inset-bottom));
}
.match-detail.mp2.mp2-player .mp2-sheet .mp2-seg button { height: 40px; }
.match-detail.mp2.mp2-player .mp2-sheet .mp2-grow > span { line-height: 40px; }
@media (prefers-reduced-motion: reduce) {
  .match-detail.mp2.mp2-player :is(.mp2-gscrim, .mp2-gpan.mp2-sheet, .mp2-sh-tiles .mp2-tile.mp2-flash) { animation: none; }
}
/* The player at 1100px and below on the page's light ground (lsrn.41,
   prototypes/player-controls-phone/ 6 and 7): the frame alone is dark. Phone
   (720px and below): edge to edge, the bar, the call line and the clips on the
   ground under it. Tablet: the frame keeps 14px corners; its row (kept until
   lsrn.40 draws the desktop row there) stays a dark band joined to the frame
   so its white icons have a ground, and the bar and the clips go on the page
   with no card. The bar's 12px inset is the phone's, the strip runs to the
   page margins. Not in full screen, where the block is the whole screen. */
@media (max-width: 1100px) {
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) { background: none; color: var(--sh-ink); }
  .match-detail.mp2.mp2-player .mp2-sbar,
  .match-detail.mp2.mp2-player .sbs-main[inert] .mp2-sk-bar { border-top: 0; color: var(--sh-ink); padding: 10px 0 8px; }
  .match-detail.mp2.mp2-player .mp2-strip { margin: 0; padding: 10px 0 12px; border-top: 0; scrollbar-color: var(--sh-line) transparent; }
  .match-detail.mp2.mp2-player .mp2-strip.mp2-under-bar { padding-top: 0; }
  .match-detail.mp2.mp2-player .sbs-main[inert] .mp2-sk-strip { margin: 0; padding: 0 0 12px; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-comment { padding: 0 0 12px; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-report { padding: 0; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen :is(.mp2-sbar, .mp2-strip, .rally-comment, .rally-report) { padding-inline: 14px; margin-inline: 0; }
}
@media (max-width: 720px) {
  /* The frame is full-bleed; what sits under it is 12px in from the screen. */
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > :is(.mp2-sbar, .mp2-sk-bar, .mp2-strip, .mp2-sk-strip, .rally-comment, .rally-report) { padding-inline: 12px; }
}
/* The tablet takes the desktop row whole (lsrn.40, prototypes/player-controls-phone/ 8):
   from 721px the control row, the caption scrim and the frame fade are the desktop's
   (lsrn.24), with no "…" on the player and no transport in the frame's middle. The
   frame keeps 14px corners inside the page's margins. */
@media (min-width: 721px) {
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage {
    position: relative; border-radius: 13px !important;
    box-shadow: 0 10px 18px -10px rgba(14,19,17,.45), 0 2px 4px -2px rgba(14,19,17,.25);
  }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen > .rally-stage { border-radius: 0 !important; box-shadow: none; }
  /* The fade the control row reads on. */
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 104px; z-index: 1; pointer-events: none;
    background: linear-gradient(transparent, rgba(6,10,8,.34));
  }
  /* The caption: a darker scrim, white title, the mono line at 84% white. */
  .match-detail.mp2.mp2-player .mp2-cap { background: linear-gradient(rgba(6,10,8,.78) 0%, rgba(6,10,8,.5) 55%, transparent); }
  .match-detail.mp2.mp2-player .mp2-cap-title { text-shadow: 0 1px 2px rgba(0,0,0,.6); }
  .match-detail.mp2.mp2-player .mp2-cap-meta { color: rgba(255,255,255,.84); text-shadow: 0 1px 2px rgba(0,0,0,.6); }
  /* The control row on the video (lsrn.24, prototypes/player-controls/ 2-4):
     two groups on dark glass pills, a 12px step all round. The row is 64px
     (12 + 40 + 12) and ends at the frame's foot; the seek line's 3px sits with
     its centre 12px over the pills' top (65.5px up, 1.5px back down so the row
     starts where it did). Hovered, the line is 5px about the same centre. */
  .match-detail.mp2.mp2-player .mp2-prog {
    margin: -65.5px 12px -1.5px; height: 3px; border-radius: 2px; background: rgba(255,255,255,.28);
    transition: height 120ms ease, margin 120ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-prog.drag { height: 5px; margin-top: -66.5px; margin-bottom: -2.5px; }
  .match-detail.mp2.mp2-player .mp2-prog > i, .match-detail.mp2.mp2-player .mp2-prog > u { border-radius: 2px; }
  .match-detail.mp2.mp2-player .mp2-prog > u { display: block; position: absolute; inset: 0 auto 0 0; width: 0; background: rgba(255,255,255,.34); pointer-events: none; }
  .match-detail.mp2.mp2-player .mp2-prog > b {
    width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px; opacity: 1; box-shadow: 0 0 0 2px rgba(6,10,8,.2);
    transition: transform 120ms ease, box-shadow 120ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-prog.drag > b { transform: scale(1.15); }
  .match-detail.mp2.mp2-player .mp2-prog > .mp2-tip {
    display: block; position: absolute; bottom: 14px; left: 0; transform: translateX(-50%); padding: 3px 8px; border-radius: 6px;
    background: rgba(16,20,18,.82); color: #fff; font: 12px var(--font-sans); font-feature-settings: "tnum";
    white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 120ms ease;
  }
  .match-detail.mp2.mp2-player .mp2-prog.drag > .mp2-tip { opacity: 1; }
  .match-detail.mp2.mp2-player .mp2-ctl {
    position: relative; z-index: 2; color: #fff; gap: 8px; padding: 12px; font: 13px var(--font-sans);
    pointer-events: none;      /* the row's top half is under the seek line: only the pills take the pointer */
  }
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-sp { flex: 1; margin: 0; }
  .match-detail.mp2.mp2-player .mp2-cg {
    display: inline-flex; align-items: center; flex: none; height: 40px; padding: 0 2px; border-radius: 999px; color: #fff; pointer-events: auto;
    background: rgba(16,20,18,.46); box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
    -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3);
  }
  .match-detail.mp2.mp2-player .mp2-cg:empty { display: none; }
  .match-detail.mp2.mp2-player .mp2-cg :is(.mp2-ib, .icon-btn, .mp2-gb, .mp2-ap) {
    position: relative; flex: none; width: 36px; height: 36px; padding: 0; border: 0; border-radius: 50%;
    background: none; color: #fff; display: inline-grid; place-items: center; cursor: pointer;
  }
  .match-detail.mp2.mp2-player .mp2-cg :is(.icon-btn, .mp2-gb):disabled > svg { opacity: .35; }
  .match-detail.mp2.mp2-player .mp2-cg :is(.icon-btn.on, .icon-btn[aria-pressed="true"], .mp2-gb.on) { color: var(--pl-accent); }
  .match-detail.mp2.mp2-player .mp2-cg :is(.mp2-ib, .icon-btn, .mp2-gb) > svg:not(.mp2-skip) { width: 20px; height: 20px; }
  .match-detail.mp2.mp2-player .mp2-g-play .icon-btn > svg:not(.mp2-skip) { display: none; }
  .match-detail.mp2.mp2-player .mp2-cg .mp2-play { width: 40px; height: 40px; }
  .match-detail.mp2.mp2-player .mp2-cg .mp2-play > svg { width: 22px; height: 22px; }
  .match-detail.mp2.mp2-player .mp2-g-time { padding: 0 14px; }
  .match-detail.mp2.mp2-player .mp2-time { margin: 0; font-feature-settings: "tnum"; letter-spacing: .01em; white-space: nowrap; }
  .match-detail.mp2.mp2-player .mp2-time > span { color: rgba(255,255,255,.72); }
  /* Auto-play next: a switch with a play mark in its knob, knob right when on. */
  .match-detail.mp2.mp2-player .mp2-cg .mp2-ap { width: 52px; border-radius: 999px; }
  .match-detail.mp2.mp2-player .mp2-ap > i { position: relative; width: 34px; height: 14px; border-radius: 999px; background: rgba(255,255,255,.34); transition: background 150ms ease; }
  .match-detail.mp2.mp2-player .mp2-ap > i > b {
    position: absolute; top: 50%; left: -2px; width: 20px; height: 20px; margin-top: -10px; border-radius: 50%; background: #fff;
    display: grid; place-items: center; color: #0e1311; box-shadow: 0 1px 3px rgba(0,0,0,.35); transition: left 180ms cubic-bezier(.2,.7,.2,1);
  }
  .match-detail.mp2.mp2-player .mp2-ap[aria-checked="true"] > i { background: rgba(255,255,255,.62); }
  .match-detail.mp2.mp2-player .mp2-ap[aria-checked="true"] > i > b { left: 16px; }
  .match-detail.mp2.mp2-player .mp2-ctl :is(button):focus-visible { outline: 2px solid var(--pl-accent); outline-offset: -2px; }
  /* Tooltips (README 4): the button's name, 10px over it, centred; the row's
     two end buttons line it up with their outer edge so it stays in the frame. */
  .match-detail.mp2.mp2-player .mp2-cg [data-tt]:is(:hover, :focus-visible)::after {
    content: attr(data-tt); position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
    padding: 4px 8px; border-radius: 6px; background: rgba(16,20,18,.86); color: #fff; font: 12px/1.35 var(--font-sans);
    white-space: nowrap; pointer-events: none; z-index: 3;
  }
  .match-detail.mp2.mp2-player .mp2-ctl > .mp2-cg:first-child > [data-tt]:first-child:is(:hover, :focus-visible)::after { left: 0; transform: none; }
  .match-detail.mp2.mp2-player .mp2-ctl > .mp2-cg:last-child > [data-tt]:last-child:is(:hover, :focus-visible)::after { left: auto; right: 0; transform: none; }
  /* No "…" on the desktop row; the menu is the phone's. */
  .match-detail.mp2.mp2-player .mp2-more { display: none; }
  .match-detail.mp2.mp2-player .mp2-cap-tools:empty { display: none; }
  .match-detail.mp2.mp2-player .mp2-skip { display: none; }
  .match-detail.mp2.mp2-player .mp2-g-play .mp2-skip { display: block; }
}
/* What a hover does to the row, only where there is one: on a touch tablet :hover
   sticks to the last thing tapped, which would leave a button lit and the seek
   line fat with its time chip showing (lsrn.40). Above 1100px nothing changes. */
@media (min-width: 1101px), (min-width: 721px) and (hover: hover) {
  .match-detail.mp2.mp2-player .mp2-prog:hover { height: 5px; margin-top: -66.5px; margin-bottom: -2.5px; }
  .match-detail.mp2.mp2-player .mp2-prog:hover > b { transform: scale(1.15); }
  .match-detail.mp2.mp2-player .mp2-prog:hover > .mp2-tip { opacity: 1; }
  .match-detail.mp2.mp2-player .mp2-cg :is(.mp2-ib, .icon-btn, .mp2-gb, .mp2-ap):hover:not(:disabled) { background: rgba(255,255,255,.16); color: #fff; }
}
@media (min-width: 721px) and (max-width: 1100px) and (hover: none) {
  /* Over the page's own :hover: a tapped star keeps saying whether it is on. */
  .match-detail.mp2.mp2-player .mp2-cg :is(.mp2-ib, .icon-btn, .mp2-gb, .mp2-ap):hover:not(:disabled) { background: none; color: #fff; }
  .match-detail.mp2.mp2-player .mp2-cg :is(.icon-btn.on, .icon-btn[aria-pressed="true"], .mp2-gb.on):hover:not(:disabled) { color: var(--pl-accent); }
}
@media (min-width: 721px) and (max-width: 1100px) {
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .rally-stage { border-radius: 14px !important; }
  /* Score off takes "estimated" away and leaves its slot, so Score stays under the finger. */
  .match-detail.mp2.mp2-player .mp2-est.hidden { display: inline !important; visibility: hidden; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar { margin-top: 0; }
}
/* Out of the way while playing (lsrn.25, prototypes/player-controls/ section 5): on a
   desktop that hovers, the caption, the seek line and the row fade out and the
   pointer hides over the frame once it has been still over a playing rally
   (strip.js sets .mp2-idle). Opacity only: nothing leaves the layout or loses
   focus. The frame's foot fade goes with the row it is drawn for. After the
   lsrn.24 shelf above, so the seek line keeps its own height and margin easing
   next to the fade; every descendant drops the pointer, since the row's pills
   and the caption's tools take it back on their own (pointer-events: auto).
   At 1100px and below (lsrn.39, prototypes/player-controls-phone/ section 4) the
   same class is what a tap on the frame sets and clears, and the phone's middle
   transport fades with the rest; the pointer stays visible. */
@media (hover: hover) and (min-width: 1101px), (max-width: 1100px) {
  .match-detail.mp2.mp2-player :is(.mp2-cap, .mp2-ctl, .mp2-mid),
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage::after { transition: opacity .3s ease; }
  .match-detail.mp2.mp2-player .mp2-prog { transition: opacity .3s ease, height 120ms ease, margin 120ms ease; }
  .match-detail.mp2.mp2-player .sbs-main.mp2-idle :is(.mp2-cap, .mp2-prog, .mp2-ctl, .mp2-mid),
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main.mp2-idle > .rally-stage::after { opacity: 0; }
  .match-detail.mp2.mp2-player .sbs-main.mp2-idle :is(.mp2-cap, .mp2-prog, .mp2-ctl, .mp2-mid),
  .match-detail.mp2.mp2-player .sbs-main.mp2-idle :is(.mp2-cap, .mp2-prog, .mp2-ctl, .mp2-mid) * { pointer-events: none; }
}
@media (hover: hover) and (min-width: 1101px) {
  .match-detail.mp2.mp2-player .sbs-main.mp2-idle > .rally-stage { cursor: none; }
}
@media (hover: hover) and (min-width: 1101px) and (prefers-reduced-motion: reduce), (max-width: 1100px) and (prefers-reduced-motion: reduce) {
  .match-detail.mp2.mp2-player :is(.mp2-cap, .mp2-prog, .mp2-ctl, .mp2-mid),
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage::after { transition: none; }
}
/* "Ask about this rally" (lsrn.27, prototypes/coach-launcher/ 3): the rally's
   door to the coach, a glass pill of its own in the row between the spacer and
   the rally's icons. Desktop only, above 1100px: the tablet and phone rows are
   out of the spec's scope. Shown while the coach card is on screen (rail hidden
   or open), as the name row's "AI coach"; not in full screen, where the rail is
   not. It sits in .mp2-ctl, so it fades with the row. */
.match-detail.mp2.mp2-player .mp2-ctl .mp2-g-ask { display: none; }
.mp2-ask {
  display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px 0 4px; border: 0; border-radius: 999px;
  background: none; color: #fff; font: 500 13px var(--font-sans); white-space: nowrap; cursor: pointer;
}
.mp2-ask-mark { width: 28px; height: 28px; border-radius: 50%; background: var(--accent); display: grid; place-items: center; flex: none; }
.mp2-ask-mark::before { content: ""; width: 10px; height: 10px; background: #fff; clip-path: polygon(28% 16%, 28% 84%, 84% 50%); }
.mp2-ask:focus-visible { outline: 2px solid var(--pl-accent); outline-offset: -2px; }
@media (min-width: 1101px) {
  .match-detail.mp2.mp2-player:has(> .mp2-rail > #ai-coach:not(.hidden)) .mp2-ctl .mp2-g-ask { display: inline-flex; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:fullscreen .mp2-ctl .mp2-g-ask,
  /* strip.js fitAsk: where the row (the frame's width, rail open or a short window) cannot hold it, it is not drawn; it never shrinks to the mark */
  .match-detail.mp2.mp2-player .mp2-ctl .mp2-g-ask.mp2-ask-off { display: none !important; }
}
@media (min-width: 1101px) and (hover: hover) {
  .mp2-ask:hover { background: rgba(255,255,255,.16); }
}
/* No tooltips and no callout on a long press at 1100px and below (lsrn.39): the
   buttons keep their accessible names. The frame takes taps now: no flash over
   the whole of it, and two quick ones are two toggles, not a zoom. */
@media (max-width: 1100px) {
  .match-detail.mp2.mp2-player :is(.mp2-cap, .mp2-ctl, .mp2-mid, .mp2-prog) { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main > .rally-stage { -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
}
.md-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.seg {
  display: inline-flex;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 2px;
  gap: 1px;
}
.seg button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 500;
  padding: 5px 10px;
  border-radius: 6px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.seg button:hover { color: var(--text-2); }
.seg button.active {
  background: var(--surface-3);
  color: var(--text);
}
/* The compact three-way per-game winner toggle on an adjust-mode game header
 * (padel-website-pkah.7). It shares the .seg pill frame but presses on `.on`
 * (not `.active`): the ? state is a neutral fill, Team A / Team B take their
 * colour tokens — the same fills the strip's won segments use. */
.seg.seg-xs button { padding: 2px 7px; font-size: 11px; font-family: var(--font-mono); }
.seg.seg-xs button.on { background: var(--surface-3); color: var(--text); }
.seg.seg-xs button.on.a { background: color-mix(in oklab, var(--team-a) 22%, var(--surface)); color: var(--team-a); }
/* Team B's orange is a mark, not text (3.9:1 on white): its words take the
   colour darkened toward ink, 65% = #8d3f20, 5.2:1 on the 28% fill. */
.seg.seg-xs button.on.b { background: color-mix(in oklab, var(--team-b) 22%, var(--surface)); color: color-mix(in oklab, var(--team-b) 65%, var(--text)); }

.video-player {
  background: #000;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  overflow: hidden;
}
.video-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  overflow: hidden;
}
.video-stage video,
.video-stage .video-js {
  width: 100%;
  height: 100%;
  display: block;
}

.md-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 2px 6px;
  border-bottom: 1px solid var(--border);
  gap: 12px;
  flex-wrap: wrap;
}
/* The Rallies toolbar's Side by side / Wide switch (sbx-6fgy.3), a .seg pill
 * pair left of "Auto-play next". Only above 1100px is there a choice to make —
 * 721–1100px is the one stack, the phone its own layout — so below that it is
 * gone, not disabled. The pressed pill is the card's surface with the mockup's
 * thin shadow, so it reads as the raised half of the frame rather than the
 * muted .seg fill; the icons are the two frames from
 * prototypes/match-review-layout/mockup.html. */
.rally-layout-seg { display: none; }
@media (min-width: 1101px) {
  .rally-layout-seg { display: inline-flex; background: var(--surface-2); }
  .rally-layout-seg button { gap: 6px; }
  .rally-layout-seg button.active {
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(14, 19, 17, 0.10);
  }
}

.rally-grid {
  display: grid;
  gap: 14px;
  padding: 14px 0 0;
}
.rally-grid.d-compact {
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 8px;
}
.rally-grid.d-comfortable {
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.rally-grid.d-showcase {
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 18px;
}

.rally-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 120ms ease, transform 120ms ease;
  text-decoration: none;
  color: inherit;
}
.rally-card:hover { border-color: var(--border-2); }
.rally-card:hover .rc-play-hover { opacity: 1; }
.rally-card.active {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 0 24px var(--accent-soft);
}
.rally-card.selected { border-color: var(--accent); }

.rc-thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: #000;
}
.rc-thumb-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.4) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.6) 100%);
}
.rc-thumb-top {
  position: absolute;
  top: 6px; left: 8px; right: 8px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.rc-thumb-bot {
  position: absolute;
  bottom: 6px; right: 8px;
}
.rc-num {
  font-size: 10.5px;
  color: rgba(255, 255, 255, 0.95);
  background: rgba(0, 0, 0, 0.55);
  padding: 2px 5px;
  border-radius: 3px;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.rc-duration {
  font-size: 11px;
  color: #fff;
  background: rgba(0, 0, 0, 0.65);
  padding: 2px 6px;
  border-radius: 3px;
}
.rc-select {
  position: absolute;
  top: 6px; right: 8px;
  width: 18px; height: 18px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.5);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent-ink);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity 120ms ease;
}
.rally-card:hover .rc-select,
.rally-card.selected .rc-select { opacity: 1; }
.rc-select.checked {
  background: var(--accent);
  border-color: var(--accent);
}

.rc-play-hover {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.25);
  color: #fff;
  opacity: 0;
  transition: opacity 120ms ease;
  pointer-events: none;
}

.rc-meta { padding: 10px 12px 11px; }
.rc-meta-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.rc-time { font-size: 11.5px; color: var(--muted); }
.rc-frames { font-size: 11px; color: var(--muted); margin-top: 4px; }

/* Rally list view (alternative to rally-grid).
 *
 * Wrap pattern: a static column-label header sits OUTSIDE the scroll
 * container so it's always visible without needing position: sticky
 * (which had cross-browser quirks with the flex column layout). */
/* View-mode switcher (guided review): a row of mode pills over the rally
 * player, plus a one-line description of the active mode. Each pill shows the
 * mode name and a count/duration hint. */
.view-modes {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin: 4px 0 6px;
}
.view-mode {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-2);
  border-radius: var(--radius);
  padding: 8px 14px;
  cursor: pointer;
  font-family: var(--font-sans);
  transition: border-color 120ms ease, background 120ms ease;
}
.view-mode:hover { border-color: var(--border-2); }
.view-mode .vm-name { font-size: 13px; font-weight: 550; }
.view-mode .vm-hint {
  font-size: 11px;
  color: var(--muted-2);
  font-variant-numeric: tabular-nums;
}
.view-mode.active {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--text);
}
.view-mode.active .vm-hint { color: var(--muted); }
.view-mode-desc {
  font-size: 12.5px;
  color: var(--muted);
  margin: 6px 2px 14px;
}

/* ── Match-structure strip and grouping (padel-website-pkah.3) ──────────────
 * The read-only sets/games/tiebreaks overlay on the rally list: the .flow strip
 * of set tiles above the view-mode pills, the .rl-set / .rl-game group headers
 * inside the list, and the per-row .rl-of position. Ported from
 * prototypes/match-structure/mockup.html; adjust-mode, score and winner styles
 * are left out (their own beads). New classes only — the shared .chip / .eyebrow
 * primitives are untouched (ADR-054). */
.flow { margin: 2px 0 14px; }
.flow-sets { display: flex; gap: 6px; align-items: stretch; }
.flow-set {
  appearance: none; text-align: left; min-width: 0;
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px 9px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-2);
  border-radius: var(--radius); cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.flow-set:hover { border-color: var(--border-2); }
.flow-set.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--text); }
.fs-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex-wrap: wrap; }
.fs-name { font-size: 13px; font-weight: 600; white-space: nowrap; }
/* The set score on a tile (padel-website-pkah.6): the read-only text, the two
 * adjust-mode inputs (labelled by pair via aria-label — the score is positional,
 * ADR-048 — never the strip's Team A/B), and the "score says N games · M marked"
 * reconciliation line. Ported from prototypes/match-structure/mockup.html. */
.fs-score { font-family: var(--font-mono); font-size: 13px; font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }
.fs-score.none { color: var(--muted-2); }
.fs-score-edit { display: inline-flex; align-items: center; gap: 4px; }
.fs-score-sep { color: var(--muted); }
.fs-score-input {
  width: 34px; height: 24px; padding: 0 6px;
  font-family: var(--font-mono); font-size: 12.5px; text-align: center;
  border: 1px solid var(--border-2); border-radius: 6px;
  background: var(--surface); color: var(--text);
  -moz-appearance: textfield; appearance: textfield;
}
.fs-score-input::-webkit-outer-spin-button, .fs-score-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.fs-mismatch { font-size: 11px; color: var(--warn); }
.fs-games { display: flex; gap: 2px; height: 18px; }
.fg {
  display: flex; align-items: center; justify-content: center;
  min-width: 4px; height: 100%; border-radius: 3px;
  background: var(--surface-3); border: 1px solid var(--border-2);
  color: var(--muted); font-family: var(--font-mono); font-size: 10px;
  overflow: hidden; cursor: pointer; transition: filter 120ms ease;
}
.fg:hover { filter: brightness(0.93); border-color: var(--muted-2); }
/* A game the owner marked won: the segment fills with the team's colour token so
 * a decided game reads at a glance in the strip (padel-website-pkah.7). Declared
 * BEFORE .fg.short so the amber short-game border below still wins — a flagged
 * won game keeps its flag rather than hiding it under the fill. */
.fg.w0 { background: color-mix(in oklab, var(--team-a) 28%, var(--surface)); border-color: color-mix(in oklab, var(--team-a) 45%, var(--border-2)); color: var(--team-a); }
.fg.w1 { background: color-mix(in oklab, var(--team-b) 28%, var(--surface)); border-color: color-mix(in oklab, var(--team-b) 45%, var(--border-2)); color: color-mix(in oklab, var(--team-b) 65%, var(--text)); }
/* A game a reader can check without serve data (fewer than 4 rallies, or a
 * tiebreak under 7 points). The only per-game flag the read-only strip shows. */
.fg.short { border-color: var(--warn); }
.fs-meta {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.flow-set.on .fs-meta { color: var(--muted); }

/* The winner key under the strip (padel-website-pkah.7): a swatch and label per
 * team, rendered only once a game has been marked won. The swatches reuse the
 * .fg.w0 / .fg.w1 team fills so the strip and its key track the theme together. */
.flow-legend {
  display: flex; gap: 16px; margin-top: 8px;
  font-size: 11px; color: var(--muted);
}
.legend { display: inline-flex; align-items: center; gap: 5px; }
.legend i {
  display: inline-block; width: 14px; height: 9px; border-radius: 2px;
  border: 1px solid var(--border-2); background: var(--surface-3);
}
.legend i.a { background: color-mix(in oklab, var(--team-a) 28%, var(--surface)); border-color: color-mix(in oklab, var(--team-a) 45%, var(--border-2)); }
.legend i.b { background: color-mix(in oklab, var(--team-b) 28%, var(--surface)); border-color: color-mix(in oklab, var(--team-b) 45%, var(--border-2)); }

/* Set / game headers inside the rally-list scroller. The GAME header is the
 * sticky one — the current game stays pinned below the table header while its
 * rallies scroll under it (the set context stays in the now bar's "Set N ·
 * Game M" line). The set header scrolls normally, marking each set's start. */
.rl-set {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 8px; background: var(--surface);
  border-bottom: 1px solid var(--border); border-top: 2px solid var(--border-2);
  font-size: 13px;
}
.rl-set:first-child { border-top: 0; }
.rl-set .nm { font-weight: 600; color: var(--text); }
.rl-game {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 6px 8px 6px 20px;
  background: color-mix(in oklab, var(--surface) 55%, var(--bg-2));
  border-bottom: 1px solid var(--border); font-size: 12.5px;
}
.rl-game .nm { font-weight: 550; color: var(--text-2); }
.rl-set .meta, .rl-game .meta {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The running games tally on a game header ("games a–b" / "a–b before"), off the
 * owner's per-game winners (padel-website-pkah.7). Shown in both modes. */
.rl-game .tally {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.rl-game .tally b { color: var(--text); font-weight: 600; }
/* Header flag pill (short game, off-count set). Reader-checkable only — no
 * serve-derived tag reaches the page. */
.tag {
  display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
  border-radius: 999px; font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.02em; color: var(--muted);
  border: 1px solid var(--border-2); background: var(--surface);
}
.tag.warn { color: var(--warn); border-color: color-mix(in oklab, var(--warn) 45%, var(--border-2)); }
/* "i/k" — the rally's place in its game — between Shots and Comment. */
.rl-of {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted-2);
  width: 40px; flex-shrink: 0;
}
.rl-row.active .rl-of { color: var(--accent); }
/* At phone widths the tiles stack (no page-level horizontal scroll) and the
 * narrow "i/k" column drops, matching the rally table's other mono columns. */
@media (max-width: 720px) {
  .flow-sets { flex-direction: column; }
  .flow-set { flex-basis: auto !important; }
  .rl-of { display: none; }
}

/* ── Adjust mode: edit the sets and games (padel-website-pkah.5) ────────────
 * Ported from prototypes/match-structure/mockup.html. The .adjusting class on
 * #rallies-card reveals the adjust bar, the header tools and the per-row split
 * controls (and gives the row actions' slot to the splits); a boundary-source
 * tag marks where each set/game boundary came from. Per-game winner and set
 * score controls are their own beads (pkah.7 / pkah.6). */
.adjust-bar {
  display: none; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 12px; padding: 10px 12px; border-radius: var(--radius);
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  font-size: 12.5px; color: var(--text-2);
}
.adjusting .adjust-bar { display: flex; }
.adjust-bar .sp { flex: 1; }
.tag.detected { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
.rl-set .tools, .rl-game .tools {
  margin-left: auto; display: none;
  align-items: center; gap: 6px; flex-wrap: wrap;
}
.adjusting .rl-set .tools, .adjusting .rl-game .tools { display: flex; }
.lnk {
  appearance: none; border: 0; background: none; padding: 2px 4px;
  font-size: 11.5px; color: var(--accent); cursor: pointer; border-radius: 4px;
}
.lnk:hover { background: var(--accent-soft); }
.lnk.quiet { color: var(--muted); }
.rl-row .split { display: none; gap: 4px; margin-left: auto; flex-shrink: 0; }
.adjusting .rl-row .split { display: flex; }
.adjusting .rl-row .rl-actions { display: none; }
.split .lnk { border: 1px dashed var(--border-2); padding: 2px 7px; color: var(--text-2); }
.split .lnk:hover { border-color: var(--accent); color: var(--accent); }

/* Rank badge on rally rows in Longest mode (1 = longest). */
.rl-rank {
  font-size: 10px;
  color: var(--accent);
  border: 1px solid var(--accent-line);
  border-radius: 999px;
  padding: 0 5px;
  margin-left: 4px;
  font-family: var(--font-sans);
  vertical-align: middle;
}

/* ---- Match at a glance: stat tiles + shots-per-rally histogram (8ze.2) ----
   Four equal tiles plus a wider histogram cell. When the match has no shot data
   the histogram is omitted and .no-hist collapses the reserved 5th column. */
.stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr) minmax(230px, 1.6fr);
  gap: 10px;
}
.stats.no-hist { grid-template-columns: repeat(4, 1fr); }
.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.stat .label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-2);
}
.stat .value {
  font-size: 22px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.stat .sub { font-size: 11.5px; color: var(--muted); }

.hist {
  grid-column: 5;
  gap: 8px;
}
.hist-bars {
  display: flex;
  align-items: flex-end;
  gap: 14px;
  height: 76px;
  padding-top: 14px;
}
.hbar {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  height: 100%;
}
.hbar .pct {
  font-size: 11.5px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.hbar .bar {
  width: 100%;
  max-width: 44px;
  background: var(--data-ink);
  border-radius: 4px 4px 0 0;
  opacity: 0.85;
  transition: opacity 0.15s;
}
.hbar:hover .bar { opacity: 1; }
.hist-labels {
  display: flex;
  gap: 14px;
  border-top: 1px solid var(--border);
  padding-top: 5px;
}
.hist-labels span {
  flex: 1;
  text-align: center;
  font-size: 10.5px;
  color: var(--muted-2);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 760px) {
  .stats, .stats.no-hist { grid-template-columns: repeat(2, 1fr); }
  .hist { grid-column: 1 / -1; }
}
/* Above 1100px v2's column can be narrower than the window says (lsrn.47). */
@media (min-width: 1101px) {
  .match-detail.mp2 #stats-block { container: stats-block / inline-size; }
  @container stats-block (max-width: 640px) {
    .stats, .stats.no-hist { grid-template-columns: repeat(2, 1fr); }
    .hist { grid-column: 1 / -1; }
  }
}

/* ---- Player analytics section (gvtk.2) -----------------------------------
   Compact one-row-of-four on desktop; lifted from prototypes/player-analytics/
   mockup.html. Rendered by static/js/player-analytics.js from the verified
   match.json summary; hidden until it loads, absent on a 404. Zone-bar and
   swatch colours are color-mix() of the --team-a/--team-b tokens so they track
   the theme with no re-render. */
.pa-card { padding: 18px 20px 24px; }
.pa-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* "Rallies 1–5 excluded: different players on court" — the pipeline left a
   run of rallies out of every number on the cards (another lineup was on
   court). Muted and beside the eyebrow; wraps under it on a narrow card. */
.pa-note { font-size: 12px; color: var(--muted); }

.pa-teams {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px 18px;
  margin-top: 18px;
}
.pa-team-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.team-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-weight: 600;
  font-size: 13px;
}
.team-badge .sw { width: 10px; height: 10px; border-radius: 3px; }
.pa-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 1100px) { .pa-teams { grid-template-columns: 1fr; } }
/* Above 1100px v2's column can be narrower than the window says (lsrn.47): the
   column's own width decides, so a player card never drops under the ~200px
   its three-up numbers need. */
@media (min-width: 1101px) {
  .match-detail.mp2 .pa-card { container: pa-card / inline-size; }
  @container pa-card (max-width: 920px) { .pa-teams { grid-template-columns: 1fr; } }
}
@media (max-width: 560px) { .pa-pair { grid-template-columns: 1fr; } }
.pl-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-1);
  padding: 13px 15px;
  color: var(--text);
  /* Size container for the speed-label swap below — the card's width, not the
     viewport, decides when "Top speed" runs out of room. */
  container-type: inline-size;
}
.pl-head { display: flex; align-items: center; justify-content: center; gap: 10px; }
.pl-photo {
  width: 30px;
  height: 56px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border-2);
  background: var(--surface-3);
}
.pl-name { font-size: 14.5px; font-weight: 600; }
.pl-sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
/* "This is me" (h21z.6): the card's claim row under "started left/right" — a
   ghost button, the viewer's "You" chip, another player's name chip, or the
   warn chip asking to reconfirm after a rerun. Drawn only on the owner's and a
   member's match page; the shared page never gets the claims to draw it. */
/* min-height is the ghost button's, so a card showing a chip keeps its stats
   level with its neighbour's. */
.pl-claim {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px;
  margin-top: 6px; min-height: 28px;
}
/* inline-block, not the chip's inline-flex, so a long name ellipsises. */
.pl-claim .chip {
  display: inline-block; line-height: 22px; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pl-claim-act { cursor: pointer; }
.pl-nums { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 12px; }
/* The second stat row — Shots / Winners / Errors — under a divider, when the
   match carries BOTH the hitter shot counts and the shot-outcomes aggregate. Same
   3-column grid as .pl-nums (no container query needed now that both rows are
   3-across); a top border sets it off as its own group. */
.pl-outcomes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
/* Stat cells centre in both rows, matching the centred bar legends —
   without this the six numbers were the only left-aligned things on the card. */
.pl-num { text-align: center; }
/* The explicit line-height is load-bearing: the "Top ⚡" swap below clamps the
   emoji span to this exact line box so the bolt (whose glyph is taller than
   10px text) cannot stretch its label line and push the speed value below the
   other two — the misalignment this whole mechanism exists to prevent. */
.pl-num .lbl { font-size: 10px; line-height: 12px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted-2); }
.pl-num .val { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.25; }
.pl-num .val small { font-size: 10.5px; font-weight: 400; color: var(--muted); font-family: var(--font-mono); margin-left: 3px; }
/* The speed label's two variants (markup in paPlayerCardHtml): "Top speed" on a
   wide card, "Top ⚡" once the card is narrow enough that the text would wrap
   and drop its number below Distance/Sprints. The query measures the card's
   CONTENT box (inline-size containers do; the 30px of .pl-card padding is
   outside it): "TOP SPEED" is ~62px, so three columns + two 10px gaps wrap
   below ~207px of content — 220px swaps just above that (~250px outer). The
   wide span goes sr-only rather than display:none so the accessible name stays
   "Top speed" at every width; the narrow span is an inline-block clamped to the
   .lbl line box so the taller emoji glyph cannot re-break the number line. */
.pl-lbl-narrow { display: none; }
@container (max-width: 220px) {
  .pl-lbl-wide {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .pl-lbl-narrow { display: inline-block; height: 12px; line-height: 12px; }
}
.pl-zones { margin-top: 12px; }
/* A proportional-bar label flagged data-label="wide" (12-17%, paBarSegmentHtml)
   fits only a wide card. Measured on the card's content box, headless Chrome
   2026-09-28: in Geist a 3-glyph percent clips below ~228px of content (up to
   15% at 168px); in the fallback sans (before Geist loads, or if it is blocked)
   below ~250px (up to 17%). The narrowest real layouts give ~194-208px (two
   cards per row just above the 560px / 1100px breakpoints). 250px / 18% covers
   both fonts. Hidden here, the value stays
   in the legend and the tooltip — never shown half-cut. */
@container (max-width: 250px) {
  .zbar span[data-label="wide"], .sbar span[data-label="wide"] { font-size: 0; }
}
/* Proportional segments: each span's width is its inline flex, set by
   paBarSegmentHtml — this rule must not set flex. Legend: the shared .sleg row. */
.zbar { display: flex; gap: 2px; height: 20px; border-radius: 5px; overflow: hidden; }
.zbar span {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-width: 0;
  overflow: hidden;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: #0e1311;
}
/* A label inside a zone or Player DNA bar takes ink or white, whichever has
   the higher contrast on its segment, with no halo (prototypes/palette). The
   choice is per fill, so paBarSegmentHtml marks the white ones: on the light
   theme that is Team A's attack (#1157a4, 7.2:1) and Overhead (#7f3a72,
   7.6:1); every other segment reads ink at 4.8:1 or more. */
.zbar span[data-ink="white"], .sbar span[data-ink="white"] { color: #fff; }
/* The dark theme (unshipped) keeps its own near-black ink and halo. */
[data-theme="dark"] .zbar span {
  color: rgba(0, 0, 0, 0.82);
  text-shadow: 0 0 2px rgba(255, 255, 255, 0.5), 0 1px 1px rgba(255, 255, 255, 0.3);
}

/* ---- Player DNA (sbx-619) -------------------------------------------------
   The stroke-mix block below the zone bar: a titled caption, a PROPORTIONAL
   shot-mix bar (Ground / Volley / Overhead / Lob — segment width is the share,
   like the zone bar above; lobs are taken out of Ground and Volley so nothing
   counts twice), and a centred legend of the raw counts. */
.pl-strokes { margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--border); }
.st-caption { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 6px; }
.st-caption .t { color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase; }
.sbar { display: flex; gap: 2px; height: 22px; border-radius: 5px; overflow: hidden; }
.sbar span {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-width: 0;
  overflow: hidden;
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: #0e1311;
}
.sleg { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 14px; margin-top: 8px; }
.sleg span { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: var(--text-2); }
.sleg i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; }
.sleg b { font-weight: 500; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); }
/* The DNA legend has four items: a centred 2x2 grid, so Lob never wraps alone
   onto a third line. Columns size to their widest item, so the swatches align. */
.sleg-dna { display: grid; grid-template-columns: repeat(2, max-content); justify-content: center; }
/* On a wide card the default 14px column gap bunches the grid in the middle;
   spread it out there, and keep it tight where the card has no room to spare. */
@container (min-width: 280px) {
  .sleg-dna { column-gap: 36px; }
}
/* "Groundstroke 122" is the widest item: on the narrowest real card (~178px of
   content) the default gaps push the grid ~10px past the content edge, so
   tighten both the column gap and the swatch-to-label gap there. */
@container (max-width: 220px) {
  .sleg-dna { column-gap: 6px; }
  .sleg-dna span { gap: 3px; }
}

/* ---- Performance score (m5f1.1) -------------------------------------------
   One step line per player under the player cards: +1 per winner, -1 per error.
   Lifted from prototypes/performance-over-time/mockup.html; drawn by
   static/js/performance-score.js. Colour is the team token; the second partner
   is dashed. Strokes ride inline style="stroke:var(...)" so a theme flip
   repaints with no re-render. */
.ps-card { padding: 18px 20px 20px; }
.ps-title { font-size: var(--fs-head); font-weight: 600; margin: 8px 0 2px; }
.ps-sub { font-size: var(--fs-sub); color: var(--muted); }
.ps-legend { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 14px 0 6px; }
.ps-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 3px 9px 3px 4px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--text-2); font: inherit; font-size: 12.5px;
  cursor: pointer;
}
.ps-chip:hover { border-color: var(--border-2); }
.ps-chip[aria-pressed="true"] { border-color: var(--text-2); }
.ps-chip img, .ps-chip .ps-face {
  width: 20px; height: 20px; border-radius: 999px; object-fit: cover; flex: none;
  background: var(--surface-2);
}
.ps-chip b { font-family: var(--font-mono); font-weight: 500; color: var(--text); }
.ps-chip svg { flex: none; }
.ps-gap { width: 10px; }
.ps-plot { position: relative; }
.ps-plot > svg { display: block; width: 100%; overflow: visible; }
.ps-plot text { font-family: var(--font-mono); font-size: 10px; fill: var(--muted); }
.ps-plot .ps-setlbl { text-transform: uppercase; letter-spacing: .06em; }
.ps-wash { fill: var(--surface-2); }
.ps-divider { stroke: var(--border-2); }
.ps-grid { stroke: var(--border); }
.ps-zero { stroke: var(--muted-2); }
.ps-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; transition: opacity .15s; }
.ps-leader { fill: none; stroke-width: 1.5; }
.ps-ring { fill: none; stroke-width: 2; }
.ps-dim .ps-line:not(.ps-on), .ps-dim .ps-end:not(.ps-on) { opacity: .18; }
.ps-dim .ps-line.ps-on { stroke-width: 2.5; }
.ps-plot .ps-end text { font-size: 11px; fill: var(--text); font-weight: 500; }
@media (max-width: 520px) {
  .ps-card { padding: 16px 14px 18px; }
  .ps-chip { font-size: 12px; }
}

/* ---- Playtomic match card (p0i.3) ----------------------------------------
   Two states in one card: the link prompt until a match is imported, the match
   itself afterwards. The scoreboard is a real table so the set columns line up
   across both teams and screen readers get the row/column headers. */
.pt-card {
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.pt-where {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 14px;
  color: var(--text-2);
}
.pt-sep { color: var(--muted-2); }

.pt-board-table { border-collapse: collapse; font-size: 13.5px; }
.pt-board-table th,
.pt-board-table td { text-align: left; padding: 5px 0; }
.pt-board-table thead th {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--muted-2);
}
.pt-board-table tbody th {
  font-weight: 500;
  color: var(--text-2);
  padding-right: 22px;
  white-space: normal;
}
.pt-board-table td {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  min-width: 30px;
  padding-right: 12px;
}
/* Colour alone would not carry "who won" — the row also gets a "Won" chip. */
.pt-board-table tr.pt-won th,
.pt-board-table tr.pt-won td { color: var(--text); font-weight: 600; }
.pt-win { margin-left: 8px; height: 19px; padding: 0 7px; font-size: 10px; }

/* A player's Playtomic level, suffixed to their name. Subordinate on purpose:
   the line-up is read as names, the level is the detail hanging off one. The
   explicit colour and weight also hold it back inside a winning row, where the
   row rule bolds and brightens everything it inherits into.
   --muted, not the --muted-2 the labels around it use: at this size --muted-2
   is ~3.1:1 on the card in both themes, under the 4.5:1 AA floor. The labels
   get away with it because they are chrome that repeats what the layout
   already says — the level is the content, and unreadable means absent.
   The space before the number lives in the text, not in a margin, so it
   survives a copy-paste (see ptNames). */
.pt-level {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 400;
  color: var(--muted);
}

.pt-line { display: flex; flex-wrap: wrap; gap: 10px; font-size: 13.5px; color: var(--text-2); }
.pt-line-label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-2);
  padding-top: 3px;
}

/* ---- Match details as the compact scoreboard card (lsrn.28) ---------------
   Match page v2 on a desktop only: video-detail.js puts .pt-b2 on #pt-card
   when the server drew .mp2 and the viewport is above 1100px, and takes it off
   for the form (spec prototypes/match-details-v3/, sections 1 and 2). Nothing
   here reaches v1 or v2 at 1100px and below. The eyebrow row, the club-and-date
   line and "Edit" are today's card's and stay out of sight under the class. */
.pt-card.pt-b2 {
  position: relative;
  padding: 12px 16px;
  border-radius: 16px;
  gap: 0;
}
.pt-card.pt-b2 > .row,
.pt-card.pt-b2 .pt-where { display: none; }
.pt-b2-shelf {
  display: grid;
  grid-template-columns: auto 1px minmax(0, 1fr);
  gap: 0 24px;
  align-items: stretch;
  padding-right: 40px; /* the pencil's column: the facts never run under it */
}
.pt-b2-board { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
.pt-b2-rule { background: var(--border); }
.pt-b2-table { border-collapse: separate; border-spacing: 4px; margin: -4px; }
.pt-b2-table th,
.pt-b2-table td { padding: 0; text-align: left; font-weight: 400; }
.pt-b2-table thead th {
  height: 14px;
  width: 32px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--muted-2);
}
.pt-b2-table tbody th {
  height: 36px;
  padding-right: 18px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text-2);
  white-space: nowrap;
}
.pt-b2-table tr.pt-won th { color: var(--text); }
.pt-b2-table td { width: 32px; height: 32px; }
.pt-b2-box {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--muted-2);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.pt-b2-box.hi {
  background: var(--accent-soft);
  color: var(--text);
  font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
/* "won": the soft pill of the header's "✓ ready", without its outline. */
.pt-b2-won {
  display: inline-flex;
  align-items: center;
  height: 20px;
  margin-left: 8px;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  text-transform: lowercase;
  vertical-align: 1px;
}
.pt-b2-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, max-content));
  gap: 8px 40px;
  align-content: center;
  margin: 0;
  min-width: 0;
}
.pt-b2-facts > div { display: flex; flex-direction: column; min-width: 0; }
.pt-b2-facts dt {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 16px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted-2);
}
.pt-b2-facts dd {
  margin: 0;
  max-width: 240px;
  font-size: 13.5px;
  line-height: 20px;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pt-b2-facts dd a {
  color: var(--text-2);
  text-decoration: none;
  border-bottom: 1px solid var(--border-2);
}
.pt-b2-facts dd a:hover { color: var(--text); border-color: var(--text-2); }
/* v2's column can be narrower than the board and the facts side by side
   (lsrn.47): the facts go under the board, and a long name wraps. */
.pt-card.pt-b2 #pt-board { container: pt-board / inline-size; }
@container pt-board (max-width: 800px) {
  .pt-b2-shelf { grid-template-columns: minmax(0, 1fr); gap: 12px 0; }
  .pt-b2-rule { height: 1px; }
  .pt-b2-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pt-b2-table tbody th { white-space: normal; }
}
.pt-b2-edit {
  position: absolute;
  top: 10px;
  right: 10px;
  border-radius: 50%;
  border-color: transparent;
  background: transparent;
  color: var(--muted);
}
.pt-b2-edit:hover { background: var(--surface-3); color: var(--text); border-color: transparent; }

/* Which pair is Team A in the video (lsrn.37, prototypes/team-link/). The disc
   is the strip's team colour with its letter, before a pair's names; a button
   for the owner and members, who change the answer through it. */
.pt-b2-disc {
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  margin-right: 8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  color: #fff;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  vertical-align: 1px;
  flex: none;
}
.pt-b2-disc-a { background: var(--team-a); }
.pt-b2-disc-b { background: var(--team-b); }
button.pt-b2-disc { cursor: pointer; transition: transform 0.12s var(--ease, ease); }
button.pt-b2-disc:hover { transform: scale(1.12); }
.pt-b2-tl-ask {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: flex-start;
  height: 24px;
  margin: 2px 0 0 -6px;
  padding: 0 8px 0 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
}
.pt-b2-tl-ask:hover { background: var(--accent-soft); }
.pt-b2-tl-dots { display: inline-flex; }
.pt-b2-tl-dots i { width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--surface); }
.pt-b2-tl-dots i:first-child { background: var(--team-a); }
.pt-b2-tl-dots i:last-child { background: var(--team-b); margin-left: -3px; }
.pt-b2-tlc {
  position: absolute;
  z-index: 8;
  width: 340px;
  max-width: calc(100% - 8px);
  padding: 12px;
  border-radius: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 18px 40px -16px rgba(14, 19, 17, 0.32), 0 2px 6px -2px rgba(14, 19, 17, 0.12);
  color: var(--text);
}
.pt-b2-tlc-label {
  margin-bottom: 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-2);
}
.pt-b2-tlc-who {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  padding: 8px;
  border-radius: 10px;
  background: var(--surface-2);
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--text-2);
}
.pt-b2-tlc-crops { display: flex; flex: none; gap: 4px; }
.pt-b2-tlc-crops img {
  width: 30px;
  height: 40px;
  border-radius: 7px;
  object-fit: cover;
  box-shadow: 0 0 0 2px var(--team-a);
}
.pt-b2-tlc-opt {
  appearance: none;
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 38px;
  padding: 0 10px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text-2);
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}
.pt-b2-tlc-opt:hover { background: var(--surface-2); color: var(--text); }
.pt-b2-tlc-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--muted);
}
.pt-b2-tlc-clear {
  appearance: none;
  margin-left: auto;
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.pt-b2-tlc-clear:hover { text-decoration: underline; }

/* Nothing known, for the owner: a dashed slot the height of a button. */
.pt-card.pt-b2.pt-b2-slot {
  padding: 0;
  background: transparent;
  box-shadow: none;
  border: 1.5px dashed var(--border-2);
}
.pt-b2-slot-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 45px;
  padding: 0 8px 0 16px;
  color: var(--muted);
  font-size: 13.5px;
}
.pt-b2-glyph { display: inline-flex; flex: none; color: var(--muted-2); }
.pt-b2-pill {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  margin-left: auto;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}
.pt-b2-pill:hover { background: var(--border-2); }

/* ---- Match details edited in place in the compact card (lsrn.29) ---------
   Spec prototypes/match-details-v3/ section 3. The pencil or the slot's pill
   puts .pt-b2-editing on #pt-card (matchCardEditShape, video-detail-card.js), and
   these rules lay the shared form (_match_form.html, ADR-083) out as the
   card's own board and facts column. CSS only, over the same nodes and ids:
     - #up-hand-block becomes display: contents, so its date, players, rule and
       club are placed on #up-match's grid next to the Playtomic lines;
     - #up-players is the board: the two .up-player-row are its rows, and the
       score row between them in the DOM spans both, because each .up-set
       already stacks the first pair's box over the second's. The set numbers
       are a CSS counter on .up-set, so a folded set has none;
     - the facts' labels and the offer's "Added manually" wording are spans
       the form ships hidden everywhere else (.up-b2-lb, .up-pt-manual).
   The upload card never carries .pt-b2-editing, so none of this reaches it. */
.up-b2-lb,
.up-pt-manual { display: none; }
.pt-b2-editing .up-match {
  display: grid;
  grid-template-columns: max-content 1px max-content minmax(0, 1fr);
  column-gap: 24px;
  row-gap: 0;
  align-items: start;
  margin-top: 0;
}
.pt-b2-editing #up-hand-block { display: contents; }
.pt-b2-editing .up-b2-lb {
  display: block;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 16px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted-2);
}
/* The board: names, then the set boxes under bare numbers, then the round "+". */
.pt-b2-editing #up-players {
  grid-column: 1;
  grid-row: 1 / 6;
  align-self: center;
  display: grid;
  grid-template-columns: max-content max-content;
  grid-template-rows: 16px 32px 32px;
  gap: 4px 12px;
}
.pt-b2-editing #up-players > .pt-field-label,
.pt-b2-editing .up-player-add,
.pt-b2-editing .up-scorebar > .up-score-side:first-child,
.pt-b2-editing #up-score-locked > .pt-field-label { display: none; }
.pt-b2-editing .up-player-row {
  grid-column: 1;
  grid-row: 2;
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: 140px;
  gap: 6px;
}
.pt-b2-editing .up-scorebar ~ .up-player-row { grid-row: 3; }
.pt-b2-editing .up-player-row .pt-input,
.pt-b2-editing #up-club,
.pt-b2-editing #up-date,
.pt-b2-editing #up-url {
  height: 32px;
  padding: 0 10px;
  border-radius: 8px;
  border-color: var(--border-2);
}
.pt-b2-editing #up-scorebar {
  grid-column: 2;
  grid-row: 1 / 4;
  align-items: flex-end;
  gap: 8px;
}
.pt-b2-editing .up-sets { counter-reset: up-set; align-items: flex-start; gap: 4px; }
.pt-b2-editing .up-set { counter-increment: up-set; }
.pt-b2-editing .up-set::before {
  content: counter(up-set);
  height: 16px;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 16px;
  letter-spacing: 0.06em;
  text-align: center;
  color: var(--muted-2);
}
.pt-b2-editing .up-sc {
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 8px;
  border-color: var(--border-2);
}
/* The "+" is centred on the two rows of boxes, not on the numbers above them.
   Its words ("+ set") stay its accessible name; the glyph is drawn. */
.pt-b2-editing .up-scorebar > .up-score-side:last-child {
  flex: none;
  height: 68px;
  display: flex;
  align-items: center;
}
.pt-b2-editing #up-set-add {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px dashed var(--border-2);
  border-radius: 50%;
  color: var(--muted-2);
  font-size: 0;
  text-decoration: none;
}
.pt-b2-editing #up-set-add::before {
  content: "";
  width: 12px;
  height: 12px;
  background:
    linear-gradient(currentColor, currentColor) center / 12px 2px no-repeat,
    linear-gradient(currentColor, currentColor) center / 2px 12px no-repeat;
}
.pt-b2-editing #up-set-add:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
.pt-b2-editing #up-score-locked { grid-column: 2; grid-row: 1 / 4; align-self: center; }
/* The rule between the board and the facts. */
.pt-b2-editing .up-rule { grid-column: 2; grid-row: 1 / 6; align-self: stretch; width: 1px; height: auto; }
/* The facts: Club and Date side by side, Source under them across both. The
   club is a field from the start ("Where you played"), not "+ Add club": the
   suggestion list under it replaces the Recent chips on v2 (lsrn.30). */
.pt-b2-editing .up-club { grid-column: 3; grid-row: 1 / 3; gap: 0; }
.pt-b2-editing #up-club-add,
.pt-b2-editing #up-club-picks { display: none; }
.pt-b2-editing #up-club-field.hidden { display: flex !important; }
.pt-b2-editing #up-club { width: 190px; }
.pt-b2-editing #up-hand-block > .up-b2-lb { grid-column: 4; grid-row: 1; }
.pt-b2-editing #up-date-line,
.pt-b2-editing #up-date-field {
  grid-column: 4;
  grid-row: 2;
  display: flex;
  flex-direction: row;
  align-items: center;
  min-height: 32px;
}
.pt-b2-editing #up-date { width: 120px; font-family: var(--font-mono); }
/* No start time on the card (an imported one still rides along on Save). */
.pt-b2-editing #up-date-time { display: none; }
.pt-b2-editing #up-date-live { grid-column: 3 / 5; grid-row: 3; margin-top: 6px; }
.pt-b2-editing .up-match > .up-b2-lb { grid-column: 3 / 5; grid-row: 4; margin-top: 8px; }
.pt-b2-editing #up-pt-offer,
.pt-b2-editing #up-pt-prov,
.pt-b2-editing #up-pt-field { grid-column: 3 / 5; grid-row: 5; min-height: 20px; }
.pt-b2-editing #up-pt-field { gap: 6px; padding-top: 2px; }
.pt-b2-editing.pt-b2-known .up-pt-manual { display: inline; }
.pt-b2-editing.pt-b2-known .up-pt-ask { display: none; }
/* While Playtomic import is parked (no .pt-import-on, app.js) the offer line
   is an import control and hides with the rest. The Source fact keeps what is
   not one: "Added manually" for a match that holds something, as the card's
   view says it; with nothing known there is nothing to say and the label goes
   too. */
/* :not(.hidden): the override lifts the import gate only. A row holding a
   Playtomic match shows its prov line, and showPtLine hides the offer with
   .hidden, which this rule (an id and !important) would otherwise outrank. */
html:not(.pt-import-on) .pt-b2-editing.pt-b2-known #up-pt-offer:not(.hidden) { display: flex !important; }
html:not(.pt-import-on) .pt-b2-editing #up-pt-offer > :not(:first-child) { display: none; }
html:not(.pt-import-on) .pt-b2-editing:not(.pt-b2-known) .up-match > .up-b2-lb { display: none; }
.pt-b2-editing #up-pt-field .pt-field-label {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.pt-b2-editing #up-url { width: min(380px, 100%); }
.pt-b2-editing #up-match-error { grid-column: 1 / -1; grid-row: 6; margin-top: 8px; }
/* Save and Cancel as pills on a foot row at the card's right, Cancel first. */
.pt-b2-editing .up-foot { grid-column: 1 / -1; grid-row: 7; justify-self: end; margin-top: 10px; }
.pt-b2-editing .up-foot .btn {
  height: 32px;
  padding: 0 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
}
.pt-b2-editing #up-cancel { order: -1; background: var(--surface-3); color: var(--text); }
.pt-b2-editing #up-cancel:hover { background: var(--border-2); }
/* The editor takes focus when it opens (showMatchForm), so that Tab goes on
   into its fields; the card's own border already says where it is. */
.pt-b2-editing #pt-editor { container: pt-edit / inline-size; }
.pt-b2-editing #pt-editor:focus { outline: none; }
/* A narrow desktop card (the coach rail open below about 1360px) has no room
   for Club and Date side by side next to the board: the spec's row needs about
   820px. The facts stack in one column instead, Club, Date, Source, and the
   name fields narrow, so the card never runs under the rail with its Save. */
@container pt-edit (max-width: 819px) {
  .pt-b2-editing .up-match { grid-template-columns: max-content 1px minmax(0, 1fr); }
  .pt-b2-editing #up-players,
  .pt-b2-editing .up-rule { grid-row: 1 / 8; }
  .pt-b2-editing .up-player-row { grid-auto-columns: 112px; }
  .pt-b2-editing #up-club { width: min(190px, 100%); }
  .pt-b2-editing #up-hand-block > .up-b2-lb { grid-column: 3; grid-row: 3; margin-top: 8px; }
  .pt-b2-editing #up-date-line,
  .pt-b2-editing #up-date-field { grid-column: 3; grid-row: 4; }
  .pt-b2-editing #up-date-live { grid-column: 3; grid-row: 5; }
  .pt-b2-editing .up-match > .up-b2-lb { grid-column: 3; grid-row: 6; }
  .pt-b2-editing #up-pt-offer,
  .pt-b2-editing #up-pt-prov,
  .pt-b2-editing #up-pt-field { grid-column: 3; grid-row: 7; }
  .pt-b2-editing #up-match-error { grid-row: 8; }
  .pt-b2-editing .up-foot { grid-row: 9; }
}
/* A crowded board, a stored line-up of more than two a side or a fourth set,
   is wider than any facts column beside it can share: at any width the facts
   go under the board, one column, and the name fields share what the set
   boxes leave them. Rare by construction (padel is two against two, best of
   three), so it is drawn plainly rather than to the spec's row. */
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) {
  grid-template-columns: minmax(0, 1fr);
}
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-players {
  grid-column: 1;
  grid-row: 1;
  grid-template-columns: minmax(0, max-content) max-content;
}
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) .up-player-row { grid-auto-columns: minmax(0, 140px); }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) .up-rule { display: none; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) .up-club { grid-column: 1; grid-row: 2; margin-top: 12px; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-club { width: min(190px, 100%); }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-hand-block > .up-b2-lb { grid-column: 1; grid-row: 3; margin-top: 8px; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-date-line,
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-date-field { grid-column: 1; grid-row: 4; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-date-live { grid-column: 1; grid-row: 5; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) > .up-b2-lb { grid-column: 1; grid-row: 6; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-pt-offer,
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-pt-prov,
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-pt-field { grid-column: 1; grid-row: 7; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) #up-match-error { grid-row: 8; }
.pt-b2-editing .up-match:has(.up-player-extra:not(.hidden), #up-set4:not(.hidden)) .up-foot { grid-row: 9; }
/* The same layout when v2's column is too narrow for a facts column beside the
   board at all (lsrn.47): what the window's height leaves on a short screen. */
@container pt-edit (max-width: 620px) {
  .pt-b2-editing .up-match { grid-template-columns: minmax(0, 1fr); }
  .pt-b2-editing #up-players { grid-column: 1; grid-row: 1; grid-template-columns: minmax(0, max-content) max-content; }
  .pt-b2-editing .up-player-row { grid-auto-columns: minmax(0, 140px); }
  .pt-b2-editing .up-rule { display: none; }
  .pt-b2-editing .up-club { grid-column: 1; grid-row: 2; margin-top: 12px; }
  .pt-b2-editing #up-hand-block > .up-b2-lb { grid-column: 1; grid-row: 3; margin-top: 8px; }
  .pt-b2-editing #up-date-line,
  .pt-b2-editing #up-date-field { grid-column: 1; grid-row: 4; }
  .pt-b2-editing #up-date-live { grid-column: 1; grid-row: 5; }
  .pt-b2-editing .up-match > .up-b2-lb { grid-column: 1; grid-row: 6; }
  .pt-b2-editing #up-pt-offer,
  .pt-b2-editing #up-pt-prov,
  .pt-b2-editing #up-pt-field { grid-column: 1; grid-row: 7; }
}

/* The match form's own lines. ADR-054 takes the three that are type — the
   hint, the error and the field label — onto the scale; the input keeps its
   16px where touch is primary, which is a threshold rather than a size (below
   it, iOS Safari zooms the page on focus), and takes the body step on fine
   pointers — see the media rule below. All four are shared with
   video_detail.html, which draws the same form: this is one component at one
   size per pointer class on two pages, not a sweep of the video-detail
   screen. */
.pt-hint { margin: 0; font-size: var(--fs-sub); color: var(--muted); }
.pt-input {
  flex: 1 1 320px;
  min-width: 200px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  /* 16px: anything smaller makes iOS Safari zoom the page on focus. */
  font-size: 16px;
}
.pt-input:focus { outline: none; border-color: var(--accent-line); }
/* The 16px above is a threshold, not a choice — and the threshold only exists
   where focus can zoom. On a device whose primary input is a fine pointer
   that hovers (a desktop, not any touch-primary phone or tablet), no focus
   zoom exists, and 16px fields were the largest text on a card whose prose is
   13.5px — the form shouted over the screen it sits in. So the pair takes the
   body step there and keeps the 16px everywhere touch is primary. Both boxes
   together, or the score digits render bigger than the names beside them.
   Same rule on video detail, which draws the same form — it is one component
   at one size per pointer class, not two. */
@media (hover: hover) and (pointer: fine) {
  .pt-input,
  .up-sc { font-size: var(--fs-body); }
}
.pt-error { margin: 0; font-size: var(--fs-sub); color: var(--danger); }

/* ---- Hand editor (p0i.4) --------------------------------------------------
   The same card in edit mode. Two pair columns, because the set scores are
   attached to a SIDE rather than to the names typed into it — the pair heading
   shows them so the owner can see which side they are filling in. */
.pt-edit { display: flex; flex-direction: column; gap: 16px; }
.pt-fields { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; }
.pt-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.pt-field-grow { flex: 1 1 220px; }
.pt-field-label {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-2);
}
/* .pt-input carries `flex: 1 1 320px` for the full-width link box, which is a
   ROW. Inside a labelled field — a column — that same grow factor stretches
   the input to the height of the tallest field instead, and its 200px floor
   would overflow a phone inside a pair column. Overridden per context, because
   both defaults are right for the link box they were written for. */
.pt-field .pt-input { flex: 0 0 auto; width: 100%; min-width: 0; }
.pt-pairs { display: flex; flex-wrap: wrap; gap: 16px; }
.pt-pair { flex: 1 1 220px; display: flex; flex-direction: column; gap: 7px; }
.pt-pair-rows { display: flex; flex-direction: column; gap: 7px; }
.pt-pair-score {
  margin-left: 8px;
  color: var(--muted);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.pt-player-row { display: flex; align-items: center; gap: 6px; }
.pt-player-row .pt-input { flex: 1 1 auto; min-width: 0; }
.pt-add-player { align-self: flex-start; }
.pt-winner { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
.pt-radio {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-2);
  cursor: pointer;
}
.pt-radio input { accent-color: var(--accent); }

.rally-list-wrap {
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-2);
}
.rally-list-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 8px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.rally-list-head .rl-num,
.rally-list-head .rl-time,
.rally-list-head .rl-length,
.rally-list-head .rl-shots,
.rally-list-head .rl-comment-h,
.rally-list-head .rl-actions-h {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.rl-actions-h {
  margin-left: auto;
}
.rally-list {
  max-height: clamp(420px, 65vh, 780px);
  overflow-y: auto;
}
.rl-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 8px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  cursor: pointer;
  transition: background 120ms ease;
  color: inherit;
  text-decoration: none;
}
.rl-row:last-child { border-bottom: 0; }
/* A game section (.rl-gsec) wraps a game header with its rows so the sticky
 * game header is bounded by its game and the next game's header replaces it
 * (flat sticky siblings would stack). The wrapper is layout-neutral; only
 * restore the row divider its :last-child rule drops before the next header,
 * and re-drop it for the very last row in the list. In adjust mode the header
 * is static (the owner is editing, not scroll-watching). */
.rl-gsec .rl-row:last-child { border-bottom: 1px solid var(--border); }
.rl-gsec:last-child .rl-row:last-child { border-bottom: 0; }
.adjusting .rl-game { position: static; }
.rl-row:hover { background: var(--surface); }
.rl-row.active {
  background: var(--accent-soft);
}
.rl-num {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--muted);
  width: 56px;
  flex-shrink: 0;
}
.rl-time {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-2);
  width: 96px;
  flex-shrink: 0;
}
.rl-length {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-2);
  width: 76px;
  flex-shrink: 0;
}
.rl-shots {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-2);
  width: 56px;
  flex-shrink: 0;
}
/* "Not computed" reads as a dimmed dash, distinct from a real 0. */
.rl-shots.empty { color: var(--muted); }
.rl-row.active .rl-num,
.rl-row.active .rl-time,
.rl-row.active .rl-length,
.rl-row.active .rl-shots { color: var(--accent); }
.rl-actions {
  display: flex;
  gap: 4px;
  margin-left: auto;
  flex-shrink: 0;
}
/* Hidden rallies stay in the owner's list (still clickable) but read as
 * de-emphasized; the data columns dim while the action buttons stay legible. */
.rl-row.hidden-rally .rl-num,
.rl-row.hidden-rally .rl-time,
.rl-row.hidden-rally .rl-length,
.rl-row.hidden-rally .rl-shots,
.rl-row.hidden-rally .rl-comment-cell { opacity: 0.4; }
.rl-row.hidden-rally .rl-hide { color: var(--accent); }
/* Favourite star (row action + now-playing bar). Muted like any icon-btn until
 * starred, then reads in the accent color; the filled glyph is set on the SVG.
 * Keep the accent on hover so a starred rally doesn't lose its signal. */
.rl-fav.on,
.rl-fav.on:hover { color: var(--accent); }
/* Comment text fills the row's free space and truncates; the full note is
 * available on hover (title attr) and in the editor below the player. */
.rl-comment-cell {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12.5px;
  color: var(--text-2);
}
.rl-comment-cell.empty::before {
  content: "—";
  color: var(--muted);
}
/* Link mode (ADR-090) has no Comment column: the template leaves the header
 * out and the shared rallies route never returns a note, so the row's cell
 * would be an unlabelled column of dashes. .rl-actions keeps its auto margin. */
[data-viewer="link"] .rl-comment-cell { display: none; }
.rl-comment-h {
  flex: 1;
  min-width: 0;
}

/* Per-rally comment editor, between the player and the rally list. */
.rally-comment {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-top: 12px;
}
.rally-comment textarea {
  flex: 1;
  resize: none;
  overflow: hidden;
  min-height: 38px;
  padding: 9px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  line-height: 1.4;
}
.rally-comment textarea:focus {
  outline: none;
  border-color: var(--border-2);
}
.rally-comment #rally-comment-status {
  font-size: 12px;
  white-space: nowrap;
  padding-top: 11px;
}

/* Quiet per-rally problem reporting, below the comment box. Collapsed to a
 * single trigger by default; expands to per-dimension toggles + descriptions.
 * Panel open state is sticky across rally switches (see setReportPanelOpen). */
.rally-report {
  margin-top: 8px;
}
.rally-report-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 4px 4px 2px;
  background: transparent;
  border: 0;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: color 120ms ease;
}
.rally-report-trigger:hover { color: var(--text-2); }
/* Selected rally has a problem reported: tint the collapsed trigger so its
 * status reads without expanding the panel. */
.rally-report-trigger.has-flags {
  color: color-mix(in oklab, var(--danger) 70%, var(--text-2));
}
/* Preserve hover feedback on an already-flagged trigger (else .has-flags,
 * later in source order at equal specificity, would swallow the :hover). */
.rally-report-trigger.has-flags:hover {
  color: color-mix(in oklab, var(--danger) 45%, var(--text));
}
.rally-report-trigger .rr-chevron { transition: transform 150ms ease; }
.rally-report-trigger.open .rr-chevron { transform: rotate(180deg); }

/* Keyboard-hint row under the report trigger (sbx-6fgy.5): the arrow / Space / F
 * hints for the keys rally-keys.js installs. The chips reuse the shared .kbd
 * glyph pill. Hidden at 720px and below — the mobile stack has no keyboard, and
 * that breakpoint is also where the now-bar's own touch controls take over. */
.rally-kbd-hints {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 14px;
  font-size: 12px;
  color: var(--muted);
}
.rk-hint {
  display: inline-flex;
  gap: 5px;
  align-items: center;
}
@media (max-width: 720px) {
  .rally-kbd-hints { display: none; }
}
.rally-report-panel {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
  padding-left: 2px;
}
.rally-report-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.rally-report-desc {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.35;
}
.rally-report-toggle {
  font-size: 12px;
  line-height: 1;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.rally-report-toggle:hover {
  color: var(--text-2);
  border-color: var(--border-2);
}
.rally-report-toggle.on {
  color: var(--text);
  border-color: color-mix(in oklab, var(--danger) 45%, var(--border));
  background: color-mix(in oklab, var(--danger) 10%, transparent);
}

/* Muted marker on a flagged rally row. Lives inside the number cell so it
 * adds no column; hidden until the rally carries a flag. */
.rl-flag {
  display: none;
  width: 5px;
  height: 5px;
  margin-left: 6px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--danger) 55%, var(--muted));
  vertical-align: middle;
}
.rl-row.rl-flagged .rl-flag {
  display: inline-block;
}

/* Full-match collapsible (match + shared pages). Hidden by default; the
 * rally view is the primary view, the full match is opt-in. */
.full-match-section {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.full-match-toggle {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 14px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
  text-align: left;
}
.full-match-toggle:hover { border-color: var(--border-2); background: var(--surface-2); }
.full-match-toggle svg { color: var(--accent); flex-shrink: 0; }

/* Inline rally player block (under primary player) */
.rally-now {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: var(--bg-2);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  color: var(--text-2);
}
.rally-now .chip { background: var(--surface); }
/* The now-playing line is segments (renderNowPlaying, match-review-layout.js)
 * joined by a CSS dot, so the phone can drop a segment with its dot. */
#rally-now-playing { font-size: 12px; }
.rn-seg + .rn-seg::before { content: " · "; }

/* ── Match review: "Side by side" layout above 1100px (sbx-6fgy.2) ──────────
 * Above the site's two-column threshold the Rallies card lays the player and
 * the rally list beside each other instead of stacking. The mode pills, their
 * description and the match-structure strip / adjust bar still span the card;
 * below them a grid puts the now bar, stage, comment field, report trigger and
 * the keyboard-hint row in a left column (.sbs-main) and the list in a fixed
 * 384px right column (prototypes/match-review-layout, README decision 1).
 *
 * The grid is the card itself: since 6fgy.4 the list is the card's second box
 * (outside .rallies-pin and #rallies-player-wrap, which the phone needs), so
 * the two wrappers between the card and its columns are display:contents here
 * and every descendant lands on the card's grid. The toolbar is not one of the
 * player's rows — it sat outside the grid before — so it cancels the row gap
 * after itself and the stack under it measures exactly as before.
 *
 * Both columns share a bottom edge: the left column sets the grid row height
 * (its content is taller than the list's min-content), and .rally-list-wrap —
 * stretched to that row — is a flex column whose scroller (.rally-list,
 * flex:1 min-height:0) contributes ~0 to intrinsic sizing, so the list never
 * grows the row and instead caps to it and scrolls inside.
 *
 * The list drops its Comment column here and uses the compact row widths from
 * the 720px block; .rl-comment-cell / .rl-comment-h are hidden, not removed
 * (Wide mode and the 720px stack still show them). Below 1100px nothing here
 * applies and the stack is unchanged.
 *
 * The grid applies only while the card carries .layout-sbs (sbx-6fgy.3): the
 * template draws the card with the class (the default), the toolbar's Side by
 * side / Wide switch (video-detail-layout.js) removes it for a remembered Wide
 * while the skeleton stands in (frrx.19), and toggles it on click. Without the class the card is Wide — the stack the page had before
 * 6fgy.2, Comment column and wide row widths included — so Wide needs no rules
 * of its own and can never drift from the stack below 1100px. */
#rallies-card { padding: 18px 20px 24px; }
@media (min-width: 1101px) {
  #rallies-card.layout-sbs {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 384px;
    gap: 16px;
    align-items: stretch;
  }
  #rallies-card.layout-sbs > .rallies-pin,
  #rallies-card.layout-sbs #rallies-player-wrap { display: contents; }
  /* Everything spans the card by default; only the two content columns below
     split it. A display:none child (hidden strip / collapsed adjust bar) takes
     no track, so the grid collapses to the rows that are actually shown. */
  #rallies-card.layout-sbs > .rallies-pin > *,
  #rallies-card.layout-sbs #rallies-player-wrap > * { grid-column: 1 / -1; }
  #rallies-card.layout-sbs > .rallies-pin > .md-toolbar { margin-bottom: -16px; }
  /* The player block is the card's own child now (6fgy.7), not inside the
     player wrap, so it takes the grid's left column directly. */
  #rallies-card.layout-sbs > .sbs-main {
    grid-column: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
  }
  #rallies-card.layout-sbs > .rally-list-wrap {
    grid-column: 2;
    margin-top: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  /* Header keeps its size; the scroller takes the rest of the column and caps
     to it. max-height:none drops the standalone clamp that governs the stack. */
  #rallies-card.layout-sbs > .rally-list-wrap .rally-list-head { flex: 0 0 auto; }
  #rallies-card.layout-sbs > .rally-list-wrap .rally-list {
    flex: 1 1 0;
    min-height: 0;
    max-height: none;
  }
  /* No Comment column in this mode; compact row widths (the 720px densities). */
  #rallies-card.layout-sbs > .rally-list-wrap .rl-comment-h,
  #rallies-card.layout-sbs > .rally-list-wrap .rl-comment-cell { display: none; }
  /* The per-row "i/k" also drops here: with four action buttons the 384px
     column cannot hold it, and it is redundant with the now bar ("rally 1 of
     7") and the game headers. The phone list (<=720px) drops it for the same
     reason; this keeps the two narrow layouts consistent. Without it the row
     measures 372px against the 382px column (headless Chrome, 1400px viewport),
     so the download button no longer sits in a horizontal scroll. */
  #rallies-card.layout-sbs > .rally-list-wrap .rl-of { display: none; }
  #rallies-card.layout-sbs > .rally-list-wrap .rally-list-head,
  #rallies-card.layout-sbs > .rally-list-wrap .rl-row { gap: 8px; }
  #rallies-card.layout-sbs > .rally-list-wrap .rl-num { width: 34px; }
  #rallies-card.layout-sbs > .rally-list-wrap .rl-time { width: 66px; }
  #rallies-card.layout-sbs > .rally-list-wrap .rl-length { width: 46px; }
  #rallies-card.layout-sbs > .rally-list-wrap .rl-shots { width: 44px; }
}

/* ── Match review on the phone: the pinned box and the sticky header (sbx-6fgy.4)
 * At 720px and below the Rallies card is two boxes, ported from
 * prototypes/match-review-layout/phone.html (README decision 4). The upper box
 * (.rallies-pin: title and count, mode pills, now bar, stage, comment field,
 * report trigger) sticks 14px under the 61px topnav — the phone's status-bar
 * zone plus the card's margin in the prototype's 48px — with its corners and
 * border; the list has no scroller of its own, its rows scroll in the page and
 * hide behind the pin; the table header sticks 10px under the pin so the
 * columns stay labelled, and the set headers (structured matches) under that.
 *
 * The mechanics, each of which cost the prototype a round:
 * - Sticky dies under any overflow other than visible between it and the
 *   scroll container, so the list wrapper carries no overflow/background/
 *   border here; the header draws the top corners, the rows box the bottom
 *   ones (overflow: clip, which clips without becoming a scroll container so
 *   the set headers inside still stick).
 * - Rows show through the rounded corners of a sticky box, so both sticky
 *   boxes paint a square page-background ::before under a rounded ::after
 *   (z-index -1 inside their own stacking context), and the header's ::before
 *   reaches 11px up to close the gap to the pin. No opaque strip above the
 *   pin and no shadow: whatever scrolls up stays visible above it.
 * - The header's offset = 85px (pin top + 10px gap) + the pin's height, which
 *   moves with the description, the comment field, the report panel and the
 *   match-structure strip (its set tiles stack at this width); match-review-
 *   layout.js measures it into --pinned-h with a ResizeObserver rather than
 *   fixing the pin's contents to one line. --rl-stick is that offset, shared
 *   by the header, the set headers and the scroll margin that keeps a jumped-to
 *   header (jumpGame's scrollIntoView) out from under the sticky stack.
 * - A pin taller than the viewport can leave, never sticks in a useful way: a
 *   sticky box taller than the scrollport hides everything under it until its
 *   containing block ends. When the pin leaves less than a header and a few
 *   rows of viewport (a structured match's stacked strip, a short phone),
 *   match-review-layout.js sets .pin-overflow on the card and the pin scrolls
 *   with the page; the table header then sticks under the topnav instead.
 * - The comment field and the report trigger already sit in the pin; only the
 *   save status moves, onto the report row. .sbs-main becomes a grid with
 *   named areas and .rally-comment / .rally-report go display:contents so the
 *   textarea, status, trigger and panel place themselves — a media-query-only
 *   reorder, the desktop DOM and layouts untouched. */
@media (max-width: 720px) {
  #rallies-card {
    --rl-stick: calc(85px + var(--pinned-h, 320px));
    padding: 0; background: transparent; border: 0; border-radius: 0;
  }
  /* Three stacked boxes now, each its own rounded white card on the page
     ground (the card itself is bare here): the pin (title, match-structure
     strip, mode pills, description), then the player block (.sbs-main: now bar,
     stage, comment, report), then the list. Only the player block sticks, so
     the video stays in view while the list scrolls under it and the pin scrolls
     away above (alpha review). The player block is a real sibling of the list —
     it left the pin in the markup (6fgy.7) rather than being lifted out with
     display:contents — so a plain sticky sticks it over the list; the pin is a
     static card that just scrolls. */
  #rallies-card > .rallies-pin {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 12px;
  }
  /* The pinned block does not fit: it scrolls with the page and the header
     sticks under the topnav, no gap to close. */
  #rallies-card.pin-overflow { --rl-stick: 61px; }
  #rallies-card.pin-overflow .sbs-main { position: relative; top: auto; }
  /* The pill row is one line that scrolls sideways, as in the prototype. */
  #rallies-card .view-modes { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  #rallies-card .view-modes::-webkit-scrollbar { display: none; }
  #rallies-card .view-mode { flex-shrink: 0; }
  /* The now bar carries only "Rally N" and "N of M": the timecode and length
     segments (and their dots) go, the gap tightens to 6px. The segments are
     a wrapping flex row so a structured match's longer line ("Set 1 · Game 2
     · rally 3 of 5") folds between segments instead of pushing the buttons
     off the bar; a flex item's leading space collapses, so the dot leads. */
  #rallies-card .rally-now { gap: 6px; padding: 8px 10px; }
  #rallies-card .rally-now .chip { white-space: nowrap; flex-shrink: 0; }
  #rallies-card #rally-now-playing { display: flex; flex-wrap: wrap; column-gap: 4px; font-size: 11px; }
  #rallies-card .rn-drop { display: none; }
  #rallies-card .rn-drop + .rn-seg::before { content: none; }
  /* Now bar, stage, comment field, then the report row with the status at its
     right end, then the (usually hidden) report panel. */
  #rallies-card .sbs-main {
    position: sticky;
    top: 75px;
    z-index: 4;
    /* 14px so the ::before that reaches 14px up (to close the gap to the topnav
       when stuck) lands exactly in the gap to the pin card at rest, not over the
       pin's bottom edge. */
    margin-top: 14px;
    padding: 12px 12px 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "now now" "stage stage" "comment comment" "report status" "panel panel";
    align-items: center;
    column-gap: 10px;
  }
  /* A square page-bg reaching 14px up closes the gap to the 61px topnav (the
     box sticks at 75); a rounded surface sits over it. The two-layer paint is
     the pin's and the table header's — a rounded sticky box otherwise shows the
     scrolled rows through its corners. */
  #rallies-card .sbs-main::before {
    content: ""; position: absolute; left: 0; right: 0; top: -14px; bottom: 0; z-index: -1;
    background: var(--bg);
  }
  #rallies-card .sbs-main::after {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  }
  #rallies-card .sbs-main > .rally-now { grid-area: now; }
  #rallies-card .sbs-main > .rally-stage { grid-area: stage; }
  #rallies-card .sbs-main > .rally-comment,
  #rallies-card .sbs-main > .rally-report { display: contents; }
  #rallies-card #rally-comment-input {
    grid-area: comment;
    width: 100%;
    margin-top: 8px;
    min-height: 32px;
    padding: 6px 10px;
  }
  /* Inset 10px like the field's text: the trigger's own 2px of left padding
     plus 8px. The row's 10px bottom margin is the pin's bottom inset. */
  #rallies-card #rally-report-trigger { grid-area: report; justify-self: start; margin: 2px 0 10px 8px; }
  #rallies-card #rally-comment-status { grid-area: status; justify-self: end; margin: 2px 0 10px; padding: 0; }
  #rallies-card #rally-report-panel { grid-area: panel; margin: 0 0 10px; }
  /* Hidden at this width per the spec (decision 3); kept out of the grid's
     auto-placement while it is. */
  #rallies-card .rally-kbd-hints { display: none; }

  #rallies-card > .rally-list-wrap {
    margin-top: 10px;
    border: 0;
    border-radius: 0;
    background: transparent;
    overflow: visible;
  }
  /* No Comment column on the phone (the field above the list is the editor). */
  #rallies-card .rl-comment-h,
  #rallies-card .rl-comment-cell { display: none; }
  #rallies-card > .rally-list-wrap .rally-list-head {
    position: sticky;
    top: var(--rl-stick);
    z-index: 3;
    border: 0;
    background: transparent;
    padding: 11px 9px;
  }
  #rallies-card > .rally-list-wrap .rally-list-head::before {
    content: ""; position: absolute; left: 0; right: 0; top: -11px; bottom: 0; z-index: -1;
    background: var(--bg);
  }
  #rallies-card > .rally-list-wrap .rally-list-head::after {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px 14px 0 0;
  }
  #rallies-card > .rally-list-wrap .rally-list {
    max-height: none;
    overflow: clip;
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 14px 14px;
    background: var(--bg-2);
  }
  /* The game header is the sticky one; on the phone it sticks below the sticky
     table header (the list has no scroller of its own here). */
  #rallies-card .rl-game { top: calc(var(--rl-stick) + var(--rlhead-h, 37px)); }
  /* The list has no scroller of its own here, so jumpGame's scrollIntoView
     (block: start) lands a header at the top of the viewport — under the
     topnav, the pin and the table header — unless the header carries the
     sticky stack as its scroll margin. */
  #rallies-card .rl-set,
  #rallies-card .rl-game,
  #rallies-card .rl-row { scroll-margin-top: calc(var(--rl-stick) + var(--rlhead-h, 37px)); }
}

/* Modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 480px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.2);
  overflow: hidden;
}
.modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 20px 20px 12px;
}
.modal-body {
  padding: 0 20px 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* Feedback: the floating button and its modal (prototypes/feedback-form;
   _feedback.html, js/feedback.js). Signed-in pages only. */
/* The kind switch: the shared .seg, stretched to the modal's width so each
   segment is a full-size tap target on a phone. */
.fb-kind { display: flex; }
.fb-kind button { flex: 1 1 auto; justify-content: center; padding: 8px 6px; font-size: 13px; white-space: nowrap; }
.fb-title { margin-top: 4px; }
.fb-text { resize: vertical; min-height: 120px; font-size: 16px; }   /* 16px: iOS focus-zoom threshold, as .pt-input */
@media (pointer: fine) { .fb-text { font-size: 13.5px; } }
.fb-meta { display: flex; justify-content: space-between; gap: 12px; font: 12px var(--font-mono); color: var(--muted); }
.fb-count { white-space: nowrap; margin-left: auto; }
.fb-count.near { color: var(--warn); }
.fb-error { color: var(--danger); font-size: 13px; }
.fb-actions { display: flex; justify-content: flex-end; gap: 8px; }
.fb-actions-center { justify-content: center; }
.fb-sent { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px; padding: 12px 0 4px; }
.fb-sent p { margin: 0; font-size: 13.5px; }
.fb-sent-mark { width: 40px; height: 40px; border-radius: 50%; background: var(--accent-soft); color: var(--accent);
  display: inline-flex; align-items: center; justify-content: center; }

.share-link {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 8px 8px 12px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 12.5px;
}
.share-link input {
  flex: 1;
  background: transparent;
  border: 0;
  outline: 0;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-2);
  min-width: 0;
}
/* Who joined the match through its link (h21z.3), owner's share modal only. */
.share-members-block {
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.share-members {
  list-style: none;
  margin: 8px 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.share-members li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 6px 6px 12px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 12.5px;
}
.share-members .who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The invite (ws9c.1; spec prototypes/invite-to-match). The actions column puts
   Invite's disabled hint as a visible line under the buttons — a phone has no
   hover for the title. The modal scrolls inside itself on a short phone rather
   than running off-screen. */
.md-act-col { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
/* On a phone the buttons wrap to the full width from the left edge; a hint kept
   at the right would float away from them. */
@media (max-width: 720px) { .md-act-col { align-items: flex-start; } }
.invite-hint { margin: 0; font: 12px var(--font-mono); color: var(--muted); }
#invite-modal .modal { max-height: calc(100dvh - 48px); overflow-y: auto; }
.invite-active { display: flex; flex-direction: column; gap: 14px; }
.invite-players li { padding: 5px 8px 5px 6px; }
.invite-players .who { display: flex; align-items: center; gap: 10px; }
.invite-players .thumb {
  width: 18px;
  height: 36px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--border-2);
  background: var(--surface-3);
  flex: none;
}
.invite-players .pn { font-weight: 500; color: var(--text); }
.invite-players .tm { color: var(--muted); font: 11px var(--font-mono); margin-left: 6px; }
.invite-players .chip {
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;
  line-height: 22px;
  flex: none;
}
.chip-open { background: transparent; border-style: dashed; color: var(--muted); }
.invite-couple { margin: 0; font-size: 12px; color: var(--muted); }

/* The invite's pick screen, /invite/{token} (ws9c.3; invite.html). Two cards
   per row at every width: at 375 px each is ~165 px wide and still holds the
   56x112 crop, the name and a full-width button, so the grid never drops to one
   column. A taken card is dimmed and carries no name; the card being claimed is
   outlined in the accent colour. */
.inv-wrap { max-width: 560px; margin: 0 auto; }
.inv-title { margin-top: 6px; overflow-wrap: anywhere; text-wrap: balance; }
.inv-title .t { font-weight: 600; }
.inv-date { margin-top: 8px; font-size: 12.5px; color: var(--muted); }
.inv-ask { margin-top: 22px; font-size: 15px; font-weight: 500; }
.inv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.inv-team { grid-column: 1 / -1; margin-top: 6px; }
.inv-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-1);
  padding: 14px 12px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 2px;
  min-width: 0;
  transition: opacity 160ms ease, border-color 160ms ease;
}
.inv-card .pl-photo { width: 64px; height: 128px; border-radius: 8px; margin-bottom: 8px; }
.inv-card .act { margin-top: 10px; width: 100%; min-height: 30px; display: flex; justify-content: center; align-items: center; }
.inv-card .act .btn { width: 100%; }
.inv-card.taken { background: var(--surface-2); box-shadow: none; }
.inv-card.taken .pl-photo { opacity: .45; filter: grayscale(1); }
.inv-card.taken .pl-name { color: var(--muted-2); }
.inv-card.pending { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.inv-card .mini {
  width: 12px;
  height: 12px;
  border: 2px solid var(--surface-3);
  border-top-color: var(--accent);
  border-radius: 50%;
}
.inv-empty { margin-top: 22px; margin-bottom: 6px; display: flex; flex-direction: column; gap: 4px; }
.inv-empty p { font-size: 15px; font-weight: 500; }
.inv-empty p.sub { font-size: 13.5px; font-weight: 400; color: var(--muted); }
.inv-in { margin-top: 20px; padding: 18px; display: flex; gap: 16px; align-items: center; }
.inv-in .pl-photo { width: 48px; height: 96px; border-radius: 8px; flex: none; }
.inv-in .big { font-size: 17px; font-weight: 600; }
.inv-in .pl-sub { font-size: 12.5px; }
.inv-in .btn { margin-top: 12px; }
.inv-gone {
  min-height: 52vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 8px;
  padding: 24px 0;
}
.inv-gone .ico {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border-2);
  display: grid;
  place-items: center;
  color: var(--muted);
  margin-bottom: 6px;
}
.inv-gone p { color: var(--muted); font-size: 13.5px; }
@media (max-width: 720px) {
  .inv-card { padding: 12px 10px 10px; }
  .inv-card .pl-photo { width: 56px; height: 112px; }
}

/* ───────────────────────────────────────────────────────────────────────── */
/* Shared match                                                              */
/* ───────────────────────────────────────────────────────────────────────── */
/* The page shell is base.html's .app (min-height 100vh, flex column): the rally
   page (/r/{token}) extends it since 6wn0.4 and keeps its main and header
   classes inside it. */
.sm-main {
  max-width: 1280px;
  margin: 0 auto;
  width: 100%;
  padding: 36px 28px 48px;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 28px;
}
.sm-context { display: flex; flex-direction: column; }

/* ───────────────────────────────────────────────────────────────────────── */
/* Account                                                                   */
/* ───────────────────────────────────────────────────────────────────────── */
.account-screen { max-width: 1040px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
.ac-title { margin: 0; }
.ac-layout {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: 32px;
  align-items: start;
}
.ac-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: 88px;
}
.ac-nav-item {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 36px;
  padding: 0 12px;
  background: none;
  border: 1px solid transparent;
  color: var(--text-2);
  font: 400 var(--fs-body) var(--font-sans);
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
  transition: background 120ms ease, color 120ms ease;
}
.ac-nav-item svg { flex: none; color: var(--muted); }
@media (hover: hover) and (pointer: fine) { .ac-nav-item:hover { background: var(--surface); color: var(--text); } }
.ac-nav-item.active { background: var(--surface); border-color: var(--border); color: var(--text); font-weight: 500; }
.ac-nav-item.active svg { color: var(--accent); }
.ac-nav-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* One white card per section; its rows are hairlines. */
.ac-main { min-width: 0; }
.ac-section {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0 24px;
}
.ac-section-head { padding: 18px 0 14px; border-bottom: 1px solid var(--border); }
.ac-section-head .h2 { margin: 0; font: 500 var(--fs-head)/1.3 var(--font-sans); letter-spacing: -0.01em; color: var(--text); }

/* A row: the label and its help on the left, the value or control beside it. */
.ac-row {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: 6px 24px;
  align-items: start;
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}
.ac-row:last-child { border-bottom: 0; }
.ac-row-label { display: flex; flex-direction: column; gap: 2px; min-width: 0; padding-top: 8px; }
.ac-row-title { display: block; font-weight: 500; font-size: var(--fs-body); line-height: 20px; color: var(--text); }
.ac-row-desc { font-size: var(--fs-sub); line-height: 1.45; color: var(--muted); }
.ac-value { min-height: 36px; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 6px; }
.ac-inline { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0; }
.ac-text { font-size: var(--fs-body); line-height: 20px; color: var(--text-2); overflow-wrap: anywhere; }
.ac-meta { font: 400 var(--fs-sub)/20px var(--font-mono); color: var(--muted); font-variant-numeric: tabular-nums; }

/* A list row (switches, actions): the text takes the width, the control sits at its end. */
.ac-row--end { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
.ac-row--end .ac-row-label { padding-top: 0; }
.ac-row--end .ac-row-desc { max-width: 56ch; }
.ac-end { display: flex; align-items: center; gap: 10px; }

/* The Name header (padel-website-frrx.28; spec prototypes/email-verification/). The name
   leads the Profile card and is edited where it stands. Line, field and buttons share one
   height, so the monogram never moves between viewing and editing. */
.ac-name { display: flex; gap: 12px; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--border); }
.ac-mono {
  position: relative;
  overflow: hidden;
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  font: 500 var(--fs-head) var(--font-sans);
}
.ac-mono-glyph { display: block; line-height: 1; }
.ac-mono-glyph-out { position: absolute; left: 0; right: 0; top: 50%; margin-top: -0.5em; text-align: center; }
.ac-name-body { flex: 1; min-width: 0; }
.ac-name-line { display: flex; align-items: center; gap: 4px; min-height: 32px; flex-wrap: wrap; }
.ac-name-text { font: 500 var(--fs-head)/1.35 var(--font-sans); color: var(--text); overflow-wrap: anywhere; }
.ac-name-edit { width: 24px; height: 24px; border-color: transparent; background: transparent; color: var(--muted); }
@media (hover: hover) and (pointer: fine) { .ac-name-edit:hover { background: var(--bg-2); color: var(--text); } }
.ac-name-form { display: flex; align-items: center; gap: 8px; min-height: 32px; }
.ac-name-form[hidden], .ac-name-line[hidden] { display: none; }
.ac-name-input { width: min(100%, 280px); height: 32px; padding: 0 9px; font: 500 var(--fs-head) var(--font-sans); }
.ac-name-act { height: 32px; display: inline-flex; align-items: center; }
.ac-name-act .ac-name-ico { display: none; }
.ac-name-status { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-sub); color: var(--accent); }
.ac-name-status:empty { display: none; }
.ac-name-status svg { flex: none; }
.ac-name-status path { stroke-dasharray: 22; }
.ac-name-status span { display: inline-block; }
/* An optimistic save the server refused: the old name is back, the line says so and offers the retry. */
.ac-name-status.is-error { color: var(--danger); gap: 8px; }
.ac-name-retry { min-height: 24px; padding: 0; border: 0; background: none; font: inherit; color: var(--text); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
@media (hover: hover) and (pointer: fine) { .ac-name-retry:hover { color: var(--accent); } }
/* Phone: the edit stays on one line. The field takes the room, Save and Cancel become
   36px icon buttons, and line and field share one 36px height. */
@media (max-width: 520px) {
  .ac-name-line, .ac-name-form { min-height: 36px; gap: 6px; }
  .ac-name-input { flex: 1; min-width: 0; width: 100%; height: 36px; }
  .ac-name-act { flex: none; width: 36px; height: 36px; padding: 0; display: grid; place-items: center; }
  .ac-name-act .ac-name-ico { display: block; }
  .ac-name-act .ac-name-txt { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ac-nav-item { transition: none; }
}

@media (max-width: 720px) {
  .account-screen { gap: 16px; }
  .ac-layout { grid-template-columns: 1fr; gap: 16px; }
  /* The section switcher becomes the segmented control. */
  .ac-nav { position: static; flex-direction: row; gap: 2px; padding: 2px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); overflow-x: auto; }
  .ac-nav-item { flex: 1 0 auto; justify-content: center; height: 34px; padding: 0 10px; border: 0; font-size: 12.5px; }
  .ac-nav-item.active { background: var(--surface-3); }
  .ac-section { padding: 0 16px; }
  .ac-row { grid-template-columns: 1fr; gap: 8px; padding: 14px 0; }
  .ac-row-label { padding-top: 0; }
  .ac-value { min-height: 0; }
  .ac-name-input { font-size: 16px; }
  /* A switch keeps its place at the end of its label; an action button drops under its text. */
  .ac-row--end { grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; }
  .ac-row--action { grid-template-columns: 1fr; gap: 10px; }
}

/* Avatar circle helper */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: oklch(35% 0.08 210);
  color: oklch(92% 0.06 210);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 12px;
  border: 1px solid oklch(40% 0.05 210 / 0.5);
}

/* ───────────────────────────────────────────────────────────────────────── */
/* Clip export                                                               */
/* ───────────────────────────────────────────────────────────────────────── */
.export-screen { display: flex; flex-direction: column; gap: 28px; }
.ex-head {
  display: flex;
  align-items: flex-end;
  gap: 18px;
  flex-wrap: wrap;
}
.ex-grid {
  display: grid;
  grid-template-columns: 1fr 360px;
  gap: 20px;
  align-items: flex-start;
}
.ex-picker {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.ex-picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border);
  gap: 12px;
  flex-wrap: wrap;
}
.ex-rally-list {
  max-height: 620px;
  overflow-y: auto;
}
.ex-rally {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 120ms ease;
}
.ex-rally-comment {
  font-size: 12px;
  color: var(--text-2);
  margin-top: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Hidden rallies: unchecked by default, dimmed, and tagged so it's clear
 * why they're excluded — still selectable if the user opts in. */
.ex-rally-hidden .ex-rally-num,
.ex-rally-hidden > div:nth-child(2) > div:first-child { opacity: 0.5; }
.ex-rally-hidden-tag {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 9.5px;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0 5px;
}
.ex-rally:last-child { border-bottom: 0; }
.ex-rally:hover { background: var(--surface-2); }
.ex-rally.selected { background: color-mix(in oklab, var(--accent-soft) 70%, var(--surface)); }
.ex-rally.playing {
  box-shadow: inset 2px 0 0 var(--accent);
}
.ex-rally.playing .ex-rally-num,
.ex-rally.playing .rally-num-text {
  color: var(--accent);
}
/* Loading in place (frrx.21): the skeleton row is the real row, inert. The
   quiet 4 s line sits under it and goes with the skeleton. */
.ex-rally.sk-row { pointer-events: none; cursor: default; }
.ex-rally-list .sk-slow { margin: 8px 18px 12px; }

/* Export preview pane — mini player above the rally list */
.ex-preview {
  padding: 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-2);
}
.ex-preview-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.ex-preview-stage video {
  width: 100%;
  height: 100%;
  display: block;
}
.ex-preview-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  color: rgba(255, 255, 255, 0.55);
  font-size: 12px;
  letter-spacing: 0.04em;
  pointer-events: none;
  z-index: 1;
}
.ex-preview-meta {
  margin-top: 10px;
  font-size: 11.5px;
  color: var(--muted);
}

/* Make the checkbox a larger hit target without changing visuals: the .ex-check
 * stays 18×18 visually but the click area inflates with negative margin + padding. */
.ex-check {
  margin: -8px;
  padding: 8px;
  box-sizing: content-box;
}
.ex-rally-num {
  width: 40px;
  text-align: center;
  font-size: 12px;
  color: var(--muted);
  font-family: var(--font-mono);
}
.ex-rally.selected .ex-rally-num { color: var(--accent); }
.ex-rally-pos {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: 50%;
  font-weight: 500;
  font-size: 11px;
}
.ex-rally-thumb {
  position: relative;
  width: 56px;
  aspect-ratio: 16 / 10;
  border-radius: 4px;
  overflow: hidden;
}
.ex-check {
  width: 18px;
  height: 18px;
  border-radius: 4px;
  border: 1px solid var(--border-2);
  background: var(--bg-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent-ink);
  flex-shrink: 0;
}
.ex-check.checked {
  background: var(--accent);
  border-color: var(--accent);
}

.ex-side {
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: sticky;
  top: 80px;
}
.ex-mode-options {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 12px 12px;
}
.ex-mode-opt {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  background: var(--bg-2);
}
.ex-mode-opt input { display: none; }
.ex-mode-opt.active {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.ex-mode-icon {
  width: 36px; height: 36px;
  border-radius: 8px;
  background: var(--surface-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
}
.ex-mode-opt.active .ex-mode-icon {
  background: var(--accent);
  color: var(--accent-ink);
}

.ex-set {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  border-top: 1px solid var(--border);
  font-size: 13px;
  gap: 10px;
  flex-wrap: wrap;
}
.ex-set label {
  color: var(--text-2);
  font-weight: 500;
}

.ex-summary {
  background: var(--bg-2);
  padding: 14px 16px;
  margin-top: 6px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.ex-render {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 440px;
}
.ex-render-card {
  width: 100%;
  max-width: 520px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 30px 30px 26px;
}
.ex-render-stage {
  display: flex;
  gap: 8px;
  margin-top: 18px;
  font-size: 11px;
  color: var(--muted-2);
  flex-wrap: wrap;
}
.ex-render-stage span {
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
}
.ex-render-stage span.done {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}
.ex-render-stage span.current {
  color: var(--text);
  background: var(--surface-3);
  border-color: var(--border-2);
}

.ex-done {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 480px;
}
.ex-done-card {
  width: 100%;
  max-width: 640px;
  padding: 50px 40px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.ex-done-icon {
  width: 72px; height: 72px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--accent-line);
}
.ex-done-preview {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  margin-top: 40px;
  width: 100%;
  max-width: 480px;
}
.ex-done-thumb {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 4px;
  overflow: hidden;
  background: #000;
}
.ex-done-more {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 13px;
}

/* ───────────────────────────────────────────────────────────────────────── */
/* Auth screens                                                              */
/* ───────────────────────────────────────────────────────────────────────── */
.auth-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background:
    radial-gradient(ellipse 80% 50% at 50% 0%, var(--accent-soft) 0%, transparent 70%),
    var(--bg);
}
.auth-card {
  width: 100%;
  max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 36px 32px;
  box-shadow: var(--shadow-2);
}
.auth-head { display: flex; flex-direction: column; gap: 6px; text-align: center; margin-bottom: 24px; }
.auth-logo { display: flex; justify-content: center; margin-bottom: 12px; }
.auth-logo img { object-fit: contain; }
.auth-account-link { margin-top: 24px; text-align: center; font-size: 13px; color: var(--muted); }
.auth-password-label { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 4px 12px; }
.auth-forgot { color: var(--accent); font-size: 12px; }
.auth-card .field label { font-family: var(--font-sans); font-size: 13px; font-weight: 500; text-transform: none; letter-spacing: normal; }
.auth-card form { display: flex; flex-direction: column; gap: 14px; }
.auth-card .field { width: 100%; }
/* Readable auth fields avoid Safari focus zoom during password autofill. */
.auth-card input, #resetEmail { font-size: 16px; }
.auth-card .input, .auth-card .btn { border-radius: 4px; }
.auth-card .signup-notice { margin: 20px 0 0; font-size: 12px; text-align: center; text-wrap: balance; }
.auth-card .signup-notice a { white-space: nowrap; }
.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 24px 0;
  color: var(--muted);
  font-size: 14px;
}
.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}
/* GIS medium suppresses account personalization; Apple shares its 32px height. */
.auth-providers { display: grid; gap: 10px; }
.auth-google { min-height: 32px; min-width: 0; }
.auth-provider {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 32px;
  padding: 5px 10px;
  border: 1px solid #dadce0;
  border-radius: 4px;
  background: #fff;
  color: #3c4043;
  font-family: "Google Sans", arial, sans-serif;
  letter-spacing: .25px;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 500;
  line-height: 20px;
  cursor: pointer;
  transition: background 150ms ease, box-shadow 150ms ease;
}
.auth-provider img { flex-shrink: 0; width: 14px; height: 16px; }
.auth-provider:hover:not(:disabled) { background: #f8f9fa; box-shadow: 0 1px 3px rgb(0 0 0 / 8%); }
.auth-provider:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.auth-provider:disabled { opacity: .55; cursor: wait; }
.auth-error {
  padding: 10px 12px;
  background: color-mix(in oklab, var(--danger) 10%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--danger) 35%, var(--border));
  border-radius: 8px;
  color: var(--danger);
  font-size: 13px;
}

/* ───────────────────────────────────────────────────────────────────────── */
/* Responsive                                                                */
/* ───────────────────────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
  .cta-card { grid-template-columns: 1fr; gap: 24px; padding: 32px; }
  .ex-grid { grid-template-columns: 1fr; }
  .lb-shell { grid-template-columns: 168px 1fr; gap: 16px; }
  .lb-card { grid-template-columns: 150px 1fr auto; gap: 14px; }
  .ex-side { position: static; }
}

@media (max-width: 720px) {
  /* The one step of the type scale that moves with the viewport (ADR-054).
     The countdown is 40px of mono on a card 560px wide and 22px on a phone,
     and the second number is a measured fit — see the pin's height table at
     the bottom of this block. Declared on the token so every rule that
     spends it, and both of the ratios derived from it, come down together. */
  :root { --fs-eta: 22px; }
  .topnav { padding: 12px 16px; }
  .topnav, .sr-topbar { --nav-control: 36px; }
  .nav-screens { display: none; }
  .nav-username, .nav-logout-desktop { display: none; }
  /* The balance popover hangs from the bar's right edge, not from the chip, which
     sits left of the language select and the menu button here. */
  .nav-credits { position: static; }
  .nav-credits-pop { right: 16px; top: calc(100% - 4px); width: min(290px, calc(100vw - 32px)); }
  .nav-toggle { display: inline-flex; }
  .page, .page-narrow, .page-wide { padding: 24px 16px 60px; }
  .sm-main { padding: 24px 16px 36px; }
  /* The phone library. The rail stops being a column and becomes a row of
     chips above the list, and the card drops to two columns with the status,
     the when-line and the kebab on a third row of their own. The mockup drives
     this by class (it is a page inside a page, so a media query there would
     key off the wrong width); here it is the real viewport. */
  .lb-shell { grid-template-columns: 1fr; }
  .lb-rail { flex-direction: row; flex-wrap: wrap; gap: 8px; }
  .lb-rail-group { flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .lb-rail-label { display: none; }
  .lb-rail-item {
    width: auto;
    border: 1px solid var(--border);
    background: var(--surface);
  }
  .lb-rail-item.active {
    border-color: var(--accent-line);
    background: var(--accent-soft);
    color: var(--accent);
  }
  .lb-card { grid-template-columns: 116px 1fr; gap: 12px; align-items: start; }
  .lb-side {
    grid-column: 1 / -1;
    justify-content: space-between;
    padding-left: 0;
    padding-top: 10px;
    border-top: 1px solid var(--border);
  }
  .lb-side-stack { flex-direction: row; align-items: center; gap: 10px; }
  /* One column, progress first — which is source order, so nothing is
     reordered — and since vyt.10 that is true at EVERY width, so this block no
     longer has a column to collapse. Part of the instrument still STICKS: on a
     phone, focusing a field scrolls the form up under the keyboard and the
     progress would otherwise leave the screen for the whole time someone is
     typing into it. A person who cannot see the upload while typing has no
     reason to believe it is still running. Needs .up-card to clip rather than
     hide — see there. `top` clears the sticky .topnav, which at this width is
     12px + a 36px toggle + 12px + its 1px border.

     `.up-card-pin`, not everything above the form: that block was 273px tall
     when it carried the three-phase stepper and a phone with the keyboard open
     has ~360px of viewport, so sticking all of it held 334px of 360 and painted
     the progress panel over the field being focused (measured in headless
     Chrome at 390x360: #up-title landed at y=160 inside a sticky band of
     61–334). The pin is the instrument plus the tab notice and pause/cancel,
     which leaves the form usable — see the height budget below.

     The `display: contents` that used to sit here is GONE with the grid it
     existed for. Sticky is bounded by its containing block, and for a grid item
     that is its own grid area — so a pin nested inside a column track had zero
     travel and the column had to be dissolved into the grid to give it any.
     With one column the pin's containing block is #dz-active, the whole card
     body, and it travels the length of the card for free. Nothing replaced the
     declaration; there is nothing left for it to do. */
  .up-card-pin {
    position: sticky;
    top: 61px;
    z-index: 1;
    background: var(--surface);
    padding-bottom: 12px;
  }
  /* The pin's height is the budget the form underneath lives on — see the
     measurement above: at 390x360 with the keyboard up, a pin taller than
     ~90px starts eating the field being typed into, and that is the exact
     failure the sticky exists to prevent.

     The instrument does not fit in that budget and cannot be made to: the
     estimate is the number this screen exists to answer, and the card was
     already at 87px without it. So the rules below buy back everything that is
     spacing rather than information, and the numbers are MEASURED in headless
     Chrome at 390x360 rather than reasoned about:

       87px    before the instrument (bar + byte counts + controls)
       146px   the instrument laid out as it is above 720px
       119px   with the rules below
       181.4px the same pin once vyt.1 put the tab notice inside it
       144.4px vyt.10, with the bar and the byte row taken out of it

     pnot.1 put a bar back in and added the figures line, and pnot.3 collapsed
     the notice to one line — see the per-state table below, which is measured
     and which this list is not any more: none of the five figures above
     reproduces under the harness that measured that one, and rather than pick a
     winner between runs nobody can re-execute, the table below measures BOTH
     sides of the latest change in one run and is the number to work from. What
     it will NOT give you is the cost of the row against this list: its two
     columns are pnot.1 and pnot.3, so the 1px–33px spread this paragraph
     quoted for pnot.1 came off a column that no longer exists here.

     Stuck, the worst state in that table occupies 61–244.4 in a 360px viewport
     and leaves 115.6px of it below. (The figure this paragraph carried before
     pnot.1 — 154.6px, against 129.8px "before" — was the vyt.10-era pin, from
     the run above that no longer reproduces; the 98.6px it carried between
     pnot.1 and pnot.3 was read off the column pnot.3 re-measured. Neither is
     what the card measures today.) It is over the 90px the sentence above
     records, and this comment is not going to pretend that is much of a margin:
     90px was the measured height of the OLD contents, not a threshold anything
     derived, and the failure it describes was a 334px pin covering the field
     being typed into. 115.6px of clearance is not that. Re-measure here before
     adding another row — the last three lines of that list, and the whole table
     below, are each somebody doing exactly that.

     (The first three figures read y=169 / 191px / 223px until vyt.1 re-measured
     below and the two blocks disagreed. They were taken against a 50px sticky
     top; the rule twelve lines up has said `top: 61px` since it was written, so
     each was 11px out. The heights themselves — 87 / 146 / 119 — reproduce
     exactly.)

     Two of them matter and one of them does not, which is worth writing down
     because the first version of this block got it backwards. `.ins-head` was a
     ROW: its height is the taller of its two columns, and the estimate column
     (eta + note, stacked) is the tall one. Hiding `.ins-trace` — the obvious
     saving, 22px — therefore reclaimed exactly ZERO vertical pixels, because it
     lived in the shorter column. The height came from folding the note up onto
     the estimate's own line and taking the display type down, and it still
     does.

     Since pnot.1 that arithmetic is history on this card, and since pnot.4 on
     the other one too: BOTH `.ins-head`s have one child (`.ins-primary`) and
     neither has a trace, so `align-items` beside the rule below now reaches
     nothing anywhere. `.ins-trace { display: none }` stood here one bead longer
     than the pin needed it, for `#mp-trace` on the old multi-part panel — width
     relief on a panel rather than height bought for a pin — and went with the
     element.
     What is left below is a real saving in every case: the note folded onto the
     estimate's line, and the display type down a step. */
  .ins-head { margin-bottom: 8px; align-items: baseline; }
  /* The note sits BESIDE the estimate here rather than under it — "12m 34s
     remaining" reads as one phrase either way, and stacked it costs the height
     of a second line for a word. */
  .ins-primary { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
  .ins-note { min-height: 0; margin-top: 0; }
  /* The countdown is the one step on the scale that takes a different value
     on a phone, and it takes it on the TOKEN (`:root` above, in this same
     block) rather than here — so `.ins-eta { font-size: var(--fs-eta) }` is
     true at every width, and the two sizes derived from it — the unit suffix
     at 0.42em of its child span, and the paused word at calc(--fs-eta * 0.6)
     on the countdown element itself — come down with it (9.24px and 13.2px)
     instead of needing overrides of their own. The two say their ratio in
     different units for a reason .ins-eta-word explains at length: only the
     unit is a child, and only a child can use `em` here. 22px is a measured
     fit, not a type choice: it is what keeps the pin inside the height budget
     below. `.ins-eta-word { font-size: 14px }` stood here and is deleted with
     it. */
  .ins-eta { min-height: 24px; }
  /* No line of its own here, unlike above 720px: it takes .ins-primary's
     wrapping row and drops to a second line only when it does not fit, which
     is the same trade the note makes one line up. `Paused 3.1 of 8.7 GB · 36%`
     fits and costs nothing; `14m 20s remaining` plus three figures does not,
     and wraps. The 7px above becomes .ins-primary's own 6px row gap. */
  .ins-figures { margin-top: 0; }
  /* #up-tabline is the row the comment above asked to be re-measured before it
     was added (vyt.1), and it is paid for the same way the instrument was:
     nothing is hidden and the type comes down a step. Hiding it was the obvious
     saving and it is the wrong one — "it is safe" is the sentence a PHONE most
     needs, since a phone is the thing that wanders off to another app
     mid-upload.

     Re-measured in headless Chrome inside 320px and 390px iframes (Chrome on
     macOS will not open a window narrower than ~500px and silently renders a
     smaller --window-size at 500). The page inside those frames is the real
     index.html rendered through Jinja, with the webfonts loaded (asserted, not
     assumed — a fallback face wraps differently and would move every number
     here), and each state is applied by calling the page's OWN createInstrument
     and upFigures and replaying setState's three button toggles, so the readout
     is whatever render() writes rather than a hand-typed approximation of it.

     PER STATE, because the pin has two halves and BOTH of them move. The
     readout above the row is rewritten per phase by render() — the estimate
     blanks and a note takes its place while offline, "Paused" replaces it while
     paused — so a table built by swapping the notice over one frozen uploading
     readout describes cards that cannot occur. Pin height at 320 / at 390, then
     what is left below it in a 360px viewport (360 - (61 + pin)):

       state          pnot.1          pnot.3          headroom after
       initializing   134   / 134     134   / 134     165   / 165
       uploading      180.4 / 180.4   180.4 / 180.4   118.6 / 118.6
       paused         192.4 / 175.4   175.4 / 158.4   123.6 / 140.6
       offline        183.4 / 183.4   183.4 / 183.4   115.6 / 115.6
       reconnecting   183.4 / 183.4   183.4 / 183.4   115.6 / 115.6
       completing     134   / 134     134   / 134     165   / 165

     BOTH columns are measured in one run of one harness, and that is the point:
     the pnot.1 column above is NOT the pnot.1 column this table carried before
     — that one had initializing at 183.4 / 162.4 and completing at 162.4 /
     162.4, for two states that cannot differ in HEIGHT: neither is in
     INSTRUMENT_LIVE, so render() leaves the estimate and its note empty for
     both — there is no separate rate readout left to blank, on either card
     since pnot.4 — and draws the same one-line figures row
     (upload-instrument.js), and setState hides all three buttons for both. What
     differs is the digits inside that row — 0% against 100% — which is not
     21px. Its harness is not left to re-execute, so
     rather than mix two methods in one table the whole thing was re-measured
     from the pnot.1 merge and from this commit with the harness described above.
     FOUR of the six rows moved, not only the two that disagreed: offline and
     reconnecting read 200.4 / 183.4 in the old column and 183.4 / 183.4 in this
     one, and that 17px at 320 is what the paragraph two below rests on — so it
     is the row to distrust first if this table is ever re-measured again.
     Uploading and paused reproduce EXACTLY across the two harnesses, which is
     the reason to trust the four that did not.

     pnot.5 (ADR-054) moved type INSIDE this pin — `.ins-note` and
     `.ins-figures` from 11.5px to the meta step at 11px, and the paused word
     from a hard 14px here to a ratio of the countdown, i.e. 13.2px — so the
     table was re-checked. What is recorded is a DELTA rather than a new set of
     absolutes, and deliberately: a third harness (the real index.html served
     over http into 320px and 390px iframes, webfonts asserted loaded, each
     state driven through the page's own createInstrument with two progress
     samples four seconds apart so the countdown has a measured rate) reads
     `uploading` 28px shorter than the column above in BOTH trees, so its
     absolutes are not comparable with these and would be a fourth method
     mixed into one table. Its old-against-new difference is, and it is
     identical at 320 and at 390:

       initializing +1   uploading  0   paused  0
       completing   +1   offline    0   reconnecting 0

     So the FLOOR does not move, and neither does any live state by more than
     a pixel. Offline and reconnecting are the states this card is measured
     against and they are untouched, which leaves 115.6px of a 360px viewport
     exactly as before. Nothing here is close to the 334px pin that started
     this.

     The paused row is worth a sentence, because it read +3 until review and
     that +3 was a BUG rather than a wrap. `.ins-eta-word` shipped as `0.6em`
     on the element that also carries `.ins-eta`, where `em` resolves against
     the parent and not against the countdown, so the word drew 9.6px instead
     of 13.2 and the shorter word re-wrapped .ins-primary into a 27px row. The
     rule is calc(var(--fs-eta) * 0.6) now (see .ins-eta-word), the word is
     13.2px, and the row measures 24px at both 320 and 390 — the same as the
     old tree. A delta this table could not explain was the tell; that is what
     the "distrust it first" habit above is for.

     WHAT THIS ROW COSTS IS NOW A DESKTOP SAVING, and the phone keeps almost
     all of what it had. Collapsing each notice to one line takes 19px off the
     560px card (banner 60 → 42) — but at 320 and 390 the one-line copy still
     wraps to two lines, which is the same two lines the stacked shape occupied
     once `.banner-sub { display: inline }` had folded it, so the only state
     that gets shorter here is `paused`, whose sub-line was the long one: -17px
     at both widths. The bead that made this change expected ~22px per live
     state; on a phone that is true of one state in six, and this table is the
     reason to say so rather than repeat the expectation.

     The tightest case in the card is a DROPPED CONNECTION at both widths, and
     this change is what makes that true at 320 as well: offline is untouched
     here (183.4 / 183.4, 115.6px left of a 360px viewport) while paused, which
     was tighter than it at 320 on the strength of that long sub-line, becomes
     the roomiest live state on a phone. Uploading — the state you actually type
     under — keeps 118.6 / 118.6. The floor is 115.6px, against a failure mode
     that was a 334px pin drawing itself over the field being typed into; that
     is the number to re-measure against before another row goes in here.

     7wdc.10 put a fourth notice in the row, #up-tabline-stopped, for the two
     states a single-file run ends in with its bytes kept. Measured with a
     DIFFERENT harness from the table — headless Chrome on the Jinja-rendered
     page served with a mock backend and GCS, the real upload driven to the
     stop, webfonts not asserted — so these are a separate pair, not a column.
     Pin height at 320 / 390, then headroom in a 360px viewport:

       failing      194.4 / 177.4    104.6 / 121.6   ("Upload keeps failing…")
       unreadable   228.4 / 211.4     70.6 /  87.6   ("Your phone can't read…")

     Both sit below the 115.6px floor above, which holds for the live states
     only; `unreadable` most of all, and that is accepted rather than cut: its copy is the owner's decision (two sentences, one of them the
     instruction), and it shows only once nothing will ever upload under it —
     the way on is Cancel and a re-saved file, which is a new upload with a
     new card, so nobody is typing match details for this one. */
  .up-tabline { margin-top: 12px; }
  .banner { padding: 8px 10px; gap: 7px; font-size: var(--fs-sub); }
  /* `.banner-sub { display: inline }` used to be the third rule in this group,
     and it is gone because the element it re-flowed is: pnot.3 authors the
     reason clause as text after the lead's <b>, which is one paragraph by
     construction at every width. What that rule bought at this breakpoint is
     now bought at all of them, and by the markup rather than by a declaration
     nothing would notice the loss of. */
  .h-display { font-size: clamp(36px, 8vw, 56px); }
  .h1 { font-size: 28px; }
  .h2 { font-size: 20px; }
  .auth-wrap { padding: 0; min-height: 100svh; align-items: stretch; background: var(--surface); }
  .auth-card {
    max-width: none;
    min-height: 100svh;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: max(28px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(28px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
  }
  /* The rally table's fixed columns were sized for the desktop card: 284px of
     columns and 14px gaps before the actions cell, against a list box that
     measures 289px on the shared page at a 375px viewport. The Shots header
     clipped to "SHO" and the download button sat ~90px into the row's own
     horizontal scroll — which exists, but touch gives no affordance to find
     it. Each width below is the column's widest content plus a step: a
     3-digit rally #, h:mm:ss under the 9-char TIMESTAMP header (~64px of
     10.5px mono at 0.08em), m:ss under LENGTH (~43px, the binding one), and
     2-digit shots under SHOTS. Tightened, every column plus one 32px action
     button fits the 289px box with room to spare; video_detail's copy of the
     table gets the same densities and keeps its comment cell's flex.
     The gap is 4px, not 8: video_detail's owner row carries three actions
     (favourite, hide, download — the share icon hides here, lwlk.3), and at 8px
     the row's min-content is 342px, 8px over a 360px phone's list box, so the
     download button clipped there. 4px between the mono columns (which already
     carry trailing whitespace) drops it to ~326px, inside a 360px phone's
     ~334px list box (the smallest live phone width); below that the row still
     scrolls. The shared page's one-action row only gains room. */
  .rally-list-head, .rl-row { gap: 4px; }
  .rl-num { width: 34px; }
  .rl-time { width: 66px; }
  .rl-length { width: 46px; }
  .rl-shots { width: 44px; }
}

/* The placeholders are what name the sides now, so they have to be readable:
   the mockup (docs/mockups/upload-card-v2.html) stacks the pair to one column
   below a 430px CARD, and this is the nearest thing to that without container
   queries — a 480px viewport is ~408px of card content once the page's 16px
   padding and the card's own are taken off.

   Measured in headless Chrome at a 320px viewport: two across leaves each box
   119px, i.e. ~99px of text after the 10px padding either side, and "Your
   partner" at 16px is right on that edge. It fits, and it fits by nothing.
   The date keeps its own line at every width; it is ten characters. */
@media (max-width: 480px) {
  .up-player-row { grid-template-columns: 1fr; }
  /* The score row stacks with the pairs it sits between, at the same width and
     for the same reason: three cells across cannot hold the digits on the
     centre line once the card is this narrow — the label and the link would
     squeeze them off it. Stacked, the centring comes from `align-items` rather
     than from the equal side cells, so those give up their share here. */
  .up-scorebar { flex-direction: column; align-items: center; gap: 6px; }
  .up-scorebar > .up-score-side { flex: 0 0 auto; text-align: center; }
}

/* Below iPhone-SE width the anonymous topnav cannot hold the full brand plus
   both auth CTAs (~326px of content, body clips horizontal overflow), so in
   the width-constrained header the mark stands alone. Footer and auth-card
   brands are centered with room to spare and keep the wordmark. */
@media (max-width: 374px) {
  .topnav .brand > span { display: none; }
}

/* ───────────────────────────────────────────────────────────────────────── */
/* Public rally page (/r/{token}) — padel-website-lwlk.2.                    */
/* Appended as one block at the END of the file on purpose: lwlk.3 (the      */
/* owner's share control) is being built concurrently, and a tail block      */
/* keeps the eventual rebase trivial. Spec: prototypes/rally-share/          */
/* recipient.html — these are its <style> rules moved here (its own one-row */
/* topbar went with 6wn0.6: the page has base.html's nav).                  */
/* ───────────────────────────────────────────────────────────────────────── */

/* One-rally page: the stage is the hero, so it gets the card treatment the
   shared-match page gives its rally block. */
.sr-stage-card { padding: 0; overflow: hidden; }
.sr-stage {
  position: relative;
  aspect-ratio: 16 / 9;
}
.sr-stage video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* A class, not an inline `display: flex` — an inline display outranks .hidden
   and only stays hideable because .hidden carries !important (same note as
   .up-match-hand). Shown by shared-rally.js when the play-url fetch fails:
   revoked mid-visit, or the network died. */
.sr-player-fail {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 24px;
  text-align: center;
  background: var(--surface);
  color: var(--muted);
  font-size: 14px;
}
.sr-under {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}

/* ── Per-rally sharing (padel-website-lwlk.3) ──────────────────────────────
   Appended as one block while lwlk.2 was built concurrently against this
   file — kept at the end so the rebase stayed trivial.

   Below 720px the rally table's columns are sized so that every column plus
   ONE 32px action fits the 289px list box (see the pin at the rally-table
   rules in the 720px media query above); anything more sits in the row's
   horizontal scroll, which touch gives no affordance to find. So the row's
   share icon hides here rather than becoming a fourth icon nobody can reach
   — the now-playing bar's share button (#rally-now-share, in every rally's
   tap path) is the phone entry point. */
@media (max-width: 720px) {
  .rl-share { display: none; }
}

/* ── Per-rally share modal v3: chat-app tiles (padel-website-lwlk.4) ───────
   .share-targets is the deterministic per-network intent row from
   prototypes/rally-share/owner.html (wa.me / t.me links, plus the Instagram
   file tile a capable mobile browser reveals). Appended at the end, like the
   lwlk.3 block above, while lwlk.5 works this file concurrently — kept as
   one block so the rebase stays trivial. */
.share-targets { display: flex; gap: 8px; margin-top: 14px; }
/* Two <a> (the wa.me / t.me deep links) and one <button> (Instagram, which
   has no link intent and hands over a file instead) share this class, so it
   also resets the UA button defaults the anchors do not carry — without
   font-weight/line-height the Instagram tile renders a hair off its
   neighbours. */
.share-target {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 11px 6px 9px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: inherit;
  line-height: inherit;
  text-align: center;
}
.share-target:hover {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  background: color-mix(in oklab, var(--accent) 7%, var(--surface));
}
.share-target svg { width: 18px; height: 18px; }

/* The MP4 is being fetched for the system sheet (setRallyShareFileBusy):
   dim, block re-taps, spin the icon (keyframes `spin`, defined above with
   the other spinners). Cleared when the share settles — the sheet closed, or
   the fetch honestly failed — and reset on every modal open, so a previous
   rally's abandoned fetch never leaves a fresh modal spinning. On Android
   and desktop the page stays visible beside the sheet, so the spin covers
   the open sheet too; iOS's sheet is fully modal and hides it.
   pointer-events blocks the mouse; `disabled` (set alongside) blocks Enter,
   which pointer-events does not. */
.share-file-busy { opacity: 0.55; pointer-events: none; }
.share-file-busy svg { animation: spin 0.9s linear infinite; }

/* ---- Processing-progress card (ADR-062) ------------------------------------
   Rendered by static/js/video-progress.js into #processing-progress on the
   match page from the pipeline's progress document; hidden until the first
   poll answers, absent for a match that is uploading, long done, or processed
   before the document existed. One card, several kinds (data-vp-kind), and
   ONE bar for both of the document's phases: the core phase fills the track
   up to the .vp-milestone tick (the match is watchable past it), the
   analytics phase fills the rest. The bar's width is the document's own two
   fractions combined by the renderer — the library, by contrast, still draws
   no bar (ADR-049 decision 9), because a list card has no document to draw
   from. */
.vp-card {
  padding: 16px 20px 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.vp-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.vp-eta { color: var(--text); }
.vp-line { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.vp-message { font-size: 15px; font-weight: 500; }
.vp-percent { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.vp-sub { font-size: 12.5px; color: var(--muted); margin: 0; }
.vp-note { font-size: 12.5px; color: var(--warn); margin: 0; }
.vp-actions { display: flex; gap: 8px; }

/* The track is 6px, fully rounded; the fill is the document's fraction, set
   inline by the renderer. No transition: the card is rebuilt on every poll,
   so a width transition would restart from zero each time. */
.vp-bar {
  position: relative;
  height: 6px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.vp-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
}
/* The tick where the match becomes watchable (the renderer's VP_CORE_SHARE):
   a hairline notch across the track, faint enough to sit on the fill and on
   the empty track alike. Decorative — the label and the ETA chip carry the
   meaning in words. */
.vp-milestone {
  position: absolute;
  top: 0; bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--text);
  opacity: 0.35;
}
/* Queued / unavailable: a bar with no position. A short segment sweeps the
   track so it reads as "busy, not measured" — never a fixed fill that a reader
   would take as a fraction. */
.vp-bar-busy .vp-fill {
  width: 28%;
  opacity: 0.55;
  animation: vp-sweep 1.6s ease-in-out infinite;
}
@keyframes vp-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(360%); }
}
@media (prefers-reduced-motion: reduce) {
  .vp-bar-busy .vp-fill { animation: none; transform: none; width: 100%; opacity: 0.25; }
}

/* The stepper: one line per step of the phase being drawn, in the writer's
   order, wrapping on narrow screens. The mark is drawn by state. */
.vp-steps {
  list-style: none;
  margin: 2px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  font-size: 12.5px;
  color: var(--muted);
}
.vp-step { display: inline-flex; align-items: center; gap: 7px; }
.vp-step.running { color: var(--text); }
.vp-step.failed { color: var(--danger); }
.vp-step-pct { font-size: 11.5px; color: var(--muted); }
.vp-mark {
  width: 14px; height: 14px;
  border-radius: 999px;
  border: 1.5px solid var(--border-2);
  box-sizing: border-box;
  flex-shrink: 0;
  position: relative;
}
.vp-step.done .vp-mark {
  border-color: var(--accent);
  background: var(--accent);
}
.vp-step.done .vp-mark::after {
  content: "";
  position: absolute;
  left: 4px; top: 1.5px;
  width: 3px; height: 6.5px;
  border: solid var(--surface);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}
.vp-step.running .vp-mark {
  border-color: var(--surface-3);
  border-top-color: var(--accent);
  animation: spin 1s linear infinite;
}
.vp-step.failed .vp-mark { border-color: var(--danger); }
.vp-step.failed .vp-mark::before,
.vp-step.failed .vp-mark::after {
  content: "";
  position: absolute;
  left: 5px; top: 2px;
  width: 1.5px; height: 7px;
  background: var(--danger);
}
.vp-step.failed .vp-mark::before { transform: rotate(45deg); }
.vp-step.failed .vp-mark::after { transform: rotate(-45deg); }
@media (prefers-reduced-motion: reduce) {
  .vp-step.running .vp-mark { animation: none; border-top-color: var(--accent); border-right-color: var(--accent); }
}

.vp-failed .vp-message { color: var(--danger); }

/* An admin reading someone else's match (i779.6) — app.js fills it. Quiet on
   purpose: it explains the missing controls, it is not an alert. */
.read-only-line { margin: 0; color: var(--muted); font-size: 12.5px; }
#page-sub + .read-only-line { margin-top: 6px; }
/* A share-link holder's view-only notice on the match page (zg1l.9). The same
   quiet register as .read-only-line, with the lock glyph; not the old share
   page's green banner, because green is the action colour (DESIGN.md, One
   Meaning). */
.link-note { display: flex; align-items: center; gap: 8px; margin: 0; color: var(--muted); font-size: var(--fs-sub); line-height: 1.45; }
.link-note svg { flex: none; color: var(--muted-2); }
/* The link page on v2 (lsrn.56, prototypes/shared-match-v2/ sections 1 and 7): the
   view-only line and the sign-up card stay above the player, and the strip's
   Hidden points, which it builds for every viewer, is not drawn for a link holder.
   The rally's pill always holds the download (lsrn.57), with the owner's star
   before it on a starred rally. At
   1100px and below the name row goes under the player too, as the spec's phone draws
   it (the owner's phone keeps its header on top).
   The first paint's column is v2's sum (--mp2-col-w, below) less the line and its
   gap (39), not counting the card; match-page-v2.js's fit() measures the same. */
.match-detail.mp2-player[data-viewer="link"] .mp2-hidden { display: none; }
/* The owner's star, read-only (lsrn.57): a mark the size of the pill's buttons, gold,
   and nothing at all on a rally that is not starred. Over the frame on the phone. */
.match-detail.mp2-player [data-mp2-mark] { display: inline-grid; place-items: center; flex: none; width: 36px; height: 36px; color: #e0a92e; }
.match-detail.mp2-player [data-mp2-mark]:empty { display: none; }
.match-detail.mp2-player [data-mp2-mark] > svg { width: 20px; height: 20px; }
.match-detail.mp2-player .mp2-cap-tools > [data-mp2-mark] { width: 38px; height: 38px; }
/* ...and on the clip, a small star beside the rally number. */
.match-detail.mp2-player .mp2-clip .mp2-star { font: normal 11px/20px var(--font-sans); color: #e0a92e; margin-left: 5px; }
.match-detail.mp2-player .mp2-clip b.sc .mp2-star { margin-left: auto; margin-right: 4px; }
/* One rally's download with no rally in the player. */
.match-detail.mp2-player :is(.mp2-dl, .mp2-menu-item):disabled { opacity: .4; cursor: default; }
@media (max-width: 1100px) {
  .match-detail.mp2[data-viewer="link"] #content > * { order: 3; }
  .match-detail.mp2[data-viewer="link"] #content > :is(.link-note, .cta-card) { order: 0; }
  .match-detail.mp2[data-viewer="link"] #content > #rallies-card { order: 1; }
  .match-detail.mp2[data-viewer="link"] #content > .md-header { order: 2; }
  .match-detail.mp2[data-viewer="link"] .md-header .eyebrow { display: none; }
}
/* The link holder's name row (lsrn.53, lsrn.63, prototypes/shared-match-v2/ section 6): the name, the status chip
   (ready included, as on the owner's page) and Export. One button in the row: it keeps its word at every column,
   out of the owner's icon-only steps below. */
@media (min-width: 1101px) {
  .match-detail.mp2[data-viewer="link"] #content > :is(.link-note, .cta-card) { order: 0; }
  .match-detail.mp2.mp2-player[data-viewer="link"]:not([data-mp2-no-frame]) { --mp2-col-w: calc(max(100vh - 338px - var(--mp2-ev-strip, 0px), 310px) * 16 / 9 + 14px); }
}
/* Read-only rows show the owner's favourite as a mark, not a toggle. */
.rl-starred { color: var(--accent); font-size: 13px; padding: 0 6px; }
/* Read-only, the star that pushed "next" to the right edge is gone. */
#rally-now-playing + #rally-next-btn { margin-left: auto; }

/* ── AI coach chat card (padel-website-k5og, ADR-071; v2: 7uq7.4) ──────────
   Built by static/js/ai-coach.js for a user enrolled in the `ai-coach-chat`
   flag on a match with a coach bundle, to prototypes/ai-coach-v2/. Palette
   tokens only, no literal colours (the glyphs are masks and boxes in the text
   colour). The card is its own container: what the spec draws at 760px and at
   390px is one set of rules and the container decides, so the same card sits
   in the match page's 420px rail, on a phone's own coach page, and in the
   page's column. The thread scrolls under the dock; the dock floats over it
   with a soft upward shadow. */
.coach-card {
  container: coach / inline-size;
  display: flex; flex-direction: column; min-width: 0; padding: 0; overflow: hidden;
}
.coach-head { display: flex; align-items: center; gap: 8px; height: 52px; flex: none; padding: 0 20px; border-bottom: 1px solid var(--border); }
.coach-title { margin: 0; font-size: 15px; font-weight: 500; letter-spacing: -0.01em; }
.coach-head .beta-tag { height: 20px; }
.coach-glyph { display: inline-block; flex: none; }

/* The thread: turns separated by a hairline. */
.coach-thread {
  flex: 1; min-height: 160px; max-height: 520px; overflow-y: auto; padding: 4px 24px 12px;
  overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--border-2) transparent;
}
.coach-turn { padding: 16px 0; }
.coach-turn + .coach-turn { border-top: 1px solid var(--border); }
.coach-restored {
  display: flex; align-items: center; gap: 10px; padding: 12px 0 4px; color: var(--muted);
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
}
.coach-restored::before, .coach-restored::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
/* The question: the one bubble, with a 4px tail corner. */
.coach-ask { display: flex; justify-content: flex-end; }
.coach-msg-user {
  max-width: 72%; padding: 7px 12px; border-radius: 14px 14px 4px 14px;
  background: var(--surface-3); color: var(--text); font-size: 13.5px; line-height: 1.55;
}
/* The answer: prose at the card's reading width beside the coach mark. */
.coach-reply { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 12px; }
.coach-ask + .coach-reply { margin-top: 12px; }
.coach-mark { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: var(--accent); color: var(--accent-ink); }
.coach-mark::before { content: ""; width: 13px; height: 13px; background: currentColor; clip-path: polygon(28% 16%, 28% 84%, 84% 50%); }
.coach-mark-quiet { background: var(--surface-3); color: var(--muted); }
.coach-msg-coach { min-width: 0; color: var(--text-2); font-size: 13.5px; line-height: 1.55; }
.coach-msg-coach > * + * { margin-top: 8px; }
.coach-msg-coach strong { color: var(--text); font-weight: 600; }
.coach-msg-coach em { font-style: italic; }
.coach-msg-coach code { padding: 1px 4px; border-radius: 4px; background: var(--surface-2); font-family: var(--font-mono); font-size: 12px; }
.coach-p { margin: 0; max-width: 66ch; white-space: pre-line; }
.coach-intro-you { color: var(--muted); }
.coach-h { margin: 0; color: var(--text); font-size: 15px; font-weight: 500; letter-spacing: -0.01em; line-height: 1.35; }
.coach-h + * { margin-top: 4px; }
.coach-list { margin: 0; padding-left: 20px; }
.coach-list li + li { margin-top: 3px; }
.coach-list-nested { margin-top: 3px; }
.coach-pre { margin: 0; padding: 10px 12px; overflow-x: auto; border-radius: 8px; background: var(--surface-2); border: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; }
.coach-pre code { padding: 0; background: none; }
/* A table sits in a hairline frame and scrolls inside it, so a wide one never widens the page. */
.coach-table-wrap { max-width: 100%; overflow-x: auto; border: 1px solid var(--border); border-radius: 10px; }
.coach-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.coach-table th {
  padding: 7px 12px; background: var(--surface-2); border-bottom: 1px solid var(--border); color: var(--muted);
  font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-align: left; text-transform: uppercase; white-space: nowrap;
}
.coach-table td { padding: 7px 12px; border-top: 1px solid var(--border); text-align: left; white-space: nowrap; }
.coach-table tbody tr:first-child td { border-top: 0; }
.coach-table td.coach-num { font-family: var(--font-mono); font-size: 12.5px; }
.coach-table .coach-num { text-align: right; }
/* A verified rally reference: a button in the text, with a dashed underline. */
.coach-cite {
  display: inline; padding: 0 1px; border: 0; border-bottom: 1px dashed var(--accent-line); border-radius: 4px 4px 0 0;
  background: transparent; color: var(--accent); font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; transition: background 0.12s;
}
.coach-cite:hover { background: var(--accent-soft); border-bottom-style: solid; }
.coach-note { display: flex; align-items: flex-start; gap: 8px; font-size: 12.5px; }
.coach-note::before { content: ""; flex: none; width: 13px; height: 13px; margin-top: 3px; background: currentColor; -webkit-mask: var(--icon-info) center / contain no-repeat; mask: var(--icon-info) center / contain no-repeat; }
/* A chart in an answer (7uq7.5, ai-coach-v2 README "Charts"): under a
   hairline, a mono uppercase title and the team legend, then the SVG that
   ai-coach.js lays out for this width. points_by_set is a butterfly: team A
   left of the axis, team B right, the undecided rallies a neutral bar on the
   axis on the same points scale. The legend's columns follow the drawing's
   axis (set inline); a narrow chart spreads it over one row. */
.coach-msg-coach > .coach-chart { margin: 12px 0; padding-top: 10px; border-top: 1px solid var(--border); }
.coach-chart-head { margin-bottom: 6px; color: var(--muted); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; }
.coach-chart-legend { display: grid; grid-template-columns: 1fr 128px 1fr; margin-bottom: 8px; color: var(--muted); font-family: var(--font-mono); font-size: 11px; line-height: 1.2; }
.coach-chart-key { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.coach-chart-key i { flex: none; width: 8px; height: 8px; border-radius: 2px; }
.coach-chart-key-a { justify-self: end; }
.coach-chart-key-u { justify-self: center; }
.coach-chart-key-a i { background: var(--team-a); }
.coach-chart-key-b i { background: var(--team-b); }
.coach-chart-key-u i { background: var(--surface-3); box-shadow: inset 0 0 0 1px var(--border-2); }
.coach-chart-narrow .coach-chart-legend { display: flex; justify-content: space-between; gap: 8px; }
.coach-chart-svg { display: block; width: 100%; height: auto; overflow: visible; font-family: var(--font-mono); font-size: 11px; font-variant-numeric: tabular-nums; }
.coach-chart-svg .coach-chart-a { fill: var(--team-a); }
.coach-chart-svg .coach-chart-b { fill: var(--team-b); }
.coach-chart-svg .coach-chart-u { fill: var(--surface-3); stroke: var(--border-2); stroke-width: 1; }
.coach-chart-svg .coach-chart-num { fill: var(--text); font-weight: 500; }
.coach-chart-svg .coach-chart-num.is-win { font-weight: 600; }
.coach-chart-svg .coach-chart-ucount { fill: var(--muted); }
.coach-chart-svg .coach-chart-set { fill: var(--muted); font-size: 10px; letter-spacing: 0.06em; }
.coach-chart-svg .is-focus .coach-chart-set { fill: var(--text); font-weight: 500; }
/* The other views (7uq7.6): serve_by_set and net_by_set are rows of stacked
   bars (a team's solid colour, its tint, the neutral undecided), win_by_length
   a share bar per rally length with its counts inside. A flat legend wraps
   instead of following an axis. */
.coach-chart-legend-flat { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.coach-chart-key-lost i { background: color-mix(in oklab, var(--muted) 30%, var(--surface)); }
.coach-chart-key-solid i { background: var(--text-2); }
.coach-chart-svg .coach-chart-a-l { fill: color-mix(in oklab, var(--team-a) 28%, var(--surface)); }
.coach-chart-svg .coach-chart-b-l { fill: color-mix(in oklab, var(--team-b) 28%, var(--surface)); }
.coach-chart-svg .coach-chart-note { font-weight: 400; }
/* The other two (7uq7.12): the zone ramp of the player cards, a team's colour
   at 40%, 70% and full, mixed into the surface so it holds in dark mode. */
.coach-chart-svg .coach-chart-zd-a { fill: color-mix(in oklab, var(--team-a) 40%, var(--surface)); }
.coach-chart-svg .coach-chart-zm-a { fill: color-mix(in oklab, var(--team-a) 70%, var(--surface)); }
.coach-chart-svg .coach-chart-zx-a { fill: var(--team-a); }
.coach-chart-svg .coach-chart-zd-b { fill: color-mix(in oklab, var(--team-b) 40%, var(--surface)); }
.coach-chart-svg .coach-chart-zm-b { fill: color-mix(in oklab, var(--team-b) 70%, var(--surface)); }
.coach-chart-svg .coach-chart-zx-b { fill: var(--team-b); }
.coach-chart-key-zd i { background: color-mix(in oklab, var(--text-2) 30%, var(--surface)); }
.coach-chart-key-zm i { background: color-mix(in oklab, var(--text-2) 60%, var(--surface)); }
.coach-chart-key-zx i { background: var(--text-2); }
.coach-chart-svg .on-a { fill: var(--surface); }
.coach-chart-svg .on-b { fill: var(--text); }
[data-theme="dark"] .coach-chart-svg .on-b { fill: var(--bg); }
/* Thinking: three dots and a label; still under reduced motion. */
.coach-thinking { display: flex; align-items: center; gap: 10px; min-height: 28px; color: var(--muted); }
.coach-thinking > * + * { margin-top: 0; }
.coach-dots { display: inline-flex; gap: 4px; }
.coach-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: coach-pulse 1.2s cubic-bezier(0.16, 1, 0.3, 1) infinite; }
.coach-dots i:nth-child(2) { animation-delay: 0.15s; }
.coach-dots i:nth-child(3) { animation-delay: 0.3s; }
@keyframes coach-pulse { 0%, 100% { opacity: 0.25; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-2px); } }
/* Couldn't answer: a quiet mark, the line, and a ghost Try again when a retry can work. */
.coach-error { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.coach-retry {
  display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; border: 1px solid var(--border-2); border-radius: 8px;
  background: var(--surface); color: var(--text-2); font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; transition: border-color 0.12s, color 0.12s;
}
.coach-retry::before { content: ""; width: 13px; height: 13px; background: currentColor; -webkit-mask: var(--icon-retry) center / contain no-repeat; mask: var(--icon-retry) center / contain no-repeat; }
.coach-retry:hover { border-color: var(--muted-2); color: var(--text); }
/* Declined: a grey tag over the refusal (v1's coloured side border is gone). */
.coach-declined { display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px; border-radius: 6px; background: var(--surface-3); color: var(--muted); font-size: 12px; font-weight: 500; }
.coach-declined::before { content: ""; width: 11px; height: 11px; background: currentColor; -webkit-mask: var(--icon-decline) center / contain no-repeat; mask: var(--icon-decline) center / contain no-repeat; }
/* Stopped: the question stays, a muted line under it, nothing answers. */
.coach-stopped { display: flex; justify-content: flex-end; align-items: center; gap: 6px; margin: 6px 0 0; color: var(--muted); font-size: 11px; }
.coach-glyph-stop { width: 10px; height: 10px; border-radius: 2px; background: currentColor; }

/* The dock: chips, the composer, the footnote. It floats over the thread. */
.coach-dock { position: relative; padding: 10px 20px 14px; background: var(--surface); box-shadow: 0 -14px 24px -16px color-mix(in oklab, var(--text) 14%, transparent); }
.coach-intro .coach-suggest { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; margin: 14px 0 0 34px; }
.coach-suggest-label { margin-bottom: 4px; font: 500 10.5px var(--font-mono, monospace); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-2); }
.coach-suggest-chip {
  display: flex; align-items: flex-start; gap: 8px; width: calc(100% + 10px); margin-left: -10px; padding: 7px 10px; border: 0; border-radius: 8px;
  background: none; color: var(--text-2); font-family: var(--font-sans); font-size: 13.5px; line-height: 1.4; text-align: left; cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.coach-suggest-chip::before { content: ""; flex: none; width: 12px; height: 12px; margin-top: 3px; background: var(--muted-2); -webkit-mask: var(--icon-arrow-right) center / contain no-repeat; mask: var(--icon-arrow-right) center / contain no-repeat; }
.coach-suggest-chip:hover { background: var(--surface-2); color: var(--text); }
.coach-suggest-more { margin: 4px 0 0 -6px; padding: 3px 6px; border: 0; border-radius: 6px; background: none; color: var(--accent); font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; }
.coach-suggest-more:hover { background: var(--accent-soft); }
/* The composer: one frame, a halo when it has focus (the field inside never gets its own outline). */
.coach-form {
  position: relative; display: block; border: 1px solid var(--border-2); border-radius: 14px; background: var(--surface);
  transition: box-shadow 0.18s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
.coach-form:focus-within { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
/* Context (7uq7.7, prototypes/match-page-v2/ "Context"): chips over the field,
   "@Set 2" in mono on surface-3 with a cross; a dashed one suggests the rally
   in the player. The row is absent while empty. */
.coach-att { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 10px 0; }
.coach-att:empty { display: none; }
.coach-ctx {
  display: inline-flex; align-items: center; gap: 4px; max-width: 100%; height: 26px; padding: 0 3px 0 9px;
  border: 0; border-radius: 6px; background: var(--surface-3); color: var(--text-2);
  font-family: var(--font-mono); font-size: 11.5px; white-space: nowrap;
}
.coach-ctx-name { overflow: hidden; text-overflow: ellipsis; }
.coach-ctx-at { font-style: normal; color: var(--muted-2); }
/* A sent question's context, over its text in the user's bubble. */
.coach-msg-ctx { display: flex; flex-wrap: wrap; gap: 4px; margin: 2px 0 4px; }
.coach-msg-ctx .coach-ctx { height: 22px; padding: 0 7px; background: var(--surface); }
.coach-ctx-x {
  flex: none; width: 20px; height: 20px; padding: 0; border: 0; border-radius: 4px; display: grid; place-items: center;
  background: none; color: var(--muted); cursor: pointer;
}
.coach-ctx-x::before { content: ""; width: 11px; height: 11px; background: currentColor; -webkit-mask: var(--icon-x) center / contain no-repeat; mask: var(--icon-x) center / contain no-repeat; }
.coach-ctx-x:hover { background: var(--border); color: var(--text); }
.coach-ctx-sug { padding: 0 9px 0 7px; border: 1px dashed var(--border-2); background: none; color: var(--muted); cursor: pointer; transition: color 0.12s, border-color 0.12s; }
.coach-ctx-sug:hover { border-color: var(--accent-line); color: var(--text); }
/* The chip "Ask about this rally" just added: one accent pulse, the accent-soft
   fill and a 1.5px accent ring fading to the chip's own look over 1.2s. */
@keyframes coach-ctx-pulse {
  from { background: var(--accent-soft); box-shadow: 0 0 0 1.5px var(--accent); }
  to { background: var(--surface-3); box-shadow: 0 0 0 1.5px transparent; }
}
.coach-ctx-new { animation: coach-ctx-pulse 1.2s ease-out 1; }
@media (prefers-reduced-motion: reduce) { .coach-ctx-new { animation: none; } }
/* The tool row's "@ Context" button: the same menu an @ typed opens. */
.coach-ctx-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 5px; height: 30px; min-width: 30px;
  margin: 0 2px 0 -8px; padding: 0 9px 0 7px; border: 0; border-radius: 8px;
  background: none; color: var(--muted); font: inherit; font-size: 12.5px; cursor: pointer; transition: background 0.12s, color 0.12s;
}
.coach-ctx-btn b { font-family: var(--font-mono); font-size: 15px; font-weight: 500; line-height: 1; color: var(--text-2); }
.coach-ctx-btn:hover, .coach-ctx-btn[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
/* The @ menu: as wide as the composer, opening upwards. The rally in the
   player first, then the match to browse (sets › games › rallies), or the
   results for what follows the @. */
.coach-card {
  --coach-icon-next: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 3.5 10.5 8 6 12.5'/%3E%3C/svg%3E");
  --coach-icon-prev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 3.5 5.5 8 10 12.5'/%3E%3C/svg%3E");
}
.coach-menu {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 8px); z-index: 5; max-height: 360px; overflow-y: auto;
  padding: 4px; border-radius: 12px; background: var(--surface); overscroll-behavior: contain;
  box-shadow: 0 0 0 1px var(--border-2), 0 18px 40px -14px color-mix(in oklab, var(--text) 35%, transparent);
  scrollbar-width: thin; scrollbar-color: var(--border-2) transparent;
}
.coach-mhd {
  display: flex; align-items: center; gap: 4px; min-height: 28px; padding: 6px 10px 2px; overflow: hidden;
  color: var(--muted-2); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  white-space: nowrap; text-overflow: ellipsis;
}
.coach-mhd b { overflow: hidden; text-overflow: ellipsis; font-weight: 500; color: var(--muted); }
.coach-mback {
  display: inline-flex; align-items: center; gap: 4px; max-width: 100%; height: 26px; margin-left: -6px; padding: 0 8px 0 4px;
  overflow: hidden; border: 0; border-radius: 6px; background: none; color: var(--text-2);
  font-family: var(--font-mono); font-size: 12px; font-weight: 500; letter-spacing: 0; text-transform: none; white-space: nowrap; cursor: pointer;
}
.coach-mback::before { content: ""; flex: none; width: 14px; height: 14px; background: currentColor; -webkit-mask: var(--coach-icon-prev) center / contain no-repeat; mask: var(--coach-icon-prev) center / contain no-repeat; }
.coach-mback:hover { background: var(--surface-2); }
.coach-mrow { display: flex; gap: 2px; }
.coach-madd {
  flex: 1; min-width: 0; display: grid; grid-template-columns: 14px minmax(0, 1fr) auto; align-items: center; gap: 8px;
  height: 34px; padding: 0 10px 0 8px; border: 0; border-radius: 6px; background: none; color: var(--text-2);
  font: inherit; font-size: 13px; text-align: left; white-space: nowrap; cursor: pointer;
}
/* A ticked row is added already: picking it again takes it out. */
.coach-madd::before { content: ""; width: 12px; height: 12px; }
.coach-madd.is-on { color: var(--text); font-weight: 500; }
.coach-madd.is-on::before { background: var(--accent); -webkit-mask: var(--icon-check) center / contain no-repeat; mask: var(--icon-check) center / contain no-repeat; }
.coach-mlabel { overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.coach-mnote { color: var(--muted); font-family: var(--font-mono); font-size: 10.5px; font-style: normal; font-weight: 400; }
.coach-mdot { display: inline-block; width: 7px; height: 7px; margin-right: 8px; border-radius: 2px; vertical-align: 1px; background: var(--border-2); }
.coach-mdot-a { background: var(--team-a); }
.coach-mdot-b { background: var(--team-b); }
.coach-mopen { flex: none; width: 32px; height: 34px; padding: 0; border: 0; border-radius: 6px; display: grid; place-items: center; background: none; color: var(--muted); cursor: pointer; }
.coach-mopen::before { content: ""; width: 14px; height: 14px; background: currentColor; -webkit-mask: var(--coach-icon-next) center / contain no-repeat; mask: var(--coach-icon-next) center / contain no-repeat; }
.coach-madd:hover, .coach-madd:focus-visible, .coach-madd.is-hi, .coach-mopen:hover, .coach-mopen:focus-visible { background: var(--surface-2); color: var(--text); outline: 0; }
.coach-mnone { margin: 0; padding: 10px 10px 12px; color: var(--muted); font-size: 13px; }
/* The menu's status (role=status, ai-coach.js): heard, never seen. */
.coach-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.coach-mft {
  position: sticky; bottom: -4px; margin: 4px -4px -4px; padding: 7px 12px; border-top: 1px solid var(--border);
  background: var(--surface); color: var(--muted-2); font-size: 10.5px;
}
.coach-input {
  display: block; width: 100%; min-height: 0; max-height: 168px; padding: 9px 14px 2px; overflow-y: auto; resize: none;
  border: 0; border-radius: 0; outline: 0; background: transparent; color: var(--text); caret-color: var(--accent);
  font: inherit; font-size: 14px; line-height: 1.5;
}
.coach-input:focus { outline: none; box-shadow: none; }
.coach-input::placeholder { color: var(--muted); }
.coach-tools { display: flex; align-items: center; gap: 8px; padding: 4px 8px 8px 14px; }
.coach-hint { color: var(--muted); font-size: 11px; }
/* Send: grey and asleep with nothing to ask, Court Green when there is a question, ink with a square (Stop) while the coach answers. */
.coach-send {
  margin-left: auto; width: 34px; height: 34px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface-3); color: var(--muted-2); cursor: pointer;
  transition: background 0.16s cubic-bezier(0.16, 1, 0.3, 1), color 0.16s cubic-bezier(0.16, 1, 0.3, 1), transform 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}
.coach-send:disabled { cursor: default; }
.coach-send.is-ready { background: var(--accent); color: var(--accent-ink); }
.coach-send.is-ready:hover { transform: translateY(-1px); }
.coach-send.is-ready:active { transform: scale(0.94); }
.coach-send.is-busy { background: var(--text); color: var(--surface); }
.coach-glyph-send { width: 16px; height: 16px; background: currentColor; -webkit-mask: var(--icon-up) center / contain no-repeat; mask: var(--icon-up) center / contain no-repeat; }
.coach-send.is-busy .coach-glyph-send { width: 10px; height: 10px; border-radius: 2px; -webkit-mask: none; mask: none; }
.coach-foot { margin: 8px 0 0; font-size: 12px; }

/* Phone width: the same card, its container decides. 400px, not the
   ai-coach-v2 mockup's 560: the match-page-v2 rail is 420px (the card 419px
   inside its border) and is drawn with the desktop card, a 390px phone with
   this one (prototypes/ai-coach-v2/README.md, "Phone"). */
@container coach (max-width: 400px) {
  .coach-head { padding: 0 14px; }
  .coach-thread { padding: 4px 14px 10px; }
  .coach-msg-user { max-width: 86%; }
  .coach-reply { grid-template-columns: 22px minmax(0, 1fr); gap: 10px; }
  .coach-mark { width: 22px; height: 22px; border-radius: 6px; }
  .coach-mark::before { width: 11px; height: 11px; }
  .coach-dock { padding: 8px 12px 12px; }
  .coach-input { font-size: 16px; }
  .coach-hint { display: none; }
  /* A phone has no arrow keys to teach. */
  .coach-mft { display: none; }
}
/* A wider phone (a 430px one) gets the desktop card, but iOS Safari still
   zooms into a field under 16px on focus. */
@media (pointer: coarse) {
  .coach-input { font-size: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .coach-dots i { animation: none; opacity: 0.6; }
  .coach-card * { transition: none !important; }
}

/* ── Vertical 9:16 rally export (padel-website-07se.2) ─────────────────────
   Spec: prototypes/rally-vertical-export/. The block under the 16:9 file row
   of the rally share modal (video-detail-vertical.js shows one state of it at
   a time), and the ready dot on a rally row's share button. */
.rsm-vertical { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
.rsv-pending {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 0;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text-2);
}
.rsv-spinner {
  width: 12px;
  height: 12px;
  border: 2px solid var(--surface-3);
  border-top-color: var(--accent);
  border-radius: 50%;
}
/* After .muted in the file, so the error colour wins on the shared note. */
.rsv-error { color: var(--danger); }
/* The badge takes no width: the row's columns are measured to the pixel on
   the phone and side-by-side layouts (see the 720px block above), and a text
   pill would push the download button into a horizontal scroll. */
.rl-share.has-vertical { position: relative; }
.rl-share.has-vertical::after {
  content: "";
  position: absolute;
  top: -3px;
  right: -3px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg);
}

.email-preference .muted { font-size: var(--fs-sub); margin: 0; }

/* The pack page, /packs (8lu9.5; spec prototypes/hour-packs/, the picker redrawn in
   8lu9.15). Your balance, then three tiles as one radio group with one Top up at the
   foot, the terms under them; and the page Checkout returns to. js/packs-page.js,
   packs-picker.js and packs-done.js draw it; every class is pk-*. */
.pk-screen { max-width: 696px; display: flex; flex-direction: column; gap: 22px; container-type: inline-size; }
.pk-screen > .h1[hidden] { display: none; }
.pk-app { display: flex; flex-direction: column; gap: 22px; }
.pk-fail { margin: 0; color: var(--text-2); font-size: var(--fs-body); }
.pk-unavailable { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; }
.pk-legal { margin: 0; font-size: var(--fs-sub); color: var(--muted); line-height: 1.6; max-width: 62ch; }
.pk-legal a { white-space: nowrap; color: var(--accent); text-underline-offset: 3px; }
.pk-legal[hidden] { display: none; }

.pk-banner { display: flex; gap: 10px; align-items: flex-start; padding: 11px 13px; border-radius: var(--radius); font-size: var(--fs-sub); line-height: 1.5; color: var(--text-2); }
.pk-banner b { font-weight: 500; color: var(--text); }
.pk-banner > svg { flex: none; width: 16px; height: 16px; margin-top: 1px; color: var(--muted); }
.pk-banner-warn > svg { color: var(--warn); }
.pk-banner-warn { background: color-mix(in oklab, var(--warn) 9%, var(--surface)); border: 1px solid color-mix(in oklab, var(--warn) 30%, var(--border)); }
.pk-banner-plain { background: var(--surface-2); border: 1px solid var(--border); }

.pk-sheet { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-2); overflow: clip; }
.pk-sec { padding: 20px 22px; }
.pk-sec-title { font-size: var(--fs-head); font-weight: 500; letter-spacing: -0.014em; margin: 0; }
.pk-bal-fig { margin-top: 12px; }
.pk-ded { list-style: none; margin: 14px 0 0; padding: 0; }
.pk-ded li { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; padding: 9px 0; border-top: 1px dashed var(--border-2); align-items: start; }
.pk-ded li:first-child { border-top: 0; padding-top: 0; }
.pk-src { display: flex; align-items: flex-start; font-size: var(--fs-body); color: var(--text-2); min-width: 0; }
.pk-src small { display: block; color: var(--muted); font-size: var(--fs-sub); margin-top: 1px; line-height: 1.4; }
.pk-ded li.pk-debt .pk-src { color: var(--danger); }
.pk-ded li.pk-debt .pk-src small { color: var(--text-2); }
.pk-key { display: inline-block; width: 9px; height: 9px; border-radius: 2px; flex: none; margin: 5px 7px 0 0; background: var(--muted-2); }
.pk-key-free { background: var(--accent); }
.pk-key-old { background: var(--muted); }
.pk-key-new { background: var(--muted-2); }
.pk-key-debt { background: var(--danger); }
.pk-num { font: var(--fs-sub) var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }
.pk-num.pk-neg { color: var(--danger); }
.pk-num.pk-pos { color: var(--accent); }

.pk-qty { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text); }
.pk-qty .pk-d { font-weight: 600; }
.pk-qty .pk-d + .pk-u { margin-left: 0.14em; }
.pk-qty .pk-u + .pk-d { margin-left: 0.26em; }
.pk-qty .pk-u { color: var(--muted); font-weight: 400; letter-spacing: 0.02em; }
.pk-qty-xl { font-size: 32px; letter-spacing: -0.03em; line-height: 1; }
.pk-qty-xl .pk-u { font-size: var(--fs-body); }
.pk-qty-lg { font-size: 28px; letter-spacing: -0.03em; line-height: 1; }
.pk-qty-lg .pk-u { font-size: var(--fs-sub); }

/* The picker: a native radio group, each tile a <label> around a visually hidden
   radio; selected = the accent wash and line plus a filled mark, so it reads
   without colour; the tiles share rows (subgrid) so the prices line up. */
.pk-pick { border: 0; margin: 0; padding: 0; min-width: 0; }
.pk-pick > legend { float: left; width: 100%; padding: 20px 22px 14px; margin: 0; }
.pk-pick > legend + * { clear: both; }
.pk-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.pk-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: auto auto auto auto; gap: 10px; padding: 0 22px 18px; }
.pk-tile { position: relative; grid-row: 1 / span 4; display: grid; grid-template-rows: subgrid; row-gap: 0; justify-items: start; align-content: start; padding: 18px 14px 16px; border: 1px solid var(--border-2); border-radius: var(--radius); background: var(--surface); min-width: 0; cursor: pointer; transition: background-color 120ms ease, box-shadow 120ms ease, border-color 120ms ease; -webkit-tap-highlight-color: transparent; }
.pk-tile:has(.pk-sr:checked) { background: var(--accent-soft); border-color: var(--accent); }
.pk-tile:has(.pk-sr:focus-visible) { box-shadow: inset 0 0 0 2px var(--accent); }
.pk-tile:has(.pk-sr:disabled) { cursor: default; }
.pk-tile:has(.pk-sr:disabled:not(:checked)) { opacity: 0.55; }
@media (hover: hover) and (pointer: fine) {
  .pk-tile:not(:has(.pk-sr:checked)):not(:has(.pk-sr:disabled)):hover { border-color: var(--muted-2); }
}
.pk-mk { position: absolute; top: 12px; right: 12px; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--border-2); background: var(--surface); display: inline-flex; align-items: center; justify-content: center; color: var(--accent-ink); transition: background-color 120ms ease, border-color 120ms ease; }
.pk-mk svg { width: 11px; height: 11px; opacity: 0; }
.pk-tile:has(.pk-sr:checked) .pk-mk { background: var(--accent); border-color: var(--accent); }
.pk-tile:has(.pk-sr:checked) .pk-mk svg { opacity: 1; }
.pk-fig { grid-row: 1; padding-right: 26px; }
.pk-chips { grid-row: 2; margin-top: 8px; }
.pk-chip { height: auto; min-height: 24px; padding: 3px 9px 3px 7px; line-height: 1.25; align-items: flex-start; white-space: normal; font-family: var(--font-sans); font-size: var(--fs-sub); font-weight: 500; border-radius: 7px; }
.pk-chip svg { width: 13px; height: 13px; flex: none; margin-top: 1px; }
.pk-chip-entry .pk-ct-one { display: none; }
.pk-chip-entry .pk-ct-two > span { display: inline; white-space: nowrap; }
.pk-chip-entry.pk-chip-two { gap: 4px; padding-right: 8px; }
.pk-chip-entry.pk-chip-two .pk-ct-two > span { display: block; }
.pk-price { grid-row: 3; margin-top: 10px; line-height: 1.15; }
.pk-price .pk-now { font: 500 17px var(--font-sans); letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.pk-price .pk-was { display: block; font-size: var(--fs-sub); color: var(--muted); text-decoration: line-through; font-variant-numeric: tabular-nums; margin-bottom: 2px; }
.pk-price small { display: block; margin-top: 3px; font: var(--fs-meta) var(--font-mono); color: var(--muted); font-variant-numeric: tabular-nums; }
.pk-sub { grid-row: 4; margin-top: 12px; padding-top: 9px; border-top: 1px dashed var(--border-2); justify-self: stretch; align-self: stretch; font-size: var(--fs-sub); color: var(--muted); line-height: 1.5; }
.pk-sub > span { display: block; }
.pk-sub > span + span { margin-top: 2px; }
.pk-sub b { font-weight: 500; color: var(--text); white-space: nowrap; }
.pk-foot { display: flex; align-items: center; justify-content: flex-end; gap: 14px; padding: 14px 22px 18px; border-top: 1px solid var(--border); }
.pk-status { margin-right: auto; font-size: var(--fs-sub); color: var(--muted); line-height: 1.45; }
.pk-status b { font-weight: 500; color: var(--text); }
.pk-status[hidden] { display: none; }
.pk-foot .pk-topup { min-width: 184px; }
/* Loading in place (frrx.22; prototypes/library-skeletons/): the balance sheet,
   the picker and the result card are drawn by the template on their real
   markup, bars in boxes of the real line heights, and packs-page.js replaces
   them. The bars are inline in the real line (.sk-text, a zero-width space). */
.sk-fig .sk-bar { display: inline-block; vertical-align: middle; }
.sk-fig .sk-bar, .pk-done-title.sk-text .sk-bar { height: 0.72em; border-radius: 6px; }
.sk-key { background: var(--surface-3); animation: sk-breath 1.8s ease-in-out infinite; }
.sk-chip { --w: 64px; height: 24px; }
.pk-done-head .sk-chip { margin-top: 4px; flex: none; }
.sk-chip.sk-pill { display: block; border-radius: 7px; }
.sk-btn { width: var(--w); background: var(--surface-3); border-color: transparent; color: transparent; pointer-events: none; animation: sk-breath 1.8s ease-in-out infinite; }
.sk-tile { cursor: default; pointer-events: none; }
.pk-sk-status { margin: 0; font-size: var(--fs-sub); color: var(--muted); }
/* The 4 s line never adds a row of its own: it shares the picker's heading row, or,
   with no tile to offer, the unavailable box's Try again row (frrx.35), which
   unavailable() in packs-page.js draws as the button alone. */
.pk-pick > legend:has(.pk-sk-slow) { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.pk-sk-row { display: flex; align-items: center; gap: 12px; align-self: stretch; min-width: 0; }
.pk-sk-row > .sk-btn { flex: none; }
.pk-sk-slow { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .sk-key, .sk-btn { animation: none; } }
.pk-btn-spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent; flex: none; }
@media (prefers-reduced-motion: reduce) { .pk-btn-spin { animation: none; } }

/* The page Checkout returns to. */
.pk-done { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-2); padding: 34px 30px 28px; }
.pk-done-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.pk-done-head .chip { margin-top: 4px; flex: none; }
.pk-done-head .chip svg { width: 12px; height: 12px; flex: none; }
.pk-done-title { font-size: 26px; font-weight: 500; letter-spacing: -0.022em; line-height: 1.15; margin: 0; }
.pk-done-body { margin: 8px 0 0; color: var(--muted); font-size: var(--fs-head); line-height: 1.5; max-width: 44ch; }
.pk-rc { list-style: none; margin: 22px 0 0; padding: 0; border-top: 1px solid var(--border); }
.pk-rc li { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 10px 0; border-bottom: 1px dashed var(--border-2); font-size: var(--fs-body); color: var(--text-2); }
.pk-rc li:last-child { border-bottom: 0; font-weight: 500; color: var(--text); }
.pk-later { font: var(--fs-sub) var(--font-mono); color: var(--muted); display: inline-flex; align-items: center; gap: 7px; }
.pk-flow { margin-top: 18px; display: flex; align-items: flex-end; flex-wrap: wrap; gap: 6px 14px; padding: 16px 18px; background: var(--bg-2); border-radius: var(--radius); }
.pk-flow-fig { display: flex; flex-direction: column; gap: 6px; }
.pk-flow-cap { font-size: var(--fs-sub); color: var(--muted); }
.pk-done-cta { margin-top: 24px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.pk-done-note { margin: 18px 0 0; font-size: var(--fs-sub); color: var(--muted); line-height: 1.55; }
/* The sum resolving into Balance now (8lu9.38, prototypes/pack-added-motion): old and
   new figure in one cell, the token landing beside them, the wash under the box.
   Before the beat only the old figure shows; then playAddedSum's fills take over,
   and once it has run only the new figure is left. */
.pk-flow:has(.pk-sum-wash) { position: relative; isolation: isolate; }
.pk-sum-wash { position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-line); opacity: 0; pointer-events: none; }
.pk-sum { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.pk-sum-stack { display: inline-grid; align-items: end; }
.pk-sum-stack > * { grid-area: 1 / 1; }
.pk-sum-new { font-weight: 600; transform-origin: 0 80%; }
.pk-sum-new .pk-d { font-weight: inherit; }
.pk-sum-tok { font: 600 22px var(--font-mono); color: var(--accent); letter-spacing: -0.02em; white-space: nowrap; opacity: 0; transform-origin: 0 50%; }
.pk-sum-pre .pk-sum-new { opacity: 0; }

@container (max-width: 520px) {
  .pk-sec { padding: 18px 16px; }
  .pk-pick > legend { padding: 18px 16px 12px; }
  .pk-tiles { grid-template-columns: 1fr; grid-template-rows: none; padding: 0 16px 16px; }
  .pk-tile { grid-row: auto; display: flex; flex-direction: column; align-items: flex-start; padding: 16px 16px 14px; }
  .pk-chip { align-items: center; }
  .pk-chip svg { margin-top: 0; }
  .pk-chip-entry .pk-ct-one { display: inline; }
  .pk-chip-entry .pk-ct-two { display: none; }
  .pk-chip-entry.pk-chip-two { padding-right: 9px; }
  .pk-foot { flex-direction: column; align-items: stretch; padding: 14px 16px 16px; gap: 10px; }
  .pk-status { margin-right: 0; text-align: center; }
  .pk-foot .pk-topup { min-width: 0; width: 100%; }
  .pk-done { padding: 26px 18px 22px; }
  .pk-done-title { font-size: 23px; }
  .pk-done-cta .btn { flex: 1 1 100%; }
  .pk-sum-tok { font-size: 18px; }
}

/* ── The player stats page (padel-website-1eg3; spec prototypes/player-stats,
   v2). `pst-` keeps clear of the Performance score's `ps-`. Tokens only: the
   team colours, the stroke palette and the status colours are the ones the
   match page already uses, so the page follows the theme for free. ── */
.pst-page { max-width: 1080px; margin: auto; }
.pst-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 8px; }
.pst-hero { display: grid; grid-template-columns: auto 1fr; gap: 18px; align-items: center; padding: 18px 20px; margin-top: 16px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--shadow-1); }
.pst-photo { width: 64px; height: 118px; object-fit: cover; border-radius: 10px; border: 1px solid var(--border-2); background: var(--surface-3); }
.pst-hero-sub { color: var(--muted); margin-top: 4px; font-size: 13.5px; }
.pst-ctx { color: var(--muted); font-size: 13px; margin-top: 8px; }
.pst-card { margin-top: 18px; padding: 18px 20px; box-shadow: var(--shadow-1); }
.pst-card h2 { margin: 2px 0 0; font-size: 18px; font-weight: 650; letter-spacing: -0.01em; }
.pst-card h3 { margin: 0; font-size: 16px; font-weight: 650; display: flex; align-items: baseline; gap: 8px; }
.pst-card h3 small { font-weight: 400; color: var(--muted); font-size: 12.5px; font-family: var(--font-mono); }
.pst-card h3 i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; align-self: center; }
.pst-lede { margin: 4px 0 0; color: var(--muted); font-size: 13.5px; max-width: 70ch; }
.pst-sub-h { font-size: 13px; font-weight: 600; color: var(--text-2); margin: 0 0 8px; }
.pst-sub-h small { font-weight: 400; color: var(--muted); margin-left: 6px; }
.pst-note { font-size: 12px; color: var(--muted); margin-top: 8px; }
summary.pst-note { cursor: pointer; }
.pst-obs { margin: 12px 0 0; font-size: 14.5px; color: var(--text); }
.pst-obs a, .pst-links a, .pst-note a { color: var(--accent); font-weight: 600; text-decoration: none; cursor: pointer; }
.pst-obs a:hover, .pst-links a:hover, .pst-note a:hover { text-decoration: underline; }
.pst-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 16px; }
.pst-sc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 14px; }
.pst-sc { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 14px 16px; }
.pst-sc .pst-links { margin-top: 8px; font-size: 12.5px; color: var(--muted); }
.pst-zone { margin-top: 6px; font-size: 12.5px; color: var(--muted); }
table.pst-t { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13.5px; }
table.pst-t th { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-2); font-weight: 400; text-align: right; padding: 2px 6px; }
table.pst-t th:first-child { text-align: left; }
table.pst-t th small { display: block; font-size: 9.5px; letter-spacing: 0; text-transform: none; color: var(--muted-2); }
table.pst-t td { padding: 7px 4px; border-top: 1px solid var(--border); text-align: right; font-variant-numeric: tabular-nums; }
table.pst-t td:first-child { text-align: left; color: var(--text-2); }
table.pst-t td.you { font-weight: 650; font-size: 17px; color: var(--text); }
table.pst-t td.oth { color: var(--text-2); }
table.pst-t .few { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted-2); margin-left: 6px; font-weight: 400; }
.pst-court-wrap { display: grid; grid-template-columns: auto 1fr; gap: 18px; align-items: start; margin-top: 14px; }
svg.pst-court { width: 260px; height: auto; display: block; }
.pst-court-leg { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin-top: 6px; }
.pst-court-leg i { display: inline-block; width: 40px; height: 8px; border-radius: 3px; background: linear-gradient(90deg, var(--surface-3), var(--team-c)); }
.pst-pbar { display: flex; gap: 2px; height: 20px; border-radius: 5px; overflow: hidden; }
.pst-pbar span { display: flex; align-items: center; justify-content: center; min-width: 0; overflow: hidden; font-size: 12px; font-weight: 600; color: #fff; text-shadow: 0 0 2px rgba(0, 0, 0, 0.55); }
.pst-leg { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 7px; }
.pst-leg span { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; font-weight: 600; color: var(--text-2); }
.pst-leg i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.pst-leg b { font-weight: 500; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); }
.pst-sw-won { background: var(--accent); }
.pst-sw-lost { background: color-mix(in oklab, var(--danger) 55%, var(--surface-3)); }
.pst-sw-none { background: var(--surface-3); border: 1px solid var(--border-2); }
.pst-rows { display: grid; gap: 7px; margin-top: 8px; }
.pst-row { display: grid; grid-template-columns: 150px 1fr 64px; gap: 10px; align-items: center; font-size: 13px; }
.pst-row .k { color: var(--text-2); }
.pst-row .k small { color: var(--muted); font-family: var(--font-mono); font-size: 10.5px; margin-left: 4px; }
.pst-row .v { text-align: right; font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
.pst-row .v b { color: var(--text); font-weight: 600; }
.pst-wl { display: flex; gap: 2px; height: 12px; border-radius: 4px; overflow: hidden; background: var(--surface-3); }
.pst-wl i { display: block; height: 100%; }
.pst-wl .w { background: var(--accent); }
.pst-wl .l { background: color-mix(in oklab, var(--danger) 55%, var(--surface-3)); }
.pst-nb { display: flex; align-items: center; height: 12px; border-radius: 4px; overflow: hidden; background: var(--surface-3); }
.pst-nb i { display: block; height: 100%; background: var(--accent); opacity: 0.8; }
.pst-stack { display: flex; gap: 2px; height: 22px; border-radius: 5px; overflow: hidden; margin-top: 6px; }
.pst-stack span { display: flex; align-items: center; justify-content: center; min-width: 0; overflow: hidden; font-size: 12px; font-weight: 600; color: #fff; text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); }
.pst-stack .w { flex: var(--w); background: var(--accent); }
.pst-stack .l { flex: var(--l); background: color-mix(in oklab, var(--danger) 55%, var(--surface-3)); }
.pst-stack .u { flex: var(--u); background: var(--surface-3); color: var(--muted); text-shadow: none; }
.pst-kv { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; font-size: 13px; margin-top: 10px; max-width: 420px; }
.pst-kv .k { color: var(--text-2); }
.pst-kv .v { font-family: var(--font-mono); font-size: 12px; color: var(--muted); text-align: right; }
.pst-kv .v b { color: var(--text); font-weight: 600; }
.pst-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.pst-chips button { font: inherit; font-size: 12.5px; padding: 5px 11px; border-radius: 999px; border: 1px solid var(--border-2); background: var(--surface); color: var(--text-2); cursor: pointer; }
.pst-chips button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--text); }
.pst-gap { width: 8px; }
.pst-rlist { display: grid; gap: 4px; margin-top: 10px; }
.pst-rl { display: grid; grid-template-columns: 52px 60px 48px 170px 1fr auto; gap: 10px; align-items: center; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface-2); font-size: 12.5px; }
.pst-rl .n { font-weight: 600; }
.pst-rl .t, .pst-rl .s { font-family: var(--font-mono); color: var(--muted); font-size: 11.5px; }
.pst-rl .end { font-weight: 500; }
.pst-rl .end i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; }
.pst-rl .seq { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pst-more { margin-top: 8px; font-size: 12.5px; color: var(--muted); }
.pst-sets { display: grid; grid-template-columns: 150px repeat(auto-fit, minmax(80px, 1fr)); gap: 6px 10px; align-items: center; margin-top: 14px; font-size: 12.5px; }
.pst-sets .h { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-2); text-align: center; }
.pst-sets .h small { display: block; letter-spacing: 0; text-transform: none; color: var(--muted); }
.pst-sets .k { color: var(--text-2); }
.pst-sets .c { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.pst-sets .c svg { width: 100%; height: 30px; display: block; }
.pst-sets .c b { font-family: var(--font-mono); font-weight: 500; font-size: 11.5px; color: var(--text); }
details.pst-more-detail { margin-top: 18px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); }
details.pst-more-detail > summary { cursor: pointer; padding: 14px 20px; font-weight: 600; font-size: 15px; list-style: none; display: flex; align-items: center; gap: 10px; }
details.pst-more-detail > summary::before { content: '▸'; color: var(--muted); font-size: 13px; }
details.pst-more-detail[open] > summary::before { content: '▾'; }
details.pst-more-detail > summary small { font-weight: 400; color: var(--muted); font-size: 12.5px; }
details.pst-more-detail > .body { padding: 0 20px 18px; }
details.pst-more-detail .pst-card { margin-top: 12px; box-shadow: none; }
.pst-cav { margin-top: 18px; font-size: 12px; color: var(--muted); border-top: 1px solid var(--border); padding-top: 12px; }
.pst-cav b { color: var(--text-2); font-weight: 600; }
.pst-video { width: 100%; background: #000; border-radius: 12px; }
/* The "Player stats →" link on a match-page player card (player-stats-link.js). */
.pl-stats-link { display: block; text-align: center; margin-top: 8px; font-size: 12.5px; font-weight: 600; color: var(--accent); text-decoration: none; }
.pl-stats-link:hover { text-decoration: underline; }
[data-theme="dark"] .pst-pbar span, [data-theme="dark"] .pst-stack span { color: rgba(0, 0, 0, 0.82); text-shadow: none; }
@media (max-width: 900px) {
  .pst-grid2, .pst-court-wrap { grid-template-columns: 1fr; }
  .pst-rl { grid-template-columns: 52px 60px 1fr auto; }
  .pst-rl .seq, .pst-rl .s { display: none; }
}
@media (max-width: 700px) {
  .pst-sc-grid { grid-template-columns: 1fr; }
  .pst-row { grid-template-columns: 110px 1fr 56px; }
  .pst-sets { grid-template-columns: 90px repeat(auto-fit, minmax(56px, 1fr)); }
}

/* Match page v2 on a phone or tablet (lsrn.5, prototypes/match-page-v2/): at
   1100px and below match-page-v2.js sets .mp2-phone and adds the bar, fixed at
   the bottom, that opens the coach as its own page. The coach card goes (it is
   that page now; on v2 it stays in the rail, which is not shown at these
   widths) and the bar shows while the card would. The help
   button rises above the bar; the page keeps room under its last block. */
.mp2-bar { display: none; font-size: 16px; color: var(--muted); text-decoration: none; }
.match-detail.mp2-phone #ai-coach { display: none; }
.match-detail.mp2-phone:has(#ai-coach:not(.hidden)) { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
.match-detail.mp2-phone:has(#ai-coach:not(.hidden)) > .mp2-bar {
  display: flex; align-items: center; gap: 10px; position: fixed; z-index: 45;
  left: 12px; right: 12px; max-width: 720px; margin: 0 auto; bottom: calc(18px + env(safe-area-inset-bottom));
  padding: 8px 8px 8px 10px; background: var(--surface); border: 1px solid var(--border-2, var(--border)); border-radius: 16px;
  box-shadow: 0 12px 30px -10px rgba(14, 19, 17, .3);
}
/* The map needs the foot of the screen on a phone. One rule, a default that can
   still be overruled (prototypes/match-edit-phone-sheet/). */
@media (max-width: 720px) {
  .match-detail.mp2-phone:has(#rallies-card .mp2-strip.mp2-map) > .mp2-bar { display: none; }
}
.mp2-bar-mark { width: 24px; height: 24px; border-radius: 6px; background: var(--accent); display: grid; place-items: center; flex: none; }
.mp2-bar-send { margin-left: auto; width: 28px; height: 28px; display: grid; place-items: center; color: var(--muted); flex: none; }

/* The coach page (coach_page.html, coach-page.js): no top bar; a header row, then
   the same card as the rail's, filling the screen. The card's own head (title,
   beta) is the header's job here. dvh follows the phone
   browser's toolbars and keyboard. */
/* The email strip, when it shows, sits above the page inside the same screen. */
body.coach-page .app { height: 100vh; height: 100dvh; min-height: 0; background: var(--surface); }
body.coach-page main { flex: 1; min-height: 0; padding: 0; display: flex; flex-direction: column; }
.cp { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--surface); }
.cp-head { flex: none; display: flex; align-items: center; gap: 6px; padding: 6px 10px 10px 6px; border-bottom: 1px solid var(--border); }
.cp-back { width: 36px; height: 36px; flex: none; border-radius: 8px; display: grid; place-items: center; color: var(--text-2, var(--text)); }
.cp-back:hover { background: var(--surface-2); color: var(--text); }
.cp-titles { flex: 1; min-width: 0; }
.cp-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: 0; }
.cp-sub { margin: 0; font-size: 12px; line-height: 1.3; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cp-loading { padding: 16px 14px; }
/* The card v2 (7uq7.4) keeps its own padding, its 400px phone layout and its
   16px field on a touch screen; the page only stretches it, drops its head
   and its footnote (the phone mockup has none) and keeps the dock clear of
   the home indicator. */
.cp #ai-coach { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.cp .coach-card { flex: 1; min-height: 0; overflow: hidden; border: 0; border-radius: 0; background: transparent; }
.cp .coach-head { display: none; }
.cp .coach-thread { flex: 1; min-height: 0; max-height: none; }
.cp .coach-dock { padding-bottom: calc(12px + env(safe-area-inset-bottom)); }
.cp .coach-foot { display: none; }

/* ── Suggestions under the match page's player and club fields (lsrn.30) ──────
   Spec: prototypes/match-details-v3/ section 4. The list is drawn by
   upload-match-suggest.js into #up-match and positioned from the field's rect,
   so it follows the fields whatever layout the editor gives them. Three 34px
   rows show and half of the fourth; the thin scrollbar stays on screen. */
#pt-editor .up-match { position: relative; }
.sugl {
  position: absolute; z-index: 6; width: 300px; max-width: 100%; padding: 6px;
  border-radius: 12px; background: var(--surface); border: 1px solid var(--border);
  box-shadow: 0 18px 40px -16px rgba(14, 19, 17, 0.32), 0 2px 6px -2px rgba(14, 19, 17, 0.12);
}
.sugl-lb {
  padding: 6px 8px 8px; font-family: var(--font-mono); font-size: var(--fs-label); line-height: 1; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted-2);
}
.sugl-sc {
  max-height: calc(3 * 36px + 18px); overflow-y: auto; overscroll-behavior: contain;
  padding-right: 4px; display: flex; flex-direction: column; gap: 2px;
}
.sugl-sc::-webkit-scrollbar { width: 4px; }
.sugl-sc::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
.sugl-sc::-webkit-scrollbar-track { background: var(--surface-2); border-radius: 4px; }
@supports not selector(::-webkit-scrollbar) {
  .sugl-sc { scrollbar-width: thin; scrollbar-color: var(--border-2) var(--surface-2); }
}
.sugl-row {
  display: flex; align-items: center; gap: 6px; height: 34px; flex: none; padding: 0 8px;
  border-radius: 8px; line-height: 1; font-size: var(--fs-body); color: var(--text-2); cursor: pointer;
}
.sugl-row.sugl-on { background: var(--surface-2); color: var(--text); }
.sugl-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sugl-name b { font-weight: 600; color: var(--text); }
.sugl-row em { margin-left: auto; flex: none; font-style: normal; font-size: var(--fs-sub); color: var(--muted-2); }
.sugl-none { margin: 0; padding: 6px 8px 8px; font-size: var(--fs-sub); color: var(--muted); }

/* ───────── Match page v2: the header under the player (lsrn.20) ─────────────
   Spec prototypes/match-header/. Above 1100px the column starts with the shelf
   (the Rallies card) and one 36px row under it holds the name, the pencil and
   the status chip on the left and the actions on the right. The server draws
   the header first (first paint and the DOM order are untouched); `order`
   puts it after the card, so nothing is moved by a script. The back link, the
   eyebrow and the meta line are not shown here; at 1100px and below the
   header is the one before. --mp2-col-w is match-page-v2.js's: the column's
   width at which the frame, the strip and this row fit the window's height. */
.md-more, .md-more-menu { display: contents; }
.md-more-btn { display: none; }
@media (min-width: 1101px) {
  .match-detail.mp2 #content > * { order: 3; }
  .match-detail.mp2 #content > .read-only-line { order: 0; }
  .match-detail.mp2 #content > #rallies-card { order: 1; }
  .match-detail.mp2 #content > .md-header { order: 2; }
  .match-detail.mp2 #content > .md-back, .match-detail.mp2 .md-header .eyebrow { display: none; }
  /* One column, one pair of edges (lsrn.47): the whole column takes the width,
     not the blocks in it, so everything under the player lines up with it. */
  .match-detail.mp2 > #content { box-sizing: border-box; width: min(100%, var(--mp2-col-w, 100%)); margin-inline: auto; }
  /* The same sum as fit() in match-page-v2.js, drawn from the first byte so the
     column does not open wide and then narrow: the height left under the top
     bar and the card's top (103), the skeleton's strip bar and strip (136), the
     row, its gap and the foot (60), as 16:9, plus the card's sides (14). The
     script's measured value replaces it on the page's own style. A match the
     server knows has no player yet draws no frame to fit, so it opens at the
     page's width. The top bar is 64px whatever font it has (.nav-screens a's
     line-height). The email strip is 45px when the server draws it open
     (_email_verify_strip.html, lsrn.48): the sum takes it off, and the coach
     rail starts under it, until the script's measured values replace these. */
  .match-detail.mp2.mp2-player:not([data-mp2-no-frame]) { --mp2-col-w: calc(max(100vh - 299px - var(--mp2-ev-strip, 0px), 310px) * 16 / 9 + 14px); }
  .app:has(#email-verify-strip:not([hidden])) .match-detail.mp2 { --mp2-ev-strip: 45px; --mp2-top: 109px; --mp2-strip: 45px; }
  /* A page with the call control where one bar row will not fit (lsrn.51; fit() decides once
     it runs): the column the one-row sum gives, less the 36px call line as 16:9,
     no narrower than the frame's floor. The step is 0 or 36px by whether that
     column holds --mp2-bar-need, the bar's width as fit() reads it, plus the
     card's sides. Separate from the rule above so that sum has one home. */
  .match-detail.mp2.mp2-player[data-mp2-calls]:not([data-mp2-no-frame]):not(.mp2-fitted) > #content {
    width: min(100%, max(calc(310px * 16 / 9 + 14px), var(--mp2-col-w) - clamp(0px, (var(--mp2-bar-need) + 14px - min(100%, var(--mp2-col-w))) * 1000, 36px) * 16 / 9));
  }
  .match-detail.mp2[data-mp2-no-frame] #rallies-card > .sbs-main[inert] { display: none; }
  .match-detail.mp2 .md-header {
    box-sizing: border-box; flex-wrap: nowrap; align-items: center; gap: 4px; height: 36px;
    margin-top: -8px;
    overflow-anchor: none;   /* first in the DOM, drawn under the card: the browser's scroll anchor would follow it down when the card grows (f2pe.16) */
  }
  .match-detail.mp2 .md-header > :first-child { display: contents; }
  .match-detail.mp2 #video-title-view { flex: 0 1 auto; min-width: 0; flex-wrap: nowrap !important; gap: 4px !important; margin: 0 !important; }
  .match-detail.mp2 #video-title {
    min-width: 0; font-size: 20px; font-weight: 600; letter-spacing: -.015em; line-height: 36px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; word-break: normal !important;
  }
  .match-detail.mp2 #video-title-edit-btn { flex: none; width: 28px; height: 28px; border-radius: 50%; }
  .match-detail.mp2 #video-meta { flex: none; margin: 0 !important; flex-wrap: nowrap !important; }
  .match-detail.mp2 #video-meta :is(.md-when, .sk-text) { display: none; }
  .match-detail.mp2 #video-title-edit { flex: 1 1 auto; min-width: 0; margin: 0 !important; flex-wrap: nowrap !important; }
  .match-detail.mp2 #video-title-input { flex: 1 1 auto !important; min-width: 0 !important; }
  .match-detail.mp2 .md-header:has(#video-title-edit:not(.hidden)) #video-meta { display: none; }
  .match-detail.mp2 .md-act-col { flex: none; margin-left: auto; padding-left: 12px; align-items: center; }
  .match-detail.mp2 .md-act-col > .row { flex-wrap: nowrap !important; align-items: center; }
  .match-detail.mp2 .md-act-col :is(.btn, .md-more-btn) {
    box-sizing: border-box; height: 36px; border: 0; border-radius: 999px; background: var(--surface-3); color: var(--text);
    font-weight: 500; white-space: nowrap; transform: none; filter: none;
  }
  .match-detail.mp2 .md-act-col :is(.btn, .md-more-btn):hover:not(:disabled) { background: color-mix(in srgb, var(--surface-3) 93%, var(--text)); }
  .match-detail.mp2 .md-act-col .btn:disabled { opacity: .5; }
  /* "…": a 36px circle; Download, Delete and Remove from my library behind it. */
  .match-detail.mp2 .md-more { display: block; position: relative; }
  .match-detail.mp2 .md-more-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; padding: 0; cursor: pointer; }
  .match-detail.mp2 .md-more-menu {
    display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; min-width: 200px; flex-direction: column; gap: 2px;
    padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 8px 24px rgba(14,19,17,.14);
  }
  .match-detail.mp2 .md-more.open .md-more-menu { display: flex; }
  .match-detail.mp2 .md-more-menu .btn { justify-content: flex-start; width: 100%; height: 36px; padding: 0 10px; border-radius: 8px; background: none; }
  .match-detail.mp2 .md-more-menu .btn:hover:not(:disabled) { background: var(--surface-2); }
  .match-detail.mp2 .md-more-menu .btn-danger { color: var(--danger, #b3261e); }
  /* Download is the Export modal's Full match row (lsrn.31): "…" keeps Delete
     (Remove from my library for a member), and an admin on someone else's
     match has nothing left behind it, so the circle goes too. */
  .match-detail.mp2 .md-more-menu .md-download { display: none; }
  .match-detail.mp2 .md-more:not(:has(.md-more-menu .btn:not(.md-download))) { display: none; }
  /* Invite's reason stays for the tooltip and the screen reader, not a line
     under the row (a hover exists here). */
  .match-detail.mp2 .invite-hint { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}

/* Match page v2, the rally filter (padel-website-lsrn.49, prototypes/rally-filter/).
   At rest a 28px icon button at the bar's left; with a filter on, a chip in the
   accent's soft fill and line that holds the option's icon, its count and an x.
   Its menu is the Set and Game dropdowns' (.mp2-dd-menu, .mp2-dd-row), taller
   where a row carries its hint. An empty row is dimmed. On a phone (720px and
   below) the chip has no x, the bar's gap is 6px, Edit games is its pen alone and
   under 380px the Score note's button gives way. */
.match-detail.mp2-player .mp2-flt { flex: none; }
.match-detail.mp2.mp2-player :is(.mp2-flt-icon, .mp2-flt-chip) {
  height: 28px; border-radius: 8px; font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap;
}
.match-detail.mp2.mp2-player .mp2-flt-icon {
  width: 28px; padding: 0; border: 0; display: grid; place-items: center;
  background: var(--sh-btn); box-shadow: inset 0 0 0 1px var(--sh-line); color: var(--sh-ink2);
}
.match-detail.mp2.mp2-player .mp2-flt-icon:hover { background: var(--sh-btn-h); }
.match-detail.mp2.mp2-player .mp2-flt-chip {
  display: inline-flex; align-items: center; background: var(--sh-on); color: var(--sh-acc);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sh-acc) 45%, transparent); font-weight: 500; font-feature-settings: "tnum";
}
.match-detail.mp2.mp2-player .mp2-flt-chip.hidden, .match-detail.mp2.mp2-player .mp2-flt-icon.hidden { display: none; }
.match-detail.mp2.mp2-player .mp2-flt-chip:hover { background: color-mix(in srgb, var(--sh-acc) 22%, transparent); }
.match-detail.mp2.mp2-player .mp2-flt-face {
  height: 28px; padding: 0 9px; border: 0; border-radius: 8px; background: none; color: inherit; font: inherit;
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
}
.match-detail.mp2.mp2-player .mp2-flt-n { font: 400 11px var(--font-mono); }
.match-detail.mp2.mp2-player .mp2-flt-clr {
  width: 18px; height: 18px; margin: 0 5px 0 -5px; padding: 0; border: 0; border-radius: 50%; background: none; color: inherit;
  display: inline-grid; place-items: center; cursor: pointer;
}
.match-detail.mp2.mp2-player .mp2-flt-clr:hover { background: color-mix(in srgb, var(--sh-acc) 30%, transparent); }
.match-detail.mp2.mp2-player :is(.mp2-flt-icon, .mp2-flt-face, .mp2-flt-clr):focus-visible { outline: 2px solid var(--sh-acc); outline-offset: 2px; }
.match-detail.mp2.mp2-player .mp2-flt-menu { min-width: 248px; }
.match-detail.mp2.mp2-player .mp2-flt-row { height: auto; min-height: 34px; padding-block: 5px; }
.match-detail.mp2.mp2-player .mp2-flt-name { display: flex; align-items: center; gap: 9px; min-width: 0; }
.match-detail.mp2.mp2-player .mp2-flt-name svg { flex: none; }
.match-detail.mp2.mp2-player .mp2-flt-name small { display: block; font-size: 11.5px; line-height: 1.3; color: var(--sh-mute); white-space: normal; }
.match-detail.mp2.mp2-player .mp2-flt-row.off { cursor: default; color: var(--sh-mute); }
.match-detail.mp2.mp2-player .mp2-flt-row.off:hover { background: none; }
@media (max-width: 720px) {
  .match-detail.mp2.mp2-player .mp2-sbar { gap: 6px; }
  .match-detail.mp2.mp2-player .mp2-flt-clr { display: none; }
  .match-detail.mp2.mp2-player .mp2-flt-menu { min-width: 0; width: min(260px, calc(100vw - 48px)); }
  .match-detail.mp2.mp2-player .mp2-flt-row { min-height: 42px; }
  .match-detail.mp2.mp2-player .mp2-edit-open { width: 28px; padding: 0; justify-content: center; position: relative; }
  .match-detail.mp2.mp2-player .mp2-edit-open > span {
    position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
}
@media (max-width: 379px) {
  .match-detail.mp2.mp2-player .mp2-sbar .mp2-info { display: none !important; }
}
/* The bar with a call control above 1100px when the column is narrow (lsrn.51, prototypes/
   match-page-v2-polish/ section 1). The order things give way in: the hairline after
   the filter, the "estimated" note, a second row. match-page-v2.js's fit() decides
   the row (`mp2-line`, the arrangement 1100px and below has, its own rule beside
   lsrn.42's), the strip's fitBar() the rest; a filter's chip that does not fit takes
   the words "Won by", so turning a filter on never flips the row. Full screen has
   the room. .mp2-bar-need is barNeed()'s, for one synchronous read: never painted. */
@media (min-width: 1101px) {
  /* First paint's guess at what barNeed() reads, longest in es and pt-PT. A member's
     bar is the owner's: they have Edit games too (prototypes/match-edit/). */
  .match-detail.mp2.mp2-player[data-mp2-calls] { --mp2-bar-need: 776px; }
  .match-detail.mp2.mp2-player[data-mp2-calls]:is(:lang(es), :lang(pt)) { --mp2-bar-need: 813px; }
  .match-detail.mp2.mp2-player.mp2-line #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar > :is(.mp2-callc, .mp2-vsep, .mp2-hidden) { display: none; }
  .match-detail.mp2.mp2-player.mp2-line #rallies-card > .sbs-main:not(:fullscreen) > .mp2-callline:not(.hidden) { display: flex; box-sizing: border-box; min-height: 36px; padding: 0 8px 8px; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar.mp2-fit-sep .mp2-fsep { display: none; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar.mp2-fit-est .mp2-est { display: none !important; }
  .match-detail.mp2.mp2-player #rallies-card > .sbs-main:not(:fullscreen) > .mp2-sbar.mp2-fit-word .mp2-callc-w { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .match-detail.mp2.mp2-player .mp2-sbar.mp2-bar-need { display: flex !important; width: max-content !important; }
  .match-detail.mp2.mp2-player .mp2-sbar.mp2-bar-need :is(.mp2-flt, .mp2-dds, .mp2-dd, .mp2-callc, .mp2-score, .mp2-hidden) { display: inline-flex !important; }
  .match-detail.mp2.mp2-player .mp2-sbar.mp2-bar-need .mp2-vsep:not(.mp2-fsep) { display: block !important; }
  .match-detail.mp2.mp2-player .mp2-sbar.mp2-bar-need .mp2-flt-icon { display: grid !important; }
  .match-detail.mp2.mp2-player .mp2-sbar.mp2-bar-need :is(.mp2-fsep, .mp2-est, .mp2-flt-chip, .mp2-edit-head, .mp2-edit-tail) { display: none !important; }
  /* The name row's actions (polish 10's policy): under about 780px Export, Share and
     Invite are 36px circles with their icon alone; the label stays as tooltip and
     accessible name (the template's). By the row's own width, which does not depend
     on its content, so there is no loop. Containment makes the row a stacking
     context: the z-index keeps the "…" menu over the blocks below. The es and pt-PT
     labels and the coach's name are longer, so they step earlier. */
  .match-detail.mp2 .md-header { container-type: inline-size; position: relative; z-index: 2; }
  @container (max-width: 779.98px) {
    .match-detail.mp2:not([data-viewer="link"]) .md-act-col > .row > .btn-ghost { width: 36px; padding: 0; gap: 0; justify-content: center; font-size: 0; }
  }
  @container (max-width: 859.98px) {
    .match-detail.mp2:not([data-viewer="link"]):is(:lang(es), :lang(pt)) .md-act-col > .row > .btn-ghost { width: 36px; padding: 0; gap: 0; justify-content: center; font-size: 0; }
  }
  /* The narrowest column (the frame's floor): the name still has its 150px, so
     "ready" is its tick alone, which says the same. Any other status keeps its word. */
  @container (max-width: 619.98px) {
    .match-detail.mp2 #video-meta .st-ready { gap: 0; font-size: 0; }
    .match-detail.mp2 .md-act-col { padding-left: 8px; }
  }
  @container (max-width: 699.98px) {
    .match-detail.mp2:is(:lang(es), :lang(pt)) #video-meta .st-ready { gap: 0; font-size: 0; }
    /* Their coach's door is 209px, against the name's 150px: its mark alone here,
       by Mikhail's call of 2026-10-31. English keeps its word; it clears the floor. */
    .match-detail.mp2:is(:lang(es), :lang(pt)) .mp2-coach-btn { width: 36px; padding: 0; gap: 0; justify-content: center; font-size: 0; }
    .match-detail.mp2:is(:lang(es), :lang(pt)) .mp2-coach-btn .beta-tag { display: none; }
  }
}
/* The Export modal, step 1: the shelf (padel-website-lsrn.31, prototypes/export-modal/).
   The modal speaks the header row's language: 36px pills, grey (--surface-3) or,
   for the one that does the thing, the accent; 36px circles for icon buttons. */
.xm { max-width: 540px; max-height: calc(100dvh - 48px); overflow-y: auto; }
.xm-head { display: flex; align-items: flex-start; gap: 12px; padding: 20px 20px 14px; }
.xm-h { margin: 0; font-size: 18px; font-weight: 500; letter-spacing: -.015em; }
.xm-sub { margin-top: 1px; color: var(--muted); font-size: 12.5px; overflow-wrap: anywhere; }
.xm-x { margin-left: auto; flex: none; width: 32px; height: 32px; border: 0; border-radius: 50%; background: none; color: var(--muted); display: grid; place-items: center; cursor: pointer; }
.xm-x:hover { background: var(--surface-2); color: var(--text); }
.xm-shelf { padding: 0 12px 12px; }
.xm-row { position: relative; display: flex; align-items: center; gap: 14px; padding: 12px 10px; border-radius: 10px; color: var(--text); text-decoration: none; }
.xm-row + .xm-row { border-top: 1px solid var(--border); }
.xm-go { cursor: pointer; }
.xm-go:hover { background: var(--surface-2); }
.xm-ico { flex: none; width: 40px; height: 40px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--border); display: grid; place-items: center; color: var(--text-2); }
.xm-ico.xm-star { color: var(--star, #b7791f); }
.xm-ico.xm-pick-ico { color: var(--accent); }
.xm-text { min-width: 0; }
.xm-title { font-weight: 500; }
.xm-meta { color: var(--muted); font-size: 13px; line-height: 18px; }
.xm-mono { font-family: var(--font-mono); font-size: 11px; font-feature-settings: "tnum"; }
.xm-fail { color: var(--danger); }
.xm-off .xm-title, .xm-off .xm-ico { opacity: .5; }
.xm-acts { margin-left: auto; display: flex; align-items: center; gap: 6px; flex: none; }
.xm-chev { color: var(--muted); display: inline-flex; }
.xm-btn { height: 36px; padding: 0 16px 0 14px; border: 0; border-radius: 999px; background: var(--surface-3); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 7px; font: inherit; font-size: 13px; font-weight: 500; white-space: nowrap; cursor: pointer; }
.xm-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--surface-3) 93%, var(--text)); }
.xm-btn.xm-primary { background: var(--accent); color: #fff; }
.xm-btn.xm-primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 88%, #000); }
.xm-btn.xm-quiet { background: none; color: var(--muted); padding: 0 14px; }
.xm-btn.xm-quiet:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }
.xm-ib { width: 36px; height: 36px; border: 0; border-radius: 50%; background: var(--surface-3); color: var(--text); display: grid; place-items: center; flex: none; cursor: pointer; }
.xm-ib:hover:not(:disabled) { background: color-mix(in srgb, var(--surface-3) 93%, var(--text)); }
.xm-btn:disabled, .xm-ib:disabled { opacity: .45; cursor: default; }
.xm-bar { position: absolute; left: 64px; right: 10px; bottom: 4px; height: 4px; border-radius: 4px; background: var(--surface-3); overflow: hidden; }
.xm-bar i, .xm-fbar i { position: absolute; top: 0; bottom: 0; width: 30%; border-radius: 4px; background: var(--accent); animation: xm-wait 1.3s ease-in-out infinite; }
@keyframes xm-wait { from { left: -30%; } to { left: 100%; } }
.xm :is(.xm-btn, .xm-ib, .xm-x, .xm-go):focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 480px) { .xm-row { gap: 10px; padding-inline: 6px; } .xm-meta { font-size: 12px; } .xm-btn { padding: 0 12px 0 10px; } }
@media (prefers-reduced-motion: reduce) { .xm-bar i, .xm-fbar i { animation: none; left: 0; width: 100%; opacity: .5; } }
.xm-det i { left: 0; width: 0; animation: none; opacity: 1; transition: width .4s cubic-bezier(.16, 1, .3, 1); }
@media (prefers-reduced-motion: reduce) { .xm-det i { transition: none; } }

/* Step 2: the picker (lsrn.32; prototypes/export-modal/, "Step 2"). The modal
   widens 540 -> 880 x 720 while the picker slides in from the right; the step is
   laid out at its final size for the morph (export-modal.js, morph). */
button.xm-row { width: 100%; border: 0; background: none; font: inherit; text-align: left; cursor: pointer; }
.xm.xm-wide { display: flex; flex-direction: column; max-width: 880px; height: min(720px, calc(100dvh - 48px)); overflow: hidden; }
.xm.xm-wide > .xm-step { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.xm.xm-morph { overflow: hidden; transition: width .34s cubic-bezier(.16, 1, .3, 1), height .34s cubic-bezier(.16, 1, .3, 1); }
.xm-in-fwd { animation: xm-in-fwd .3s cubic-bezier(.16, 1, .3, 1) .06s both; }
.xm-in-back { animation: xm-in-back .3s cubic-bezier(.16, 1, .3, 1) .06s both; }
@keyframes xm-in-fwd { from { opacity: 0; transform: translateX(28px); } }
@keyframes xm-in-back { from { opacity: 0; transform: translateX(-28px); } }
.xm-back { display: inline-flex; align-items: center; gap: 6px; padding: 0; border: 0; background: none; color: var(--muted); font: inherit; font-size: 12.5px; cursor: pointer; }
.xm-back:hover { color: var(--text); }
.xm-step2 .xm-h { margin-top: 4px; }
.xm-tb { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 20px 14px; }
.xm-chip { height: 30px; padding: 0 13px; border: 0; border-radius: 999px; background: var(--surface-3); color: var(--text); display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; }
.xm-chip:hover:not(:disabled) { background: color-mix(in srgb, var(--surface-3) 93%, var(--text)); }
.xm-chip.xm-chip-on { background: var(--text); color: var(--surface); }
.xm-chip .xm-star { display: inline-flex; color: var(--star, #b7791f); }
.xm-pick { flex: 1; min-height: 0; overflow: auto; border-top: 1px solid var(--border); scroll-padding-top: 48px; /* a focused chip clears the pinned set header */ }
.xm-empty { padding: 32px 20px; color: var(--muted); text-align: center; }
/* A set is its own block, so its header sticks only while the set is in view and
   the next set's header pushes it out. */
.xm-sh { position: sticky; top: 0; z-index: 1; width: 100%; display: flex; align-items: center; gap: 10px; padding: 10px 20px; border: 0; border-bottom: 1px solid var(--border); background: var(--surface); color: var(--text); font: inherit; font-weight: 500; text-align: left; cursor: pointer; }
.xm-sh-sum { margin-left: auto; color: var(--muted); }
.xm-gm { display: grid; grid-template-columns: 150px 1fr; gap: 12px; padding: 8px 20px; border-bottom: 1px solid var(--border); align-items: start; }
.xm-gm.xm-flat { grid-template-columns: 1fr; }
.xm-gl { display: flex; align-items: center; gap: 8px; padding: 5px 0 0; border: 0; background: none; color: var(--text-2); font: inherit; font-size: 12.5px; text-align: left; cursor: pointer; }
.xm-gl .xm-mono { color: var(--muted); }
.xm-cb { width: 16px; height: 16px; border-radius: 4px; border: 1.5px solid var(--border-2); display: grid; place-items: center; flex: none; background: var(--surface); }
.xm-cb.xm-cb-on, .xm-cb.xm-cb-part { background: var(--accent); border-color: var(--accent); }
.xm-cb-on::after { content: ""; width: 8px; height: 4px; border: 1.6px solid #fff; border-top: 0; border-right: 0; transform: translateY(-1px) rotate(-45deg); }
.xm-cb-part::after { content: ""; width: 8px; height: 1.6px; background: #fff; }
/* Even chips: the number left, the length right, a star as a corner badge, and
   who won the point as the 2px top edge (team A, team B, grey undecided). */
.xm-rl { display: grid; grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); gap: 8px 6px; }
.xm-r { --w: color-mix(in srgb, var(--muted-2) 55%, var(--surface)); position: relative; height: 30px; padding: 0 9px; border: 0; border-radius: 6px; background: var(--surface-2); box-shadow: inset 0 2px 0 var(--w), 0 0 0 1px var(--border); display: flex; align-items: center; justify-content: space-between; color: var(--text-2); font: 11.5px var(--font-mono); font-feature-settings: "tnum"; cursor: pointer; }
.xm-r[data-w="A"] { --w: var(--team-a); }
.xm-r[data-w="B"] { --w: var(--team-b); }
.xm-r:hover:not(:disabled) { background: var(--surface-3); }
.xm-r.xm-on { background: var(--accent-soft); box-shadow: inset 0 2px 0 var(--w), 0 0 0 1.5px var(--accent); color: var(--text); }
.xm-d { color: var(--muted-2); }
.xm-on .xm-d { color: var(--muted); }
.xm-st { position: absolute; top: -6px; right: -6px; width: 15px; height: 15px; border-radius: 50%; background: var(--surface); box-shadow: 0 0 0 1px var(--border); display: grid; place-items: center; color: var(--star, #b7791f); }
.xm-locked .xm-tb, .xm-locked .xm-pick { opacity: .45; }
.xm-foot { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-top: 1px solid var(--border); background: var(--surface-2); min-height: 65px; }
.xm-sum b { font-weight: 500; }
.xm-muted { color: var(--muted); }
.xm-push { margin-left: auto; }
.xm-seg { display: inline-flex; padding: 3px; border-radius: 999px; background: var(--surface-3); }
/* The format switch (lsrn.33): the chosen half is the white pill, and neither half
   is bolder, so choosing one moves nothing. */
.xm-seg-b { height: 30px; padding: 0 13px; border: 0; border-radius: 999px; background: none; color: var(--muted); font: inherit; font-size: 12.5px; font-weight: 400; white-space: nowrap; display: inline-flex; align-items: center; cursor: pointer; }
.xm-seg-b:hover:not(.xm-seg-on) { color: var(--text); }
.xm-seg-on { background: var(--surface); color: var(--text); box-shadow: 0 1px 2px rgba(14, 19, 17, .12); }
.xm-seg-b:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* A reel's bar fills with the stage the stream reports; a zip's has no figure and sweeps. */
.xm-pl { display: flex; gap: 8px; align-items: baseline; }
.xm-pl, .xm-pl b { font-weight: 500; }
.xm-foot .xm-fail { display: flex; gap: 8px; align-items: center; }
.xm-pp { margin-left: auto; color: var(--muted); }
.xm-foot .xm-go-btn { width: 188px; }
.xm-prog { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.xm-fbar { position: relative; height: 4px; border-radius: 4px; background: var(--surface-3); overflow: hidden; }
.xm :is(.xm-chip, .xm-back, .xm-r, .xm-gl):focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.xm .xm-sh:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
@media (max-width: 600px) {
  .xm-gm { grid-template-columns: 1fr; gap: 6px; }
  .xm-foot { flex-wrap: wrap; }
}

/* The Export modal as a sheet from the bottom at the v2 breakpoint (lsrn.35): the
   same rows with icon buttons; Choose rallies stretches it to the top of the
   screen with the footer pinned. prototypes/export-modal/, "Phone". */
.xm-grab { display: none; }
.xm-ftop { display: contents; }
@media (max-width: 1100px) {
  .xm-backdrop { align-items: flex-end; padding: 0; animation: xm-dim .22s ease-out; }
  .xm.modal { max-width: none; width: 100%; max-height: calc(100dvh - 42px); border-radius: 18px 18px 0 0; border-width: 1px 0 0; box-shadow: 0 -10px 30px rgba(0, 0, 0, .18); animation: xm-rise .3s cubic-bezier(.16, 1, .3, 1); }
  .xm.xm-wide { max-width: none; height: calc(100dvh - 42px); }
  /* The whole strip above the heading takes the swipe: a 4px bar is no target for a finger. */
  .xm-grab { display: block; position: relative; flex: none; height: 14px; touch-action: none; }
  .xm-grab::before { content: ""; position: absolute; left: 50%; top: 8px; width: 36px; height: 4px; margin-left: -18px; border-radius: 4px; background: var(--border-2); }
  .xm-head { padding: 8px 16px 10px; touch-action: none; }
  .xm-h { font-size: 16px; }
  .xm-shelf { padding: 0 0 max(12px, env(safe-area-inset-bottom)); }
  .xm-row { padding: 10px 16px; border-radius: 0; gap: 10px; }
  .xm-ico { width: 32px; height: 32px; border-radius: 8px; }
  .xm-bar { left: 58px; right: 16px; }
  /* Download is an icon; the label stays as the name. */
  .xm-acts .xm-dl { position: relative; width: 36px; padding: 0; border-radius: 50%; }
  .xm-dl .xm-lbl { display: block; position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  /* Its end does not scroll the match behind, or pull the page to a reload. */
  .xm-pick { overscroll-behavior: contain; }
  .xm-rl { grid-template-columns: repeat(auto-fill, minmax(56px, 1fr)); gap: 8px 5px; }
  .xm-r { padding: 0 7px; font-size: 10.5px; }
  .xm-tb { padding: 0 16px 10px; }
  .xm-gm, .xm-sh { padding-inline: 16px; }
  .xm-foot { flex-wrap: wrap; gap: 8px; padding: 10px 16px max(14px, env(safe-area-inset-bottom)); min-height: 0; }
  .xm-ftop { display: flex; align-items: center; gap: 8px; flex: 1 1 100%; min-width: 0; }
  .xm-foot .xm-go-btn { width: auto; flex: 1 1 0; min-width: 0; height: 38px; }
  .xm-foot > .xm-ib { width: 38px; height: 38px; }
  /* A finished or failed build says so on a line of its own, its buttons under it. */
  .xm-foot > .xm-sum, .xm-foot > .xm-fail { flex: 1 1 100%; }
  .xm-foot > .xm-push { margin-left: 0; }
  .xm-foot > .xm-btn.xm-primary { flex: 1; min-width: 0; height: 38px; }
}
@keyframes xm-rise { from { transform: translateY(100%); } }
@keyframes xm-dim { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .xm-backdrop, .xm.modal { animation: none; } }

/* Export while a build runs with the modal shut (padel-website-lsrn.34): the pill
   carries the progress (a green fill for a reel, a sweep for a zip), and the card
   that rises from it holds the result; with Export out of view it is a toast. */
/* Its words stay in a column where the row's actions are circles (the rule over
   779.98px above): the id outranks it, and the pills beside it make room. */
#export-link.xm-busy { position: relative; overflow: hidden; color: var(--accent); font-feature-settings: "tnum"; width: auto; padding: 9px 14px; gap: 8px; font-size: 13.5px; }
#export-link.xm-busy > :not(.xm-pfill) { position: relative; }
.xm-pfill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; background: var(--accent-soft); transition: width .4s cubic-bezier(.16, 1, .3, 1); }
#export-link.xm-wait .xm-pfill { width: 100%; background: linear-gradient(90deg, transparent, var(--accent-line), transparent) no-repeat; background-size: 50% 100%; animation: xm-sweep 1.4s linear infinite; }
/* The narrowest columns: the name keeps its 150px floor beside the longest words. */
@container (max-width: 699.98px) { #export-link.xm-busy { padding: 9px 10px; } #export-link.xm-busy > svg { display: none; } }
@keyframes xm-sweep { from { background-position: -50% 0; } to { background-position: 150% 0; } }
.xm-card { position: fixed; z-index: 200; display: flex; align-items: center; gap: 12px; min-width: 380px; max-width: calc(100vw - 16px); padding: 10px 10px 10px 14px; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 16px 36px -14px rgba(14, 19, 17, .38), 0 2px 6px -2px rgba(14, 19, 17, .12); animation: xm-card-in .3s cubic-bezier(.16, 1, .3, 1) both; }
.xm-card::after { content: ""; position: absolute; left: var(--xm-point, 50%); bottom: -6px; width: 12px; height: 12px; margin-left: -6px; background: var(--surface); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); transform: rotate(45deg); }
.xm-card.xm-toast { right: 16px; bottom: 16px; }
.xm-card.xm-toast::after { display: none; }
.xm-card > svg { flex: none; color: var(--accent); }
.xm-card.xm-bad > svg { color: var(--danger); }
.xm-ctext { min-width: 0; }
.xm-ct { font-size: 13px; font-weight: 500; white-space: nowrap; }
.xm-cm { color: var(--muted); font-size: 11px; }
.xm-ca { margin-left: auto; display: flex; gap: 6px; flex: none; }
.xm-cbtn { height: 32px; min-width: 32px; padding: 0 13px; border: 0; border-radius: 999px; background: var(--surface-3); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 6px; font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; }
.xm-cbtn:hover:not(:disabled) { background: color-mix(in srgb, var(--surface-3) 93%, var(--text)); }
.xm-cbtn:disabled { opacity: .5; cursor: default; }
.xm-cbtn.xm-cp { background: var(--accent); color: #fff; }
.xm-cbtn.xm-cs, .xm-cbtn.xm-cx { padding: 0; }
.xm-cbtn.xm-cx { background: none; color: var(--muted); }
@keyframes xm-card-in { from { opacity: 0; transform: translateY(8px) scale(.97); } }
/* Above the coach bar (.mp2-bar: 18px off the foot, about 46px tall, 720px wide at most). */
@media (max-width: 1100px) { .xm-card { min-width: 0; } .xm-ct { white-space: normal; } .xm-card.xm-toast { left: 8px; right: 8px; bottom: calc(72px + env(safe-area-inset-bottom)); max-width: 720px; margin-inline: auto; } }
@media (prefers-reduced-motion: reduce) { .xm-card { animation: none; } #export-link.xm-wait .xm-pfill { animation: none; } }
