/* ============================================================
   Baseball Scorecard — Broadsheet re-skin (Rangers-influenced)
   Every hook app.js touches is preserved; only the look changed.
   ============================================================ */

@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/source-serif-4-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: italic;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/source-serif-4-italic.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url('fonts/jetbrains-mono-latin.woff2') format('woff2');
}
/* Graduate was declared 900 lines down, beside the palette that named it. The
   palette has moved up here (F30) and the face follows it: @font-face is
   position-independent, so this is legibility only — all four families the app
   ships are now declared in one place, next to the stacks that order them. */
@font-face {
  font-family: 'Graduate';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/graduate-latin.woff2') format('woff2');
}

/* ---------- The palette (F30) ----------

   There used to be three `:root` blocks — this one, a theme layer at the top of
   the Rangers section, and a safe-area pair near the one-page-fit block. The
   second was not a theme *switch*; it was an unconditional redeclaration that
   won on source order alone, so 21 of the names below resolved to something
   other than what they said and the base values were dead weight the file went
   on carrying. Reading `--green: #17693a` here told you nothing about what
   `var(--green)` painted.

   One block now, holding what actually resolves. Six names were declared and
   never used (`--navy-light`, `--accent-light`, `--gold-light`, `--blue`,
   `--blue-deep`, `--sa-b`) and are gone; `--green` was live in the file but
   dead in the cascade — both its call sites were overridden further down — and
   is gone with them. Nothing here changes a painted pixel: this is the same
   set of resolved values the browser was already computing, written once
   instead of twice. */
:root {
  /* Three brand values. Everything else is one of them, tinted or deepened. */
  --rb-blue: #003278;
  --rb-red: #c0111f;
  --rb-white: #ffffff;

  --paper: #eef2f8;          /* white cooled toward the blue */
  --paper-2: #e2e9f4;
  --line: rgba(0,50,120,0.22);
  --line-soft: rgba(0,50,120,0.13);

  /* The semantic layer. These names are what the rest of the file reads; the
     three above are the only place a literal colour is chosen. */
  --navy: var(--rb-blue);    /* structural ink / scoreboard ground */
  --cream: var(--paper);
  --cream-dark: var(--paper-2);
  --card: var(--rb-white);
  --border: var(--rb-blue);
  --border-light: rgba(0,50,120,0.2);
  --row-alt: #f7f9fc;
  --accent: var(--rb-red);   /* the rare second spot */
  --blue-light: rgba(0,50,120,0.1);
  --select: rgba(192,17,31,0.12);
  --text: var(--rb-blue);
  /* 0.6 measured 3.66:1 on the paper — under AA for the 11-19px labels this
     paints (inactive team tab, section buttons, every field label). 0.72 is
     the lightest step that clears 4.5:1 on the darkest ground it lands on
     (--paper-2): 4.84:1 there, 5.07:1 on the paper, 5.50:1 on white. */
  --text-light: rgba(0,50,120,0.72);
  --rule: var(--line-soft);
  /* Not gold. This is the light ink used *on* the navy ground — the total
     cell's figure, the scoreboard captions, the text on every navy button
     app.js builds — and in this skin that ink is white. All 24 call sites want
     "whatever reads on navy", which is what the value is; it is the name that
     is wrong, and renaming it to `--on-navy` was ruled out as a bigger diff
     than the lie is worth. Read it as on-navy and it is correct everywhere. */
  --gold: var(--rb-white);

  /* The five notable plays. These are the only colours on the card that are *not*
     one of the three brand values tinted — green, gold and purple step outside the
     skin on purpose, because five plays need five colours a glance can tell apart
     and navy/red/white cannot supply that many. The assignments are the scorer's
     own: any hit green, home run gold, strikeout red, walk navy, double play
     purple. Routine outs get no colour of their own (see .play-go) — most plate
     appearances are outs, and colouring them would leave the five nothing to stand
     out against.
     Translucent, and the alphas are not arbitrary: the card's ink stays navy on
     every one of them, so each hue is set as bold as it can go while the smallest
     thing painted over it still clears 4.5:1. The binding constraint is never the
     15px play text (it has 5.8:1 to spare here) but the pitch marks, ~8px at
     rest and smaller the longer the at-bat runs, which is the same reason these
     started life at 4-14%. (Contrast is a ratio, so the size does not move the
     numbers below — it decides that 4.5:1 rather than 3:1 is the bar to clear,
     and at no length do these marks get to be large text.) Each value below is that
     ceiling: navy ink lands at 5.81-5.86:1 and a strike mark at 4.50-4.54:1 on all
     four of the dark hues. Solid fills were tried and abandoned — they force the
     basepaths, the diamond and the pitch marks to white, and the scorer reads the
     baserunning off those lines. */
  --play-hit: rgba(23,105,58,0.40);
  --play-hr:  rgba(242,194,0,0.40);
  --play-k:   rgba(192,17,31,0.34);
  --play-bb:  rgba(0,50,120,0.32);
  --play-dp:  rgba(106,27,154,0.33);

  /* Safe, and the tint behind a chip that is on it. The only pair here that is
     neither navy nor red, and it is the same rgb(23,105,58) the card already
     paints a hit in — so it is a value the skin owns, read at full strength
     instead of at 0.40.

     It exists because the popups needed a *safe* and had been spelling it in
     Material's #2e7d32/#e8f5e9, which is a green from no palette in this file
     (H1). Safe/out is the umpire's own signal and the fastest thing a beginner
     reads off a runner popup, so it keeps a colour of its own rather than
     folding into the navy every other selected control uses. Red stays the
     out: --accent.

     4.5:1 is the bar. A chosen chip fills, so the pairing that matters is
     white on the fill: 6.73:1 on --safe and 6.29:1 on --accent (12.16:1 on
     --navy, which the pitcher and position pickers use). At rest the ink is on
     the white card — --safe reads 6.73:1 there, --accent 6.29:1, --text-light
     5.41:1 — and --safe-soft is the hover tint beneath it, where --text-light
     measures 4.52:1 and so sets how far that tint may go. The one thing under
     the bar is a blocked chip at 0.5 opacity; WCAG exempts inactive controls,
     and it carries a dashed border and its reason on `title` besides. */
  --safe: #17693a;
  --safe-soft: rgba(23,105,58,0.12);

  --cell-w: 90px;
  --cell-h: 62px;

  /* `viewport-fit=cover` plus a black-translucent status bar hands the app the
     whole screen, insets included, so the four edges have to be readable. Zero
     wherever there is no inset, which is what makes them safe at every width.
     There is no `--sa-b`: the bottom inset is wanted in three places and all
     three want it inside a `calc()` against a padding of their own, so they
     take `env()` directly rather than through a name. */
  --sa-t: env(safe-area-inset-top, 0px);
  --sa-r: env(safe-area-inset-right, 0px);
  --sa-l: env(safe-area-inset-left, 0px);

  /* Graduate is the varsity face; the broadsheet serif stays on as the fallback
     so a missing woff2 degrades to the old skin rather than to system-ui.
     `font-src 'self'` — never link fonts.googleapis.com.

     `--mono` used to start with Graduate too, which meant the 31KB JetBrains
     Mono was fetched, precached by the service worker and never rendered once:
     Graduate resolved first for every glyph in it. The mono stack exists for
     the pitch marks, the count and the fielder keypad — the three places on the
     card where figures have to line up in a column — and Graduate is a
     single-weight display face with no tabular figures, so it was the one face
     that must not be at the front of this list. (F31)

     `--heading` is deliberately identical to `--font`. Graduate is the house
     style for both, so the two names mark intent — this is a heading, this is
     body — rather than a difference in face, and a re-skin that wants them
     apart has the seam already cut. */
  --font: 'Graduate', "Source Serif 4", Georgia, serif;
  --heading: 'Graduate', "Source Serif 4", Georgia, serif;
  --mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;

  /* ---------- The stacking scale (F29) ----------
     There used to be two ladders, authored apart and never compared: this file
     ran 1,2,3,4,120,150,300,500,600,1000 and app.js ran 180,300,400. They
     interleaved badly. Every gameplay popup sat at 300-400 and both big modals
     sat above them, so a popup opened from inside a modal painted underneath it
     and the modal stayed tappable on top of it — F22 is that bug, and the Game
     Library at 500 was the second way in.

     One scale now, and both files read it. The rule the numbers encode: a
     surface must outrank whatever it was opened from. Popups are therefore
     above every modal, not below them, because a popup is always opened from
     something. Nothing here is a literal anywhere else — if you need a new
     tier, name it here.

     `--z-banner` was 1000 and outranked every dialog, so a storage warning
     could paint over an open popup. A banner is a passive notice with a
     dismiss button, not a dialog, so it now sits above the page's own sticky
     chrome and below anything modal. That is a ruling, not an inheritance. */
  --z-grid-sticky: 2;        /* the scoring grid's sticky head row */
  --z-frozen-col: 3;         /* the four frozen columns, sticky on X */
  --z-frozen-head: 4;        /* their head cells, sticky on both axes */
  --z-deck: 120;             /* the sticky masthead + linescore strip */
  --z-deck-docked: 150;      /* the entry deck, docked to the bottom edge */
  --z-menu: 300;             /* dropdowns anchored to a button */
  --z-banner: 400;           /* page-level notices */
  --z-modal: 500;            /* full-screen overlays */
  --z-modal-child: 700;      /* an overlay that opens over another overlay */
  --z-popup-backdrop: 750;   /* one step under the popup it guards */
  --z-popup: 800;            /* every popup app.js builds */
  --z-toast: 900;            /* says why a press refused, so it clears them all */
}

/* The cell ladder. There were two of these as well, for the same reason as the
   palette: the base seeded 84/56 and stepped it down here, then the theme's
   `:root` — being later and unconditional — reset it to 90/62 and had to
   restate the whole ladder further down to get the stepping back. Every value
   in the base copy was dead. One ladder now, next to the block it steps.
   Both phone blocks narrow --cell-w again below this; ui.js measures --cell-h
   outright above 835px and on a phone, so these are the resting values it
   starts from and the floor wherever it does not run. */
@media (max-width: 1280px) { :root { --cell-w: 82px; --cell-h: 60px; } }
@media (max-width: 1024px) { :root { --cell-w: 75px; --cell-h: 58px; } }
@media (max-width: 834px)  { :root { --cell-w: 69px; --cell-h: 55px; } }

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: var(--font);
  background: var(--cream);
  color: var(--text);
  font-size: 14px;
  line-height: 1.35;
  padding: 0;
  min-height: 100vh;
  -webkit-text-size-adjust: 100%;
}
a { color: var(--navy); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--accent); }
input, select, textarea, button { font-family: inherit; font-size: inherit; color: inherit; }
/* A text field keeps its own white background, so it must not inherit a light
   colour from a dark container: app.js builds the fielder-number prompt as a
   dark bar with `color:#fff`, which left the typed "6-3" white on white.
   Deliberately weak (0,0,1) so `.linescore input` and friends, which set a
   light colour to sit on the navy scoreboard, still win. */
input, textarea { color: var(--text); }
:focus-visible { outline: 2px solid var(--navy); outline-offset: 2px; }
::selection { background: rgba(0,50,120,0.16); }

.app { max-width: 1600px; margin: 0 auto; padding: 0 18px 24px; }

/* ---------- Masthead ---------- */
.header-bar {
  display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap;
  padding: 10px 0 8px;
  border-top: 5px solid var(--text);
  border-bottom: 1px solid var(--text);
}
.masthead .kicker {
  font-family: var(--heading); font-size: 11px; font-weight: 600;
  letter-spacing: 0.34em; text-transform: uppercase; color: var(--accent);
  margin-bottom: 3px;
}
.header-bar h1 {
  font-family: var(--heading); font-weight: 600;
  font-size: 25px; letter-spacing: 0.02em; text-transform: uppercase;
  line-height: 1; white-space: nowrap;
}
.header-bar h1 span { color: var(--navy); font-style: italic; }

.header-actions { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; align-items: center; }
.header-actions button,
.game-timer button,
.ghost-btn {
  padding: 8px 14px; min-height: 40px;
  border: 1px solid var(--rule); border-radius: 2px;
  background: transparent; color: var(--text); cursor: pointer;
  font-family: var(--heading); font-size: 13px; font-weight: 600;
  letter-spacing: 0.04em; transition: background .12s, border-color .12s, color .12s;
}
.header-actions button:hover,
.game-timer button:hover,
.ghost-btn:hover { background: rgba(0,50,120,0.07); border-color: var(--navy); color: var(--navy); }
.header-actions button:active, .ghost-btn:active { background: rgba(0,50,120,0.14); }
.header-actions button.primary { background: var(--navy); border-color: var(--navy); color: #fff; }
.header-actions button.primary:hover { background: #002559; color: #fff; }
.header-actions button.danger { color: var(--accent); border-color: rgba(192,17,31,0.35); }
.header-actions button.danger:hover { background: rgba(192,17,31,0.08); border-color: var(--accent); color: var(--accent); }

.game-timer { display: flex; align-items: center; gap: 8px; }
.game-timer .timer-display {
  font-family: var(--mono); font-size: 14px; font-weight: 600;
  min-width: 54px; text-align: center; color: var(--text-light);
}

/* ---------- Game details (collapsed) ---------- */
.game-info-card { padding: 0; margin: 0; }
details.info-details > summary {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 2px; font-family: var(--heading); font-size: 13px; font-weight: 600;
  letter-spacing: 0.04em; color: var(--text-light);
}
details.info-details > summary::-webkit-details-marker { display: none; }
details.info-details > summary::before {
  content: '▸'; font-size: 11px; color: var(--navy); transition: transform .15s;
}
details.info-details[open] > summary::before { content: '▾'; }
details.info-details > summary:hover { color: var(--navy); }
.info-body {
  display: flex; flex-wrap: wrap; gap: 6px 30px;
  padding: 6px 0 14px; border-bottom: 1px solid var(--rule); margin-bottom: 10px;
}
.info-group { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; }
.info-row { display: flex; align-items: center; gap: 6px; }
.info-row label, .ump-row label, .umpires-box h3 {
  font-family: var(--heading); font-weight: 600; font-size: 11px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-light); white-space: nowrap;
}
.info-row input, .ump-row input, .info-row select {
  border: none; border-bottom: 1px solid var(--border-light);
  /* 16px, not 14: below 16 iOS Safari zooms the page in on focus (F47). 106px of
     usable column here, and the longest thing typed into it is a team name, which
     already ran past the box at 12px — so the size costs nothing. */
  background: transparent; padding: 5px 2px; font-size: 16px; width: 110px;
  border-radius: 0;
}
.info-row select { width: 62px; cursor: pointer; }
.ump-row input { width: 74px; }
.info-row input:focus, .ump-row input:focus, .info-row select:focus {
  outline: none; border-bottom: 2px solid var(--navy);
}
.umpires-box { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ump-row { display: flex; align-items: center; gap: 5px; }

/* Always-visible teams line (outside the details) */
.teams-line { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* ---------- Scoreboard strip ---------- */
.linescore-wrap {
  background: var(--navy); color: #fff; border-radius: 2px;
  padding: 8px 14px; margin: 8px 0 10px;
}
.linescore-wrap h3 {
  font-family: var(--heading); font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--gold); opacity: .8; margin-bottom: 6px; font-weight: 600;
}
.linescore { border-collapse: collapse; }
.linescore th, .linescore td {
  border: 1px solid rgba(255,255,255,0.16); text-align: center;
  padding: 0; width: 30px; height: 23px; font-size: 12px;
}
.linescore th {
  font-family: var(--heading); font-weight: 600; font-size: 11px;
  color: var(--gold); opacity: .75; letter-spacing: .04em; border-bottom-color: rgba(255,255,255,0.35);
}
.linescore th.team-col { width: 118px; text-align: left; padding-left: 8px; opacity: 1; }
.linescore td { background: rgba(255,255,255,0.04); }
.linescore td.team-col {
  text-align: left; padding-left: 8px; background: transparent;
  font-family: var(--heading); font-weight: 600; font-size: 14px; color: #fff;
  /* A club name past the column used to wrap, and a two-line row is a taller
     strip — 14px off the one-page fit on an 11" iPad, for nothing. Same answer
     the batting order gives a long name (see fitName in app.js): never wrap,
     let the box take what it reasonably can, then shrink the text. */
  white-space: nowrap;
}
/* The column grows to the name up to this cap and no further. 168px clears
   every club name at full size with the strip stacked, where the table has the
   width to itself; the deck block below tightens it for the case where the
   live readout is competing for the same row. The span is what bounds the
   column: an inline-block's max-width caps the cell's max-content
   contribution, where a `max-width` on the `td` itself is a suggestion the
   auto table layout is free to ignore. Middle-aligned because
   an inline-block with `overflow: hidden` takes its baseline from its bottom
   edge, which would drop the row back down the height this rule just saved.
   The ellipsis is the backstop for a name past shrinking. */
.linescore td.team-col > span {
  display: inline-block; vertical-align: middle; max-width: 168px;
  overflow: hidden; text-overflow: ellipsis;
}
.linescore td.total { background: rgba(255,255,255,0.12); }
/* 16px against the iOS zoom threshold (F47). The box is 25px on an 11" iPad in
   landscape and 31px in portrait, so 16px holds the two digits any real inning or
   total needs — `buildLinescore` caps the field at two for exactly that reason.
   The phone keeps 12px in the two blocks near the end of this file: its box is
   16.3px, where even one digit is tight, so 16px is not available there at all and
   `maximum-scale=1` is what covers it. */
.linescore input {
  width: 100%; height: 100%; border: none; text-align: center; background: transparent;
  font-family: var(--mono); font-size: 16px; font-weight: 600; color: #fff; padding: 0;
}
.linescore input:focus { outline: 2px solid var(--gold); outline-offset: -2px; }
.linescore td.total input { color: var(--gold); font-weight: 700; }
.linescore td.ls-active { background: rgba(255,255,255,0.22) !important; box-shadow: inset 0 0 0 2px var(--gold); }
/* `position: relative` is here for the hit-area overlay the stacked strip
   hangs off this chip — see the 834px block. The chip itself is small on
   purpose: it shares the caption's line box, and that line is pure height. */
.ei-btn {
  position: relative;
  padding: 4px 10px; font-size: 11px; font-weight: 600; font-family: var(--heading);
  letter-spacing: .06em; background: transparent; color: var(--gold);
  border: 1px solid rgba(255,255,255,0.45); border-radius: 2px; cursor: pointer; vertical-align: middle;
}
.ei-btn:hover { background: rgba(255,255,255,0.16); }

.ls-out-dot {
  width: 11px; height: 11px; border-radius: 50%;
  border: 1.5px solid rgba(255,255,255,0.45); display: inline-block;
}
.ls-out-dot.active { background: var(--accent); border-color: var(--accent); }
.ls-base { transition: fill .15s; }
.live-cell {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; padding: 0 16px; border-left: 1px solid rgba(255,255,255,0.2);
}
.live-cell .live-label {
  font-family: var(--heading); font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--gold); opacity: .7; white-space: nowrap;
}

/* The matchup cell: who is at the plate, over who is on the mound. Left-aligned
   and tighter than the other live cells, because two stacked name/label pairs
   centred against each other read as four unrelated lines. */
.live-cell.live-matchup { align-items: flex-start; min-width: 170px; gap: 1px; }
.live-matchup .ls-label-2 { margin-top: 5px; }
/* `width: 100%`, not the fit-content this flex column would otherwise give it:
   the name is measured against its own box before it is shrunk to fit (see
   fitName), and a box that is only ever as wide as the text it already holds
   says there is no room even for an em dash. The ellipsis stays as a backstop
   for a name past shrinking. */
.live-matchup .ls-name {
  font-family: var(--font); font-size: 15px; font-weight: 600; color: #fff;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;
}
.live-matchup .ls-name-sm { font-size: 14px; }
.live-matchup .ls-sub {
  font-family: var(--mono); font-size: 11px; color: rgba(255,255,255,0.6);
}

/* ---------- Team tabs ---------- */
.tab-bar { display: flex; gap: 26px; border-bottom: 1px solid var(--rule); }
.tab-btn {
  padding: 7px 0 6px; min-height: 38px;
  font-family: var(--heading); font-size: 16px; font-weight: 600; letter-spacing: .02em;
  background: none; border: none; border-bottom: 3px solid transparent;
  color: var(--text-light); cursor: pointer; margin-bottom: -1px;
}
.tab-btn:hover { color: var(--navy); }
.tab-btn.active { color: var(--text); border-bottom-color: var(--accent); }
.tab-content { display: none; }
.tab-content.active { display: block; padding-top: 6px; }
.main-area { min-width: 0; }

/* ---------- Section (sub) tabs: Batting / Pitching / Spray & Notes ---------- */
.sec-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 2px 0 6px; }

/* I5 — a control the rules already refuse says so before the tap, not after.

   `aria-disabled`, not `disabled`: a disabled button swallows its own click,
   and on an iPad the `title` never appears, so the scorer would get a grey key
   and no way to learn why. Dimmed, the key still takes the tap, still runs the
   same guard, and still raises the same named refusal — so this is purely
   additive and no entry path changed.

   Beginner only. Expert mode is today's app and an expert reads a refusal as
   fast as a dimmed key. Gated here rather than in app.js, so the pass can run
   unconditionally and there is no mode test to fall out of step with. */
[data-mode="beginner"] .quick-btn[aria-disabled="true"] {
  opacity: .38;
  border-style: dashed;
}

/* I8 — the key says what it means, in Beginner mode.

   `GO` and `FC` are the notation a scorer is learning, and the deck taught
   neither. The `<small>` slot beside each code holds the hotkey letter, which
   on an iPad with no keyboard is the one thing on the button that cannot be
   used — so in Beginner mode the word takes the slot instead.

   Part C's rule for this one: dual labels must stack into the `<small>` slot
   and must never widen a key. Stacking is what makes that possible. The letter
   sits *beside* the code (`margin-left: 3px`), so a key is as wide as
   code + letter; stacked, it is as wide as the wider of code and word, and at
   9px against 15px the word usually loses. `1B` + ` 1` is wider than `single`
   set under it.

   Every measured figure for this is in the block at the end of the file, taken
   at all five sizes before it was committed — which is what the audit asked
   for, this being the only Beginner surface that spends the height budget. */
[data-mode="beginner"] .quick-btn[data-word] {
  display: inline-flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 1px; line-height: 1.05;
}
/* The hotkey letter goes; the word takes its place. Not `display: none` on the
   whole `<small>` rule — the buttons with no word keep their letter, because
   Beginner mode is not a reason to stop a Magic Keyboard working. */
[data-mode="beginner"] .quick-btn[data-word] small { display: none; }
/* 8px, and that one point is the whole fit. At 9px the core row wraps to two
   at 1024x768 — 63px to 109px, and the deck 72px to 118px, which takes a
   batting slot — and overflows at 844x390. At 8px the row does not move at any
   of the five sizes. The figures are in the block below. */
[data-mode="beginner"] .quick-btn[data-word]::after {
  content: attr(data-word);
  font-family: var(--font); font-size: 8px; font-weight: 400;
  letter-spacing: 0; opacity: .75; white-space: nowrap;
}

/* I14 — the nine corrections become one question in Beginner mode. A straight
   swap, so it costs no width in a deck that has none to give: Expert keeps all
   nine keys, Beginner gets the one that asks which of them you wanted. */
.qb-fix-that { display: none; }
[data-mode="beginner"] .qb-fix-that { display: inline-flex; }
[data-mode="beginner"] .qb-fix-nine { display: none; }

/* I2 — the four out keys become one walkthrough in Beginner mode. Same swap as
   I14 above and for the same reason: measured at 844x390 in Beginner mode the
   deck's first line left 14.6px of slack, and the cheapest key that could carry
   this label is 89.7px. Addition was never available.

   Measured after building it, at 844x390: GO/FO/PO/LO come to 219.5px with
   their gaps, the Guide key spends 101.9px with its own, and Beginner's first
   line goes from 789.4px to 667.9px in an 804px box — 14.6px of slack becomes
   136.1px. Expert is untouched at 746.4px, as the mode contract requires.

   The hotkeys are untouched. `g`, `x`, `p` and `l` are read by the keydown
   handler, which calls `promptGroundout` and `promptPositionPlay` directly —
   so a Magic Keyboard scorer in Beginner mode keeps the fast path to the very
   codes the sheet is teaching, which is the point at which they stop needing
   the sheet.

   **The `.quick-btn` in each selector is load-bearing, not noise.** I8's
   `[data-mode="beginner"] .quick-btn[data-word]` above is three selectors of
   specificity and sets `display: inline-flex` on every worded key — which these
   five are. Written as plainly as I14's pair (`.qb-fix-nine`, two selectors)
   they lose to it and all five keys show at once. I14 never hit this because
   the nine correction keys carry no words. Do not "simplify" these back. */
.quick-btn.qb-guide { display: none; }
[data-mode="beginner"] .quick-btn.qb-guide { display: inline-flex; }
[data-mode="beginner"] .quick-btn.qb-out-four { display: none; }

/* I7 — "Read it back", and the same specificity caveat as above. This is the
   one key Beginner mode *adds* rather than swaps, which it can only afford
   because I2's swap left 136.1px of slack on the first deck line; the measured
   figure after both is in the I2 block. */
.quick-btn.qb-read { display: none; }
[data-mode="beginner"] .quick-btn.qb-read { display: inline-flex; }

/* One fact per line, so the readout is scanned rather than read. */
.read-line { font-size: 13px; line-height: 1.45; color: var(--text); }

/* ---------- The play, in a sentence, kept on screen (I4, Beginner half) ----

   Expert mode gets nothing here — since I1 a completed play is recorded in
   silence, and the toast is spent on refusals and caveats only. Beginner mode
   gets this instead of a toast, so the sentence can be read at the scorer's
   own pace rather than chased for 2.2 seconds, and nothing covers the card.

   Part C is the whole reason for the shape. At 1194x834 the card is nine rows
   at the 44px floor with about 5px to spare, so a new row under the deck is
   not on offer: the echo has to *replace* space, not ask for it. Measured, the
   section bar at that size has **361px of empty middle** between "Spray &
   Notes" and "Show sub rows" — a row that already exists, already has height,
   and is already directly under the deck where I4 wants the line.

   So `.sec-row` is a wrapper whose `display` decides, and there is exactly one
   echo element in one place in the DOM:

     display: flex   the echo shares the bar's row, costing no height at all
     display: block  the echo falls to its own line under it

   Adam's ruling: take the bar's room where it exists and a line where it does
   not. Measured after building it, Expert against Beginner at each size:

                     bar spare   placement   row grows   batting slots
     1194x834           361px    in the bar        0px    9.1 -> 9.1
     1024x768           191px    in the bar        0px    9.5 -> 9.5
      834x1194         -24px*    own line         18px    9.6 -> 9.6
      390x844              6px   own line         18px    6.7 -> 6.3
      844x390             77px   own line         18px    1.9 -> 1.9

   * the bar has already wrapped to three rows at that size.

   The page scrolls by 0 at all five, in both modes. Only one size pays a slot
   for the line, and it is the one that was already scrolling the card by 142px
   — the other two that grow a row absorb the 18px out of slack the layout
   already had. The size that must not lose a row is the 11" iPad in landscape,
   which is what Part C's 5px is about, and it is also the one with the most
   bar to spare: it keeps all nine.

   That is better than the estimate this was chosen on, which had the three
   own-line sizes each losing about 0.4 of a row. Two of them lose nothing. */
.sec-row { margin: 2px 0 6px; }
.sec-row > .sec-bar { margin: 0; }

.play-echo { display: none; }
[data-mode="beginner"] .play-echo {
  display: block; margin: 0; min-width: 0;
  font-family: var(--font); font-size: 13px; line-height: 1.35;
  color: var(--text);
  /* One line, and the end clipped rather than the row grown: this sits in a
     control row whose height other things are measured against. The full
     sentence is on the cell's own aria-label either way. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Nothing recorded yet — says what the line is for rather than sitting blank,
   which reads as something failing to load. Short enough to survive the
   narrowest slot it gets: 341px at 1194x834, where the longer version this
   started as was ellipsised mid-sentence. */
[data-mode="beginner"] .play-echo:empty::before {
  content: 'Each play will be explained here.';
  color: var(--text-light);
}

/* Sharing the bar's row. The bar shrinks to its controls, which parks "Show
   sub rows" against them rather than at the far edge — its `margin-left: auto`
   has no slack left to push into — and the echo takes what is left. */
@media (min-width: 835px) and (min-height: 501px) {
  [data-mode="beginner"] .sec-row { display: flex; align-items: center; gap: 12px; }
  [data-mode="beginner"] .sec-row > .sec-bar { flex: 0 1 auto; }
  [data-mode="beginner"] .sec-row > .play-echo { flex: 1 1 0; text-align: right; }
}
.sec-btn {
  padding: 5px 14px; min-height: 34px; cursor: pointer;
  font-family: var(--heading); font-size: 13px; font-weight: 600; letter-spacing: .03em;
  background: transparent; border: 1px solid var(--rule); border-radius: 2px; color: var(--text-light);
}
.sec-btn:hover { border-color: var(--navy); color: var(--navy); }
.sec-btn.active { background: var(--navy); border-color: var(--navy); color: #fff; }
.sec-panel { display: none; }
.sec-panel.active { display: block; }

/* ---------- Quick entry bar ---------- */
.quick-bar { display: flex; flex-direction: column; margin-bottom: 6px; }
.qb-core {
  display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
  padding: 6px 0; border-bottom: 1px solid var(--rule);
}
.qb-drawer {
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height .22s ease, opacity .18s ease, padding .22s ease;
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 0;
}
.qb-drawer.open { max-height: 620px; opacity: 1; padding: 8px 0 4px; border-bottom: 1px solid var(--rule); }
.qb-sep { width: 1px; align-self: stretch; background: var(--rule); margin: 0 4px; }
.qb-label {
  font-family: var(--heading); font-size: 11px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-light); margin-right: 2px;
}

/* I15/B12 — pitch tracking read as mandatory. "Pitch: S F B" leads the deck
   and said nothing about being a choice, and the count it feeds lives up in
   the linescore strip, far from the buttons producing it.

   Beginner mode only, and free at every size, which is why it is a second line
   on the label rather than a longer label. Measured at 844x390, the tightest
   deck there is: the word sets at 8px and is narrower than "Pitch" at 11px, so
   the label stays 43px and deck line one stays 746px in an 804px box — the
   same numbers as without it. Height is free too: the row is 53px because the
   buttons beside it are 40px, and a 21px label still clears it.

   An expert is not told; he already scores without pitches when he wants to.

   `.qb-label-pitch`, not `.qb-label`: there are four of these and two of them
   say "Clear", on the button that wipes a play. Told they were optional. */
[data-mode="beginner"] .qb-label-pitch {
  display: inline-flex; flex-direction: column; align-items: center; line-height: 1.1;
}
[data-mode="beginner"] .qb-label-pitch::after {
  content: 'optional';
  font-size: 8px; letter-spacing: .04em; font-weight: 600; opacity: .8;
}
.qb-more-btn {
  margin-left: auto; padding: 8px 14px; min-height: 40px; cursor: pointer;
  font-family: var(--heading); font-size: 13px; font-weight: 600; letter-spacing: .04em;
  border: 1px solid var(--rule); border-radius: 2px; background: transparent; color: var(--text-light);
}
.qb-more-btn:hover { border-color: var(--navy); color: var(--navy); }
.qb-more-btn.open { background: var(--navy); border-color: var(--navy); color: #fff; }

.quick-btn {
  padding: 8px 12px; min-height: 40px; min-width: 44px; cursor: pointer;
  font-family: var(--mono); font-size: 13px; font-weight: 600;
  border: 1px solid var(--rule); border-radius: 2px; background: var(--card); color: var(--text);
  transition: background .1s, border-color .1s, color .1s;
}
.quick-btn:hover { background: var(--cream-dark); border-color: var(--navy); }
.quick-btn:active { background: rgba(0,50,120,0.14); }
.quick-btn small { font-size: 9px; opacity: .45; margin-left: 3px; font-family: var(--font); }
.quick-btn.hit { color: var(--accent); border-color: rgba(192,17,31,0.35); }
.quick-btn.hit:hover { background: rgba(192,17,31,0.08); border-color: var(--accent); }
.quick-btn.out { color: var(--text); }
.quick-btn.walk { color: var(--navy); border-color: rgba(0,50,120,0.35); }
.quick-btn.walk:hover { background: var(--blue-light); border-color: var(--navy); }
/* .quick-btn.runner is styled in the Rangers block, not here. It used to carry a
   green pair at this position too — the last two references to `--green` — but
   both were overridden further down and neither ever painted: the runner plays
   have been navy since the re-skin, because three brand colours is the whole
   conceit and a fourth hue on the deck was never part of it. Deleted with the
   token rather than left as a second opinion the cascade already discarded. */
.quick-btn.undo-play { color: var(--accent); border-color: rgba(192,17,31,0.35); }
.quick-btn.undo-play:hover { background: rgba(192,17,31,0.08); }
.quick-btn.pitcher-change { color: #6a1b9a; border-color: rgba(106,27,154,0.35); }
.quick-btn.pitcher-change:hover { background: rgba(106,27,154,0.08); }

/* Grouped drawer */
.qbg-tabs { display: flex; gap: 6px; flex-wrap: wrap; width: 100%; }
.qbg-tab {
  padding: 7px 14px; min-height: 38px; cursor: pointer;
  font-family: var(--heading); font-size: 13px; font-weight: 600; letter-spacing: .04em;
  background: transparent; border: 1px solid var(--rule); border-radius: 2px; color: var(--text-light);
}
.qbg-tab:hover { border-color: var(--navy); color: var(--navy); }
.qbg-tab.active { background: var(--navy); border-color: var(--navy); color: #fff; }
.qbg-panel { display: none; width: 100%; flex-wrap: wrap; gap: 6px; align-items: center; padding-top: 8px; }
.qbg-panel.active { display: flex; }

/* ---------- Scoring grid ---------- */
.grid-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
.scoring-grid { border-collapse: collapse; table-layout: fixed; width: auto; }
.scoring-grid tr.pos-starter { height: var(--cell-h); }
.scoring-grid th, .scoring-grid td { border: 1px solid var(--border-light); }
.scoring-grid thead th {
  background: var(--cream); color: var(--text-light);
  font-family: var(--heading); font-weight: 600; font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase;
  padding: 7px 2px; text-align: center; position: sticky; top: 0; z-index: var(--z-grid-sticky);
  border-bottom: 2px solid var(--text);
}
.scoring-grid thead th.player-col { text-align: left; padding-left: 8px; }
/* The column widths, moved off the inline `style` attributes buildScoringGrid
   used to write. Same numbers it wrote — deliberately not the `td` widths
   below, which differ by a pixel or four and have never had any effect:
   `table-layout: fixed` resolves the whole table from this first row and
   ignores every width declared on a body cell. Here so the phone block can
   override them. */
.scoring-grid thead th.ord-col  { width: 20px; }
.scoring-grid thead th.num-col  { width: 30px; }
.scoring-grid thead th.name-col { width: 170px; }
.scoring-grid thead th.pos-col  { width: 34px; }
.scoring-grid thead th.avg-col  { width: 40px; }
.scoring-grid thead th.inn-col {
  font-family: var(--heading); font-size: 15px; color: var(--text); letter-spacing: 0;
  border-bottom: 2px solid var(--text);
}

.scoring-grid td.order-cell {
  width: 22px; text-align: center; vertical-align: middle;
  font-family: var(--heading); font-weight: 600; font-size: 15px; color: var(--text-light);
  background: var(--cream);
}
.scoring-grid td.num-cell { width: 30px; text-align: center; padding: 0; }
.scoring-grid td.player-cell { width: 170px; padding: 0; }
.scoring-grid td.pos-cell { width: 40px; text-align: center; padding: 0; }
.scoring-grid td.avg-cell { width: 44px; text-align: center; padding: 0; }
/* ---------- F27 — the lineup's controls are the size of their cell ----------

   `height: 100%` below has never resolved. A percentage height on the child of
   a table cell needs the cell to have a definite height, and these do not — the
   row does (`tr.pos-starter { height: var(--cell-h) }`), the cell inherits it by
   layout rather than by declaration, and the input falls back to its content
   height. Every control in the batting order was therefore 24px tall (28px for
   the text inputs) whatever the row underneath it was doing — and the row is
   44-55px on an iPad. Half to two-thirds of every cell in the order — the
   jersey, the name, the position, the average — was not a target at all, and
   tapping the part of the cell above or below the text did nothing.

   Making the cell a containing block and pinning the control to it is what the
   two declarations were already asking for. Nothing moves: an input centres its
   text in its content box, and the content box is still centred in the cell, so
   every glyph stays where it was — drift measured at 0.00px on all four. It
   costs the one-page fit nothing, because this is space the cell was already
   occupying; the control now simply covers it.

   The gain is whatever `fit()` decides the row is worth, so it is a range
   rather than a number: 42px where the flat drawer squeezes hardest, 53px on an
   iPad in portrait. The table above the `pointer: coarse` block has all five.

   The three frozen columns are `position: sticky` on a phone (see the frozen
   block near the end of the file), which is also a containing block, so they
   need nothing extra there. */
.scoring-grid td.num-cell, .scoring-grid td.player-cell,
.scoring-grid td.pos-cell, .scoring-grid td.avg-cell { position: relative; }
.scoring-grid td.num-cell input,
.scoring-grid td.player-cell input,
.scoring-grid td.avg-cell input {
  position: absolute; inset: 0;
  /* 16px against the iOS zoom threshold (F47). Only the name column keeps it —
     `.num-cell` and `.avg-cell` restate 12px below, because they cannot hold 16.
     A name too long for its column is shrunk *inline* by `shrinkName` and lands
     under 16 anyway, which is why the viewport meta is the real fix here. */
  width: 100%; height: 100%; border: none; background: transparent; padding: 6px 6px; font-size: 16px;
}
.scoring-grid td.player-cell input { font-weight: 600; }
/* F48 — these two are `disabled`, so they cannot take focus, cannot raise a
   keyboard and cannot trigger the zoom. That is why their 12px is safe, and it is a
   fact about the DOM rather than a bet on Safari honouring a meta tag — `readonly`
   plus `tabindex="-1"` was the first attempt and it does *not* refuse focus, only
   the tab order. The cell around them is the control now (`role="button"`), and it
   opens the sheet.

   Two consequences to undo. A disabled input is greyed and dimmed by the engine, so
   the colour and opacity come back explicitly — the value has to read exactly as it
   did when it was editable. And `pointer-events: none` hands the tap to the cell: a
   real browser suppresses mouse events on a disabled control, so without this the
   click never reaches the delegated handler. */
.scoring-grid td.num-cell input:disabled,
.scoring-grid td.avg-cell input:disabled {
  opacity: 1; color: var(--text); -webkit-text-fill-color: currentColor;
  pointer-events: none; cursor: inherit;
}
.scoring-grid td.num-cell, .scoring-grid td.avg-cell { cursor: pointer; }
.scoring-grid td.num-cell:focus-visible,
.scoring-grid td.avg-cell:focus-visible { outline: 2px solid var(--navy); outline-offset: -2px; }

/* The two columns that stay under 16px, and the reason is width, not taste (F47).
   Measured in iPad *portrait*, which is tighter than landscape: AVG has 28px of
   usable column and ".333" at 16px needs 38.4px; the # has 17.2px and "24" needs
   19.2px. F13 already took the side padding off both to buy the digits room, so
   there is nothing left to reclaim, and `table-layout: fixed` is over-constrained
   below 1100px — a declared width here is ignored. So these two keep 12px and
   `maximum-scale=1` in index.html is what stops them zooming. Raising them without
   widening the columns cuts a batting average mid-glyph. */
.scoring-grid td.num-cell input,
.scoring-grid td.avg-cell input { text-align: center; font-family: var(--mono); font-size: 12px; font-weight: 500; }
/* A 2-digit jersey is ~16px of 12px mono, and at 1024px the # column gets 24px —
   but 12 of those went on side padding, leaving 11px, so every 2-digit number was
   cut mid-glyph, and a `.333` in AVG with it (F13). Both are centre-aligned, so
   the side padding was buying nothing; the digits get the cell instead. Widening
   the column was the other way and it does nothing: the grid is `table-layout:
   fixed` and already over-constrained at ≤1100px, so a declared width there is
   ignored — and at 1024x768 `--cell-h` sits on its 44px floor, so there are no
   pixels to take from the at-bat columns anyway. */
.scoring-grid td.num-cell input,
.scoring-grid td.avg-cell input { padding-left: 1px; padding-right: 1px; }
.scoring-grid input:focus { outline: 2px solid var(--navy); outline-offset: -2px; }
.scoring-grid td.pos-cell select {
  position: absolute; inset: 0;
  width: 100%; height: 100%; border: none; background: transparent;
  /* 16px against the iOS zoom threshold (F47); "1B" needs 19.2px and the column
     gives 29.3px even in iPad portrait, which is the tighter of the two. */
  font-family: var(--mono); font-size: 16px; cursor: pointer;
  appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; padding: 4px 0;
}
.scoring-grid td.pos-cell select:focus { outline: 2px solid var(--navy); outline-offset: -2px; }
.scoring-grid th.stat-col { width: 30px; font-size: 10px; letter-spacing: .06em; }
.scoring-grid td.stat-cell {
  width: 30px; text-align: center; vertical-align: middle; padding: 0;
  font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--text-light);
  background: var(--cream);
}

.pos-starter td { background: var(--card); }
.pos-sub td { background: var(--row-alt); }
.pos-sub td.stat-cell { background: var(--row-alt); }
/* 16px like the starter above it (F47 — below 16 iOS zooms on focus). The sub
   line no longer reads subordinate by *size*, so the indent, the lighter weight and
   the .75 opacity in the re-skin block carry that on their own now. */
.pos-sub td.player-cell input { font-style: italic; padding-left: 16px; font-weight: 400; font-size: 16px; }
/* H3 — this placeholder is the only thing naming which slot a sub row belongs to,
   and it is the whole content of every empty sub row the "show subs" toggle
   reveals at once. It was painted in --border-light, rgba(0,50,120,0.2), which
   over the --row-alt these rows sit on (#f7f9fc) measures 1.46:1 — a hairline
   token doing a label's job. --text-light, the same 0.72 navy every other
   secondary label in this file wears, measures 5.25:1 on the same ground.
   The italic stays: it is what separates a placeholder from a typed-in name. */
.pos-sub td.player-cell input::placeholder { font-style: italic; color: var(--text-light); }
/* Sub rows stay out of the way until they are used (or the scorer asks for them).
   `visibility: collapse` rather than `display: none`: the at-bat cells span the
   whole slot with rowspan, and a display:none row still holds its slot open. */
.scoring-grid tr.pos-sub { visibility: collapse; }
.scoring-grid tr.pos-sub:has(input[data-field="name"]:not(:placeholder-shown)),
.scoring-grid tr.pos-sub:focus-within { visibility: visible; }
.grid-wrap.show-subs .scoring-grid tr.pos-sub { visibility: visible; }
/* One slot's row, opened by SUB. `:focus-within` above cannot get there by itself:
   a collapsed row refuses focus, so the field the scorer needs is unreachable until
   something shows the row first. Per-slot rather than `show-subs`, which opens all
   eighteen and costs about half the visible batting slots on an iPad (F7). */
.scoring-grid tr.pos-sub.revealed { visibility: visible; }
/* ---------- A shown sub row is as tall as it says it is ----------

   It had no height at all, and unlike `tr.pos-starter` — which declares --cell-h
   right above — it could not fall back on its content for one: F27 pinned every
   control in the lineup to its cell with `position: absolute; inset: 0`, and an
   absolutely positioned child contributes nothing to its parent's intrinsic height.
   So all four cells measured empty, the row came out the height of its own borders,
   and a sub opened by SUB was a line the scorer could see appear and could not read.

   Half a starter, and derived rather than fixed so it tracks the fit: 22px where the
   flat drawer squeezes hardest, 31px on a roomy iPad. Subordinate to the starter at a
   glance, which is the same thing the 16px name indent below is saying.

   Declared on the shown states rather than on `tr.pos-sub` itself. A row under
   `visibility: collapse` should contribute no height whatever is declared on it, but
   "should" is doing the work in that sentence across four engines, and the resting
   card — nine slots, no subs, the case that has to fit on one page — is not worth
   staking on it. These are the same four selectors that turn the row on above.

   fit() needs no change: it already measures these rows (`subs`, ui.js) and takes
   them out of the starters' share. It has been measuring 2px slivers and will now
   measure the truth. */
.scoring-grid tr.pos-sub:has(input[data-field="name"]:not(:placeholder-shown)),
.scoring-grid tr.pos-sub:focus-within,
.scoring-grid tr.pos-sub.revealed,
.grid-wrap.show-subs .scoring-grid tr.pos-sub { height: calc(var(--cell-h) / 2); }
/* The controls have to fit the shorter row too, or the fix stops at the border and
   the text inside is still cut. They inherit 6px of vertical padding from the lineup
   rule above, which at a 22px row leaves 10px of content box for a 13px line. The
   side padding is left alone — that is the name's indent and the # column's F13
   fix, neither of which is about height. */
.scoring-grid tr.pos-sub td.num-cell input,
.scoring-grid tr.pos-sub td.player-cell input,
.scoring-grid tr.pos-sub td.avg-cell input { padding-top: 1px; padding-bottom: 1px; }
.scoring-grid tr.pos-sub td.pos-cell select { padding-top: 0; padding-bottom: 0; }
/* Slot separation without a heavy rule */
.scoring-grid tr.pos-starter > td { border-top: 2px solid var(--text); }
.scoring-grid tbody tr.pos-sub:last-child > td { border-bottom: 1px solid var(--border-light); }

/* At-bat cells */
.at-bat-cell {
  width: var(--cell-w); height: var(--cell-h);
  padding: 2px; vertical-align: top; position: relative; cursor: pointer; background: var(--card);
}
.at-bat-cell.selected { background: var(--select) !important; outline: 2px solid var(--accent); outline-offset: -2px; }
.at-bat-cell .diamond-wrap {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; position: relative;
}
.at-bat-cell .play-text {
  position: absolute; font-family: var(--mono); font-weight: 700; text-transform: uppercase;
  color: var(--text); pointer-events: none; line-height: 1; z-index: 1; white-space: nowrap;
}
.at-bat-cell .play-text.play-out {
  top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 16px; text-align: center;
  text-shadow: 0 0 4px #fff, 0 0 8px rgba(255,255,255,.95);
}
.at-bat-cell .play-text.play-on { bottom: 1px; right: 3px; font-size: 15px; text-shadow: 0 0 3px #fff; }
.diamond-wrap:has(.diamond-svg.scored) ~ .play-text { color: #fff; text-shadow: none; }
.at-bat-cell .out-num {
  position: absolute; bottom: 1px; right: 3px; font-family: var(--mono);
  font-size: 15px; font-weight: 700; color: transparent; user-select: none; z-index: 1;
}
.at-bat-cell .out-num.active { color: var(--text); }

.pitch-track { position: absolute; top: 2px; left: 3px; display: flex; gap: 3px; z-index: 1; line-height: 1; }
.pitch-col { display: flex; flex-direction: column; }
/* Long at-bats: step the marks down so a column of fouls can never run past the
   bottom of the cell. The ceiling is derived from --cell-h rather than hard-coded,
   because the cell shrinks to 55/58/60px at the breakpoints above while a column of
   marks would not: a column starts 3px down (the cell's 1px border plus the track's
   2px top) and has to stop 2px clear of the bottom padding edge, which leaves
   (--cell-h - 5px) to divide between n marks at line-height 1. 11px is the cap, so
   marks only shrink when the column is genuinely too tall to fit at full size.

   `--pm-n` is the marks-per-column renderPitches settled on, written onto the track
   as it draws. It replaced a ladder of `:has(.pitch-mark:nth-child(n))` rules, one
   per length, which could only count as high as somebody remembered to write a rule
   for — 7, because that was the old wrap. Nothing wraps now: the track is capped at
   two columns and a long at-bat goes on dividing, so the ladder had no top to reach.

   The fallback of 5 is renderPitches' own floor, so a track that somehow paints
   before the property lands is at its resting size rather than its largest. At the
   fitted 44px iPad row that is 7.8px, up from the 5.6px the old 7-per-column worst
   case gave — the marks are the binding constraint on the play tints above, so this
   is slack the palette gains rather than spends. Past thirteen pitches they go under
   6px and keep going: two columns whatever it costs, which is the trade Adam picked
   over letting a third one open into the diamond. */
.pitch-mark {
  font-size: min(11px, calc((var(--cell-h) - 5px) / var(--pm-n, 5)));
  font-weight: 700; line-height: 1; font-family: var(--mono);
}
.pitch-mark.strike { color: var(--accent); }
.pitch-mark.foul { color: #55504a; }
.pitch-mark.ball { color: var(--navy); }

.rbi-badge { display: none; }
.rbi-badge.active { display: inline; }
.rbi-dot-mark {
  display: inline-block; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent); margin-right: 2px; vertical-align: middle;
}

/* Play-type cell tints — quiet washes, not blocks of color */
.at-bat-cell.play-k   { background: rgba(192,17,31,0.10) !important; }
.at-bat-cell.play-hit { background: rgba(23,105,58,0.12) !important; }
.at-bat-cell.play-bb  { background: rgba(0,50,120,0.10) !important; }
.at-bat-cell.play-dp  { background: rgba(106,27,154,0.10) !important; }
.at-bat-cell.play-hr  { background: rgba(192,17,31,0.20) !important; }
.at-bat-cell.play-go  { background: rgba(32,30,29,0.05) !important; }
.at-bat-cell.selected.play-k,
.at-bat-cell.selected.play-hit,
.at-bat-cell.selected.play-bb,
.at-bat-cell.selected.play-dp,
.at-bat-cell.selected.play-hr,
.at-bat-cell.selected.play-go { outline: 2px solid var(--accent); outline-offset: -2px; }

.at-bat-cell .pitcher-change-mark {
  position: absolute; top: 0; left: 0; right: 0; z-index: 2;
  border-top: 2px solid var(--accent); display: none;
}
.at-bat-cell .pitcher-change-mark.active { display: block; }
.at-bat-cell .pitcher-change-mark::after {
  content: attr(data-pnum); position: absolute; top: 0; right: 0;
  font-family: var(--mono); font-size: 9px; font-weight: 700; line-height: 1;
  color: #fff; background: var(--accent); padding: 1px 3px; border-radius: 0 0 0 2px;
}
.at-bat-cell .pitcher-change-mark[data-pnum=""]::after { content: none; }
.at-bat-cell .sub-change-mark {
  position: absolute; top: 0; left: 0; bottom: 0; z-index: 2;
  border-left: 2px solid var(--accent); display: none;
}
.at-bat-cell .sub-change-mark.active { display: block; }
.hidden-inning { display: none !important; }

/* Diamond */
.diamond-svg { width: 52px; height: 52px; display: block; margin: 0 auto; }
.diamond-svg .base-line { stroke: var(--border-light); stroke-width: 1.5; stroke-linecap: round; transition: stroke .15s, stroke-width .15s; }
.diamond-svg .base-dot { fill: var(--border-light); }
.diamond-svg .seg.reached .base-line { stroke: var(--text); stroke-width: 3.5; }
.diamond-svg .seg.reached .base-dot { fill: var(--text); }
.diamond-svg .home-dot { fill: #b9b5b0; }
.diamond-svg .seg.reached ~ .home-dot { fill: var(--text); }
.diamond-svg.scored .base-line { stroke: var(--text); stroke-width: 3.5; }
.diamond-svg.scored .diamond-fill { fill: var(--text); opacity: .88; }
.diamond-svg .diamond-fill { fill: transparent; pointer-events: none; }
.diamond-svg .out-on-path { stroke: var(--text); stroke-width: 3.5; stroke-linecap: butt; }
.diamond-svg .out-on-cross { stroke: var(--text); stroke-width: 2.5; stroke-linecap: butt; }
.diamond-svg .adv-label { font-size: 8px; font-weight: 700; fill: var(--text); font-family: var(--mono); pointer-events: none; }

/* ---------- Pitcher table ---------- */
.pitcher-section h3 {
  font-family: var(--heading); font-size: 12px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-light); margin-bottom: 6px;
}
.pitcher-grid { border-collapse: collapse; }
.pitcher-grid th, .pitcher-grid td { border: 1px solid var(--border-light); }
.pitcher-grid th {
  background: var(--cream); color: var(--text-light);
  font-family: var(--heading); font-size: 11px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; padding: 6px 6px; text-align: center;
  border-bottom: 2px solid var(--text);
}
.pitcher-grid th.pitcher-name-col { text-align: left; padding-left: 8px; width: 170px; }
.pitcher-grid th.pitcher-num-col { width: 34px; }
.pitcher-grid td { padding: 0; height: 32px; background: var(--card); }
.pitcher-grid td input {
  width: 100%; height: 100%; border: none; text-align: center; background: transparent;
  /* 16px against the iOS zoom threshold (F47), and the 4px of side padding goes
     with it: every one of these is centred, so the padding bought nothing, and the
     widest real value — an IP of "10.1" at 38.4px — does not fit the 35px the
     padding left. The same trade F13 made on the # and AVG columns. */
  font-family: var(--mono); font-size: 16px; padding: 4px 1px;
}
.pitcher-grid td input:focus { outline: 2px solid var(--navy); outline-offset: -2px; }
.pitcher-grid td.p-name input { text-align: left; padding-left: 8px; font-family: var(--font); font-weight: 600; }
.pitcher-grid td.p-stat { width: 44px; }
/* Wider than the 44px stat columns because it holds two figures and a hyphen: a
   complete-game "52-83" is 5 characters of 12px mono, and a 44px cell with the
   input padding these carry cut it. Sized for that rather than for a 3-digit half,
   which would mean 260 pitches. */
.pitcher-grid th.pitcher-bs-col { width: 58px; }
.pitcher-grid td.p-bs {
  width: 58px; text-align: center; font-family: var(--mono); font-size: 12px;
  color: var(--text-light); background: var(--cream);
}
.pitcher-grid th.pitcher-era-col { width: 52px; }
.pitcher-grid td.p-era {
  width: 52px; text-align: center; font-family: var(--mono); font-size: 13px;
  color: var(--text-light); background: var(--cream);
}
.pitcher-grid td.p-stat.er-review { position: relative; background: rgba(192,17,31,0.10); }
.pitcher-grid td.p-stat.er-review::after {
  content: ''; position: absolute; top: 0; right: 0;
  border-width: 0 8px 8px 0; border-style: solid;
  border-color: transparent var(--accent) transparent transparent; pointer-events: none;
}

/* ---------- Spray + notes ---------- */
/* Both team tabs carry this row; the single .notes-box moves between them. */
.spray-row { display: flex; gap: 30px; align-items: flex-start; flex-wrap: wrap; }
.spray-row .notes-box { flex: 1; min-width: 280px; }
.spray-mini { text-align: center; }
.spray-mini h4 {
  font-family: var(--heading); font-size: 12px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-light); margin-bottom: 6px;
}
.spray-mini-svg { border: 1px solid var(--border-light); border-radius: 2px; background: var(--card); }
.notes-box label {
  display: block; margin-bottom: 6px;
  font-family: var(--heading); font-size: 12px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-light);
}
.notes-box textarea {
  /* 16px against the iOS zoom threshold (F47) — a full-width box, so it is free. */
  width: 100%; min-height: 220px; padding: 10px 12px; font-size: 16px; resize: vertical;
  font-family: var(--font); background: var(--card);
  border: 1px solid var(--border-light); border-radius: 2px;
}
.notes-box textarea:focus { outline: 2px solid var(--navy); outline-offset: -1px; }

.spray-popup {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
  background: var(--card); border: 1px solid var(--text); border-radius: 2px;
  /* A popup, not a menu: it is in ALSO_GUARDED (app.js) and the backdrop goes
     up behind it, so it belongs on the popup rung with the fourteen app.js
     builds rather than on the dropdown rung it happened to share (F29). */
  padding: 18px; z-index: var(--z-popup); box-shadow: 0 12px 32px rgba(45,43,43,0.22);
  text-align: center; display: none;
}
.spray-popup h3 {
  font-family: var(--heading); font-size: 16px; font-weight: 600; margin-bottom: 10px;
}
.spray-popup svg { cursor: crosshair; border: 1px solid var(--border-light); border-radius: 2px; }
.spray-popup .spray-hint { font-size: 12px; color: var(--text-light); margin-top: 8px; }
.spray-popup .spray-skip {
  margin-top: 10px; padding: 8px 22px; min-height: 40px; cursor: pointer;
  font-family: var(--heading); font-size: 13px; font-weight: 600;
  border: 1px solid var(--rule); border-radius: 2px; background: transparent;
}
.spray-popup .spray-skip:hover { background: var(--cream-dark); border-color: var(--navy); }

/* ============================================================
   The popups app.js builds (I11, closing H1 / H2 / H4)

   Thirteen popups were assembled from inline styles carrying ~160 hardcoded
   hex values, in a Material palette that exists nowhere in this file: #ccc
   x38, #fff x34, #f5f5f5 x14, #555 x13, #2e7d32 x13, #e8f5e9 x6, #fce4ec x4,
   #1565c0 x4. They did not look like the app because they were not painted
   from it. Everything below is tokens, and app.js now names classes.

   Ahead of the guided builder (I2) rather than after it: that step adds a
   fourteenth full-screen sheet, and building it against a palette that was
   about to be deleted means building it twice.

   --- Why the shell scrolls (H2, and a live defect found measuring it) ---

   Popup controls ran 22-30px against the 40x40 the deck holds itself to at
   phone landscape, and the measured target table above never covered them.
   Raising them costs height, and there was none: measured at 844x390 with the
   bases loaded, the DP runner-outcome popup was **426px tall in a 390px
   viewport** before any of this — 18px clipped off the top and 18px off the
   bottom, which is the title cut in half and Confirm unreachable. Not a
   regression from raising targets; the popup was already broken there, and
   `position: fixed` centring means it clips at *both* ends with no scroll to
   recover either.

   So the shell caps its own height and scrolls, and the action row is pinned
   outside the scroller so Confirm can never be the thing that scrolls away.
   That is what makes 40px affordable.

   Measured after, at 844x390: three popups scroll their bodies — outcome by
   185px, pitcher by 82px, runner by 31px — and all three keep their title and
   their actions on screen. Every other popup fits whole. At the four other
   target sizes nothing scrolls at all: the worst of them stands 559px against
   768, 834 or 844 of height. The per-control figures are in the target table
   above, with what each one measured before.

   `dvh`, not `vh`: on iOS Safari `vh` is the *largest* viewport, so a cap in
   vh is taller than the screen whenever the toolbars are showing, which is
   the case this exists to catch.

   --- Selected state (H4) ---

   Selection was colour alone and carried no `aria-pressed`. It now carries
   both, plus two channels that survive greyscale: an inset 2px ring where
   the unselected chip has a 1px hairline, and 700 against 600. A glyph was
   the other candidate and was dropped on width — these chips wrap, and in
   the outcome popup every extra line is 40px of a popup already scrolling.
   ============================================================ */
.jsp {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
  z-index: var(--z-popup);
  display: none; flex-direction: column;
  background: var(--card); border: 3px solid var(--navy); border-radius: 10px;
  box-shadow: 0 8px 40px rgba(0,50,120,0.4);
  font-family: var(--font);
  padding: 14px 16px; gap: 8px;
  /* One family, bounded both ways. The thirteen shells carried five different
     minimums (220/240/260/280/300) and four different caps (none/340/360/380)
     picked one popup at a time; 280 is the median of the first set and 460
     clears the widest popup measured (the outcome popup at 422px) while still
     stopping a one-sentence popup from running the full width of an iPad. */
  min-width: min(280px, calc(100vw - 24px));
  max-width: min(460px, calc(100vw - 16px - var(--sa-l) - var(--sa-r)));
  max-height: calc(100dvh - 16px);
  /* The backstop, for the popups that have no `.jsp-body` to absorb overflow
     into. Where there is one it takes the scroll first and this never fires;
     where there is not, a popup that outgrows the screen scrolls whole rather
     than losing its bottom edge, which is the failure mode being fixed. */
  overflow-y: auto;
}
/* The strikeout picker and the base picker are a title and a row of choices
   with nothing to read, so they centre and set in the display face. */
.jsp--centred { text-align: center; font-family: var(--heading); }

/* ---------- F46 — the sheet that names a substitute -------------------------

   Two departures from `.jsp`, and both are about the iPad keyboard.

   `top: 50%` is where every other popup sits, and it is wrong for the only popup
   in the app whose whole job is to be typed into. `position: fixed` anchors to the
   *layout* viewport, which iOS does not shrink when the keyboard opens — so a
   popup centred at 417px of an 834px screen has its lower half behind a ~350px
   keyboard, and the app has no `visualViewport` listener to notice. Anchored near
   the top instead, a ~170px sheet spans 12-182px and clears any iPad keyboard by
   construction rather than by measuring one.

   And the fields are 16px, against `.jsp-input`'s 12px. Below 16px Safari on iOS
   zooms the page in on focus and does not zoom back out on blur — the defect this
   sheet exists to route around. The 16px lives here, on a sheet nobody is reading
   the card through, so the card keeps its 13px italic sub line unchanged. */
.jsp--top { top: calc(12px + var(--sa-t)); transform: translateX(-50%); }

/* No. | Player | Pos, labels over fields. The name takes what the other two do
   not: a jersey is at most three digits and a position at most two characters, and
   the name is the one that runs long. */
.sn-grid {
  display: grid; grid-template-columns: 3.5em 1fr 4.5em;
  gap: 3px 6px; align-items: center; width: 100%;
}
.sn-grid > .jsp-hint { align-self: end; }
/* 16px is not a taste decision here — it is the threshold below which iOS Safari
   zooms. Do not lower it. The family is the card's, so what the scorer types reads
   the way it will read on the line.

   `.jsp-input` carries 16px itself since F47, so this is no longer the only thing
   holding the sheet above the threshold — but the qualification stays. Written as a
   bare `.sn-field` it *lost*: `.jsp-input`'s old 12px was declared ~190 lines
   further down at equal specificity, and the later rule won. It measured 12px in
   the browser while the stylesheet plainly said 16 — the sheet open, anchored and
   focused, and still zooming. Two classes beat one wherever the blocks sit, so
   neither a block being moved nor `.jsp-input` being lowered again can undo it. */
.jsp-input.sn-field {
  font-family: var(--font); font-size: 16px; min-height: 44px;
  padding: 6px 10px; width: 100%;
}
.jsp-input.sn-field--num, .jsp-input.sn-field--pos,
.jsp-input.sn-field--avg {
  font-family: var(--mono); text-align: center; padding: 6px 2px;
}
/* AVG on its own row under the other three, which already fill the width. Left
   aligned with the No. column so the sheet reads as two lines of one form rather
   than two separate forms (F48). */
.sn-grid--avg { grid-template-columns: 3.5em 6em; }

/* ---------- The guided builder's sheet (I2) ----------------------------------

   The one popup that fills the screen, and it has to. Part C's two budgets are
   both spent — ~5px of margin at 1194x834, 14.6px on the deck's first line at
   phone landscape — so a twelfth centred popup carrying the field map, a live
   code strip and five answers had nowhere to be. Full-screen costs nothing
   against either budget, which is exactly why the audit specified it.

   It is still a `.jsp`: same border colour, same tokens, same `.jsp-body`
   scroller, same named controls. Only the box changes, so everything I11 swept
   onto the vocabulary applies here without a second palette to keep in step.

   The height it has to fit, worst case, is the map step at 844x390: the map is
   180, the two action rows are 40 each, the strip about 48 and the title 18 —
   326 plus padding and gaps against 390. It fits, and `.jsp-body` takes the
   scroll if a future answer list ever does not. The 40px readout the position
   popup spends on `#pos-input` is what pays for the strip: the strip *is* the
   readout here, and a better one, because it carries the sentence too. */
.jsp--sheet {
  top: 0; left: 0; transform: none;
  width: 100%; height: 100%;
  min-width: 0; max-width: none; max-height: none;
  border-width: 0; border-radius: 0;
  padding: calc(8px + var(--sa-t)) calc(12px + var(--sa-r)) 8px calc(12px + var(--sa-l));
  align-items: center;
}

/* Full-screen is a height decision, not a width one. At 844x390 the sheet is
   full of what it is asking; at 1194x834 the same content left ~350px of dead
   space under the map and stretched two action rows across the full 1194, which
   read as an unfinished screen rather than a deliberate one. So the column is
   capped and centred — 520px clears the 300px map with room either side — and
   the body centres what it holds between the strip and the actions.

   Only the *body* centres. The strip stays pinned at the top, where the audit
   put it and where a scorer's eye returns to watch the code grow, and the
   actions stay pinned at the bottom, where every other popup in the app keeps
   them. It is the answers in the middle that move. */
.jsp--sheet > * { width: 100%; max-width: 520px; }
.jsp--sheet .jsp-body { justify-content: center; }

/* The code being built, and what it says. Lifted above the question by `order`
   rather than by being written first: `labelPopup` names a dialog after its
   first child, and the question is the right name for the screen — "6-3" is
   not. Both wants are satisfied because `.jsp` is already a flex column. */
.guide-strip {
  order: -1; flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 6px 8px; background: var(--cream); border-radius: 4px;
}
/* The notation, in the face the card writes it in and at the size the card
   writes it at — this is a preview of the mark about to be made, so it should
   not be the first time the scorer sees that shape. */
.guide-code {
  font-family: var(--mono); font-size: 22px; font-weight: 700;
  letter-spacing: 1px; color: var(--navy); line-height: 1.1;
}
.guide-said {
  font-size: 12px; line-height: 1.3; color: var(--text); text-align: center;
  min-height: 1.3em;   /* holds its line, so answering does not jump the list */
}

/* app.js opens popups with `display: flex` or `display: block` through
   `openPopup`, which writes the inline value — so the class only has to
   supply the resting `none` and the column direction. */
.jsp-title {
  flex: 0 0 auto;
  font-family: var(--heading); font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1px; color: var(--navy);
}
/* The only part that scrolls. `min-height: 0` because a flex item's default
   `auto` floor is its content, which would push the shell past its own
   max-height and put the clipping straight back. */
.jsp-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 8px;
}
.jsp-actions { flex: 0 0 auto; display: flex; gap: 6px; }
.jsp-note {
  flex: 0 0 auto;
  font-size: 11px; color: var(--accent); line-height: 1.4;
}
.jsp-hint { font-size: 11px; color: var(--text-light); line-height: 1.4; }

/* One runner and his choices. */
.jsp-group { padding: 7px 8px; background: var(--cream); border-radius: 4px; }
.jsp-group-head {
  font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 5px;
}
.jsp-group-head .jsp-from { color: var(--text-light); font-weight: 500; }
.jsp-chips { display: flex; gap: 4px; flex-wrap: wrap; }

/* The other runner popup lays a runner out along a line — name, the base he
   is leaving, then his choices — rather than in a boxed group. */
.jsp-runner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.jsp-runner--batter { border-top: 1px solid var(--border-light); padding-top: 8px; }
.jsp-runner-name {
  font-size: 12px; font-weight: 600; color: var(--text);
  min-width: 100px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.jsp-runner-from { font-size: 11px; color: var(--text-light); min-width: 24px; }
/* "on E" answers *why* he took the base, not which one, so it stands off from
   the destinations it rides beside. */
.jsp-chip--tick { margin-left: 6px; }

/* 40px is the deck's own phone-landscape figure, and these are controls a
   scorer uses mid-play — the same standard, at last. */
.jsp-chip {
  min-height: 40px; padding: 4px 9px; cursor: pointer;
  font-family: var(--mono); font-size: 11px; font-weight: 600;
  border: 1px solid var(--border-light); border-radius: 4px;
  background: var(--card); color: var(--text-light);
}
.jsp-chip:hover:not(:disabled) { background: var(--safe-soft); }
.jsp-chip--out:hover:not(:disabled),
.jsp-chip--pick:hover:not(:disabled) { background: var(--blue-light); }

/* Selection fills. F11b settled that on the runner popup — the chosen state
   there had been a pale tint a shade off the unchosen one, on a popup whose
   whole job is showing you what you have answered — and the argument is no
   weaker on its sibling, so both wear it now. It is also the honest answer to
   H4: an inverted button differs from an outlined one in greyscale, so the
   state is not carried by hue alone, and `aria-pressed` carries it to anyone
   not looking at hue at all.

   Green safe, red out. Two popups asked the same question in two palettes —
   the outcome popup in Material green, the runner popup in navy — which is
   the colour half of B9, the wording half having gone in step 0. */
.jsp-chip[aria-pressed="true"] {
  font-weight: 700;
  border-color: var(--safe); background: var(--safe); color: var(--gold);
}
.jsp-chip--out { color: var(--accent); }
.jsp-chip--out[aria-pressed="true"] {
  border-color: var(--accent); background: var(--accent); color: var(--gold);
}
/* Selection that is not safe-or-out — a pitcher, a base, an Earned run. Navy
   is the app's own "this one", and these have no opposite to distinguish. */
.jsp-chip--pick[aria-pressed="true"] {
  border-color: var(--navy); background: var(--navy); color: var(--gold);
}
/* Dashed, not just faint: the reason is on the button's own `title` and in
   the note under the list, and the scorer has to be able to tell a refused
   option from a merely unchosen one before tapping it. */
.jsp-chip:disabled {
  cursor: not-allowed; opacity: 0.5;
  border-style: dashed; border-color: var(--border-light);
  background: var(--card); color: var(--text-light);
}
.jsp-chip:focus-visible, .jsp-btn:focus-visible, .jsp-list-btn:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

.jsp-btn {
  min-height: 40px; padding: 6px 12px; cursor: pointer;
  font-family: var(--heading); font-size: 12px; font-weight: 700;
  letter-spacing: 0.5px; text-transform: uppercase;
  border: 1px solid var(--rule); border-radius: 4px;
  background: var(--cream); color: var(--text);
}
.jsp-btn--primary {
  flex: 2; border: none; background: var(--navy); color: var(--gold);
}
.jsp-btn--minor { flex: 1; }
/* A popup whose only action is "I am done looking at this" gives it the whole
   width rather than pairing it with a primary. */
.jsp-btn--wide { width: 100%; flex: none; }
.jsp-btn--danger { color: var(--accent); border-color: var(--accent); }
.jsp-btn:hover:not(:disabled) { background: var(--cream-dark); border-color: var(--navy); }
.jsp-btn--primary:hover:not(:disabled) { background: var(--navy); filter: brightness(1.15); }

/* A described row with its controls pushed to the far end — the earned-run
   review's "who scored, and how he reached" against Earned / Unearned. */
.jsp-split {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 5px 0; border-top: 1px solid var(--border-light);
}
.jsp-split-text { font-size: 12px; line-height: 1.3; }
.jsp-split-name { font-weight: 600; color: var(--text); }
.jsp-split-sub { font-size: 11px; color: var(--text-light); }
.jsp-chips--tight { flex: 0 0 auto; flex-wrap: nowrap; }

/* A label and a field on one line — "Or position play: [______]". */
.jsp-inline { display: flex; gap: 6px; align-items: center; }
/* 16px against the iOS zoom threshold (F47). This one rule is every field in
   every popup — the paste sheet's lineup box and its Save-as, the position-play
   box, the sub-name sheet — and none of them sits on the card, so there is no
   layout to trade for it. */
.jsp-input {
  flex: 1; min-height: 40px; padding: 4px 8px;
  font-family: var(--mono); font-size: 16px;
  border: 1px solid var(--border-light); border-radius: 4px;
  background: var(--card); color: var(--text);
}
.jsp-input:focus { outline: 2px solid var(--navy); outline-offset: -1px; }

/* A vertical list of choices — pitchers, substitutes, positions. */
.jsp-list { display: flex; flex-direction: column; gap: 4px; }
.jsp-list-btn {
  display: block; width: 100%; text-align: left;
  min-height: 40px; padding: 6px 10px; cursor: pointer;
  font-family: var(--font); font-size: 12px; font-weight: 500;
  border: 1px solid var(--border-light); border-radius: 4px;
  background: var(--card); color: var(--text);
}
.jsp-list-btn[aria-pressed="true"] {
  font-weight: 700;
  border-color: var(--navy); background: var(--blue-light); color: var(--navy);
  box-shadow: inset 0 0 0 2px var(--navy);
}
.jsp-list-btn:hover { background: var(--cream); border-color: var(--navy); }
.jsp-list-btn[aria-pressed="true"]:hover { background: var(--blue-light); }
/* The second line under a choice — what taking it will do. */
.jsp-list-note {
  font-size: 11px; font-weight: 400; margin-top: 2px; color: var(--text-light);
}
/* A choice that undoes something, or re-enters a player the league may not
   allow. The reason is in its own note, so the border only has to flag it. */
.jsp-list-btn--warn { border-color: var(--accent); }
.jsp-list-btn--warn .jsp-list-note { color: var(--accent); }
/* "Auto (apply the rules)" — the recommended way out of a decision popup, set
   apart from the pitchers listed above it. */
.jsp-list-btn--lead {
  margin-top: 6px; font-weight: 700;
  border-color: var(--navy); background: var(--cream);
}
.jsp-list-aside { color: var(--text-light); font-size: 11px; font-weight: 400; }
/* The inning a change is being recorded at, beside the popup's own title. */
.jsp-when {
  font-size: 11px; color: var(--accent); font-weight: 600; margin-left: 6px;
}
.jsp-check {
  display: flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--text-light);
}
.jsp-title--centred { text-align: center; }
/* The line the fielder pad is building — `6-3` as it is assembled. A readout
   first and a field second: it is readonly until "Type it" hands it over. */
.jsp-readout {
  width: 100%; box-sizing: border-box; min-height: 40px;
  font-family: var(--mono); font-size: 22px; font-weight: 700;
  padding: 6px 8px; text-align: center; text-transform: uppercase;
  border: 2px solid var(--navy); border-radius: 6px;
  background: var(--card); color: var(--navy);
}
/* The one action big enough to find without looking — DONE on the fielder
   picker, which a scorer presses mid-play with the game still going. */
.jsp-btn--lg { min-height: 46px; font-size: 14px; letter-spacing: 1px; }

/* ---------- The paste sheet -------------------------------------------------

   A lineup box and the parser's reading of it. Both want the whole width of
   the sheet's 520px column, and the box wants to be the size of the thing
   going into it — nine lines — so the scorer can see the whole lineup at once
   rather than scrolling a three-line field. */

/* The guided sheet centres its body, because the answers in the middle are the
   thing being looked at and they move as the code is built. This sheet is a
   form read top to bottom — type here, then read what that gave you — so it
   starts at the top and lets the slack fall at the bottom. Centred, it opened
   with ~300px of empty sheet above the box on a phone. */
#paste-lineup-popup .jsp-body { justify-content: flex-start; }
.pl-text {
  width: 100%; box-sizing: border-box;
  min-height: 148px; resize: vertical; line-height: 1.5;
  /* Overrides `.jsp-input`'s centring flex share — this is a block, not a
     field sharing a line with a label. */
  flex: 0 0 auto;
}
/* The reading. Compact on purpose: nine rows plus their warnings have to sit
   under the box without pushing the actions off a phone in landscape. */
.pl-table {
  width: 100%; border-collapse: collapse;
  font-size: 12px; line-height: 1.3;
}
.pl-table th {
  font-family: var(--heading); font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--text-light); text-align: left;
  padding: 2px 4px; border-bottom: 1px solid var(--border-light);
}
.pl-table td {
  padding: 3px 4px; border-bottom: 1px solid var(--border-light);
  color: var(--text);
}
/* The slot number, which is the whole point of the preview: it says which line
   of the paste lands on which line of the card. */
.pl-table .pl-slot {
  width: 22px; text-align: center;
  font-family: var(--mono); font-weight: 700; color: var(--navy);
}
.pl-table .pl-mono { font-family: var(--mono); }
/* A row the parser had to guess at. Tinted rather than reddened: the reading is
   probably right, and this is "look at this one", not "this is wrong". */
.pl-row--warn td { background: var(--cream); }
.pl-warn { font-size: 11px; color: var(--accent); }
/* A saved lineup and its remove button on one line. */
.pl-saved-row { display: flex; gap: 4px; align-items: stretch; }
.pl-saved-row > .jsp-list-btn { flex: 1 1 auto; }
.pl-saved-row > .pl-forget { flex: 0 0 auto; min-width: 40px; }
/* The scorebook mark over the word for it — `K` above SWINGING. This popup
   opens by itself at the third strike, so the two glyphs are the thing the
   scorer is aiming at and they get the room. */
.jsp-btn--glyph {
  flex: 1; min-height: 52px; padding: 8px 18px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; font-size: 17px; letter-spacing: 1px;
}
.jsp-btn--glyph small { font-size: 10px; font-weight: 600; letter-spacing: 0.5px; }
.jsp-actions--centred { justify-content: center; gap: 12px; }
/* A base taken on the defence's mistake rather than the runner's legs. */
.jsp-btn--onerror { background: var(--accent); }
.jsp-btn--onerror:hover:not(:disabled) { background: var(--accent); filter: brightness(1.15); }

/* H2 named this one by name: `font-size:10px;padding:2px 7px`, about 13px
   tall, on the control that re-attributes every recorded at-bat to a
   different pitcher. Not a popup, but the same defect and the same fix. */
.fix-stats-btn {
  min-height: 40px; padding: 4px 10px; cursor: pointer;
  font-family: var(--heading); font-size: 11px; font-weight: 700;
  letter-spacing: 0.3px;
  border: 1px solid var(--navy); border-radius: 4px;
  background: var(--card); color: var(--navy);
}
.fix-stats-btn:hover { background: var(--cream); }

/* The third control H2 measured — the summary's W/L/SV override, 9px type in a
   1px-by-5px box. It sits inline in a sentence, so it stays visually small and
   grows its *target* instead: the padding does the work and the line-height
   keeps it from pushing the sentence apart. */
.gs-change-btn {
  margin-left: 6px; min-height: 40px; padding: 4px 10px; cursor: pointer;
  font-family: var(--font); font-size: 11px; line-height: 1;
  border: 1px solid var(--border); border-radius: 4px;
  background: transparent; color: var(--text-light);
}
.gs-change-btn:hover { background: var(--cream); color: var(--navy); }

/* ============================================================
   The fielder picker's field map (I3)

   `showPositionPopup` asked for the fielders on a 3x3 numeric grid — `1 P`
   through `9 RF` in reading order, which is a phone dialpad and not where
   any of those players stand. Fielder numbering is the one thing a new
   scorer has to internalise, it is inherently spatial, and the grid taught
   none of it. Same nine numbers, same `posPadTap`, same "Type it" escape
   hatch; only the layout moved onto a field.

   The keys are still real <button>s, absolutely positioned over an SVG of
   the park, rather than <g> elements inside it. That keeps focus order, the
   focus ring, :active and `data-d` for free, and it keeps the nine controls
   measurable for the target table above — which is where H2 wants them.

   Geometry lives in `FIELD_MAP` (app.js), not here. JS writes the box and
   key sizes onto `.pos-map` as --pm-w / --pm-h / --pk-w / --pk-h and each
   key's centre as a left/top percentage, so one table drives the drawing,
   the controls and the overlap test. What is left here is palette and type.

   Sizes, and why these:

   The map is 300x180 with 44x40 keys. Height is the only scarce axis — the
   popup is fixed and centred, so it has to fit the shortest screen whole or
   DONE goes off the bottom edge. Measured before the change, at 844x390:
   popup 352px tall in a 390px viewport, 19px clear above and below. The
   3x3 grid was 160px of that.

   180px of map is 20px more than the grid, and measured at 372px the popup
   had 9px left each side. So the phone-landscape block below takes that 20px
   back out of the popup's own padding and row gaps (14px -> 8px, 8px -> 6px
   over four rows), which measures 352px with 19px clear above and below —
   the same fit the 3x3 grid had, to the pixel.

   Nothing else on the page moves: the popup is fixed, so it is not in the
   `fit()` budget and cannot lengthen the card. Checked at all five target
   sizes; the page still scrolls by 0 at 1194x834 with the popup open.

   Width is free at every target size. 300 + 32 padding + 6 border = 338px,
   against 390px on the narrowest phone in portrait. --pm-s is the safety
   for anything narrower still; it scales the box, the keys and the type
   together, so no separation closes up.

   The 44x40 key is what the geometry is solved against: no two of the nine
   boxes overlap, at any of the 36 pairs, and `FIELD_MAP` is pinned to that
   in the suite. Shrink a key and the test still passes; move one and it
   may not, which is the point.
   ============================================================ */
.pos-map {
  --pm-s: 1;
  position: relative; margin: 0 auto;
  width: calc(var(--pm-w) * var(--pm-s));
  height: calc(var(--pm-h) * var(--pm-s));
}

/* The park itself: fair territory, the infield, the bags, the plate. Purely
   a drawing — every tap belongs to a button sitting on top of it.

   Nine 44x40 keys cover a third of a 300x180 box and six of them stand in
   the infield, so most of the diamond is behind a key and only reads in the
   gaps. Drawn first in the two soft fills alone it did not: the dirt is a
   0.2 tint over a 0.13 one, which is a 0.07 step, and at that contrast the
   infield vanished under the keys and the map read as chips on a grey blob.

   So the *lines* carry it instead of the fills. Base paths and foul lines
   are solid navy at hairline weights, which survives being interrupted —
   two visible segments of a straight navy line still read as the line — and
   the bags and plate are white against the tint, which is what they are.
   The fills are left soft on purpose; they say where the grass is, and a
   heavier tint behind white-on-navy keys only muddies them. */
.pos-map-field { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.pos-map-field .pm-fair { fill: var(--line-soft); }
.pos-map-field .pm-dirt { fill: var(--border-light); stroke: var(--navy); stroke-width: 1.2; }
.pos-map-field .pm-foul { fill: none; stroke: var(--navy); stroke-width: 1; }
.pos-map-field .pm-bag  { fill: var(--card); stroke: var(--navy); stroke-width: 1; }

.pos-key {
  position: absolute; transform: translate(-50%, -50%);
  width: calc(var(--pk-w) * var(--pm-s));
  height: calc(var(--pk-h) * var(--pm-s));
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; padding: 0; border: none; border-radius: 7px; cursor: pointer;
  background: var(--navy); color: var(--gold);
  font-family: var(--heading); font-weight: 700; line-height: 1;
  box-shadow: 0 1px 3px rgba(0,50,120,0.35);
}
.pos-key .pk-n { font-family: var(--mono); font-size: calc(18px * var(--pm-s)); }
.pos-key .pk-p { font-size: calc(8px * var(--pm-s)); letter-spacing: 0.5px; opacity: 0.85; }
/* The press has to be visible on a key that is 40px tall and sitting on a
   busy drawing, and the translate has to be restated or the key jumps to
   the corner it is positioned from. */
.pos-key:active { transform: translate(-50%, -50%) scale(0.94); }
.pos-key:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Narrower than any of the five target sizes. Scales the whole map rather
   than reflowing it, so the solved separations hold. */
@media (max-width: 359px) { .pos-map { --pm-s: 0.84; } }

/* ---------- Modals ---------- */
.modal-overlay, .hotkey-modal, .game-summary-modal {
  position: fixed; inset: 0; background: rgba(32,30,29,0.55); z-index: var(--z-modal);
  display: none; align-items: center; justify-content: center;
}
/* The summary can open with the Game Library already up, so it takes the rung
   above the plain overlay — which is what the bare 600 here was buying (F29). */
.game-summary-modal { z-index: var(--z-modal-child); align-items: flex-start; justify-content: center; padding: 20px; overflow-y: auto; }
.modal-overlay.active, .hotkey-modal.active, .game-summary-modal.active { display: flex; }
.modal-box, .hotkey-modal-inner, .game-summary-inner {
  background: var(--card); border: 1px solid var(--text); border-radius: 2px;
  box-shadow: 0 12px 32px rgba(45,43,43,0.3);
}
.modal-box { padding: 24px 26px; max-width: 640px; width: 92%; max-height: 82vh; overflow-y: auto; }
.hotkey-modal-inner { padding: 24px 30px; max-width: 760px; width: 92%; max-height: 86vh; overflow-y: auto; }
.game-summary-inner { padding: 26px 32px; max-width: 900px; width: 96%; margin-top: 20px; }
.modal-box h2, .hotkey-modal h2 {
  font-family: var(--heading); font-size: 22px; font-weight: 600;
  margin-bottom: 14px; padding-bottom: 8px; border-bottom: 2px solid var(--text);
}
.modal-box .game-list { list-style: none; }
.modal-box .game-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 0; border-bottom: 1px solid var(--rule);
}
.modal-box .game-list li .game-info-text { font-size: 14px; font-weight: 600; }
.modal-box .game-list li .game-date { font-size: 12px; color: var(--text-light); font-family: var(--mono); }
.modal-box .game-list li button {
  padding: 7px 12px; min-height: 38px; cursor: pointer; margin-left: 6px;
  font-family: var(--heading); font-size: 12px; font-weight: 600; letter-spacing: .04em;
  border: 1px solid var(--rule); border-radius: 2px; background: transparent;
}
.modal-box .game-list li button.load-btn { color: var(--navy); border-color: rgba(0,50,120,0.35); }
.modal-box .game-list li button.load-btn:hover { background: var(--blue-light); }
.modal-box .game-list li button.del-btn { color: var(--accent); border-color: rgba(192,17,31,0.35); }
.modal-box .game-list li button.del-btn:hover { background: rgba(192,17,31,0.08); }

/* I10 — the practice card's pinned row. Tinted and ruled off from the games
   below it because the one thing it must never be mistaken for is one of them:
   it is built at render time, not stored, so "Delete" would have nothing to
   delete and "Update Saved Game" must never point at it. */
.modal-box .game-list--pinned { margin-bottom: 4px; }
.modal-box .game-list li.game-practice {
  background: var(--cream); border-bottom: 2px solid var(--navy);
  padding-left: 10px; padding-right: 10px; border-radius: 4px 4px 0 0;
}
.game-pin {
  font-family: var(--heading); font-size: 11px; font-weight: 600;
  letter-spacing: .04em; color: var(--navy); margin-left: 6px;
}
/* The account of the half-inning to score. A numbered list because it is a
   sequence of plate appearances and the order is the exercise. */
.practice-list { margin: 4px 0 4px 18px; padding: 0; }
.practice-list li { font-size: 13px; line-height: 1.5; margin-bottom: 4px; }
.modal-close {
  padding: 9px 18px; min-height: 40px; cursor: pointer; margin-top: 10px;
  font-family: var(--heading); font-size: 13px; font-weight: 600; letter-spacing: .04em;
  border: 1px solid var(--rule); border-radius: 2px; background: transparent;
}
.modal-close:hover { background: var(--cream-dark); border-color: var(--navy); }

.hotkey-modal h3 {
  font-family: var(--heading); font-size: 13px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-light);
  margin: 20px 0 6px; border-bottom: 1px solid var(--rule); padding-bottom: 4px;
}
.hk-table { width: 100%; border-collapse: collapse; }
.hk-table td { padding: 5px 8px; font-size: 13px; border-bottom: 1px solid var(--rule); }
.hk-table td:first-child {
  width: 58px; font-family: var(--mono); font-weight: 700; font-size: 12px;
  text-align: center; color: var(--navy); background: var(--cream); border-radius: 2px;
}
.hk-table td:nth-child(3) { color: var(--text-light); font-size: 12px; }

/* A sentence the tables cannot hold — a fact about the section rather than a
   row in it. Used by I15 to say pitch tracking is optional. */
.hk-note {
  font-size: 13px; line-height: 1.5; color: var(--text-light);
  margin: -2px 0 10px; max-width: 62ch;
}

/* ---------- The card legend (I6) ----------
   Swatches are real `.at-bat-cell` markup wearing the card's own tint and mark
   classes, so nothing here paints them — this only lays them out and undoes
   the two things a cell inherits from being a table cell in a fitted grid:
   its --cell-w/--cell-h sizing, and the `cursor: pointer` that would offer a
   tap the legend does not take. */
#card-legend { margin-bottom: 14px; }
.lg-group { margin-bottom: 12px; }
.lg-group h4 {
  font-family: var(--heading); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-light);
  margin: 0 0 6px;
}
/* 215px, not 150: the swatch is a fixed 66px plus its 9px gutter, so a 150px
   column left ~75px for the caption and broke every one of them over four
   lines. This gives the words ~130px and lands three columns in the modal's
   700px of content. */
.lg-row {
  display: grid; gap: 10px 14px;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
}
.lg-item { margin: 0; display: flex; align-items: center; gap: 9px; }
.lg-item figcaption { font-size: 12px; line-height: 1.35; color: var(--text); }
.lg-swatch { flex: 0 0 auto; }
/* Fixed at the cell's resting size rather than the fitted one: --cell-h is
   written by `fit()` against the live card, and a legend that resized itself
   when the deck folded would be describing a shape the reader is not looking
   at. 66x56 is the ladder's own resting cell, less the padding a real one
   spends on its play text. */
.at-bat-cell.lg-cell {
  width: 66px; height: 56px; cursor: default;
  border: 1px solid var(--border-light); border-radius: 2px;
}
.at-bat-cell.lg-cell .diamond-svg { width: 44px; height: 44px; }
.at-bat-cell.lg-cell .play-text.play-out { font-size: 13px; }
.at-bat-cell.lg-cell .play-text.play-on { font-size: 12px; }
/* The RBI dots ride at the bottom-left of a real cell; in a swatch there is no
   play text to sit beside, so they are placed rather than inline. */
.at-bat-cell.lg-cell .rbi-dot-mark { position: absolute; bottom: 3px; }
.at-bat-cell.lg-cell .rbi-dot-mark:nth-of-type(1) { left: 4px; }
.at-bat-cell.lg-cell .rbi-dot-mark:nth-of-type(2) { left: 11px; }
.hotkey-close {
  /* Reachable from the More menu now (F17), so it is closed by finger as often
     as by Escape — the glyph alone was a ~17x26 target. */
  position: absolute; top: 6px; right: 10px; font-size: 26px; line-height: 1;
  min-width: 44px; min-height: 44px;
  cursor: pointer; color: var(--text-light); background: none; border: none;
}
.hotkey-close:hover { color: var(--accent); }
.hotkey-hint { text-align: center; font-size: 12px; color: var(--text-light); margin-top: 16px; font-family: var(--mono); }

/* Game summary */
.gs-header { text-align: left; margin-bottom: 18px; padding-bottom: 10px; border-bottom: 2px solid var(--text); }
.gs-header h2 { font-family: var(--heading); font-size: 26px; font-weight: 600; }
.gs-header .gs-subtitle { font-size: 13px; color: var(--text-light); margin-top: 2px; }
.gs-score-banner {
  display: flex; align-items: center; gap: 28px; padding: 16px 22px; margin-bottom: 18px;
  background: var(--navy); color: #fff; border-radius: 2px; font-family: var(--heading);
}
.gs-team-score { text-align: center; }
.gs-team-score .gs-team-name { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: var(--gold); opacity: .8; }
.gs-team-score .gs-score-num { font-size: 40px; font-weight: 600; line-height: 1.05; }
.gs-vs { font-size: 14px; color: var(--gold); opacity: .7; }
.gs-final-tag { font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--gold); }
/* Every table in here is `width: 100%`, which a table treats as a floor, not a
   cap — it can never lay out narrower than its min-content, and the linescore's
   13 columns (name + 9 innings + R/H/E) are wider than the modal on a phone.
   The overflow used to spill out of the white box and off the screen: at 390px
   the table ran to 373 with the card's content edge at 331, so the E column was
   drawn over the overlay and past the right edge with nothing to scroll — the
   modal's own `overflow-y: auto` puts no scrollable width on a centred flex
   child. The section scrolls instead, and the caption stays put while it does. */
.gs-section { margin-bottom: 20px; overflow-x: auto; }
.gs-section h3 {
  font-family: var(--heading); font-size: 13px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-light);
  border-bottom: 1px solid var(--rule); padding-bottom: 4px; margin-bottom: 8px;
  position: sticky; left: 0;
}
.gs-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.gs-table th {
  font-family: var(--heading); font-size: 11px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; text-align: center; padding: 5px 6px;
  color: var(--text-light); border-bottom: 1px solid var(--text);
}
.gs-table th:first-child { text-align: left; }
.gs-table td { padding: 5px 6px; text-align: center; border-bottom: 1px solid var(--rule); font-family: var(--mono); font-size: 12px; }
.gs-table td:first-child { text-align: left; font-family: var(--font); font-weight: 600; }
.gs-table tr.gs-totals td { font-weight: 700; border-top: 1px solid var(--text); }
.gs-highlight { display: flex; gap: 28px; flex-wrap: wrap; margin-bottom: 18px; }
.gs-highlight-card { flex: 1; min-width: 200px; }
.gs-highlight-card .gs-hl-label {
  font-family: var(--heading); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-light); margin-bottom: 4px;
}
.gs-highlight-card .gs-hl-value { font-size: 18px; font-weight: 600; }
.gs-highlight-card .gs-hl-detail { font-size: 13px; color: var(--text-light); margin-top: 2px; }
.gs-pitching-line { font-size: 13px; margin-bottom: 3px; }
.gs-plays { font-size: 13px; line-height: 1.7; }
.gs-plays span { display: inline-block; background: var(--cream); border-radius: 2px; padding: 2px 7px; margin: 2px 3px; font-weight: 600; }

/* ---------- Banners ---------- */
.banner {
  display: none; align-items: center; gap: 12px; flex-wrap: wrap;
  position: sticky; top: 0; z-index: var(--z-banner); padding: 10px 18px;
  font-family: var(--heading); font-size: 14px; font-weight: 600; color: #fff;
}
.banner span { flex: 1; min-width: 220px; }
.banner button {
  padding: 7px 14px; min-height: 38px; cursor: pointer; border-radius: 2px;
  font-family: var(--heading); font-size: 13px; font-weight: 600;
  background: #fff; border: none;
}
.banner button.hollow { background: transparent; color: #fff; border: 1px solid #fff; }

/* ---------- Touch ----------

   F27 measured every interactive element at the five viewports this app is used
   at and found that exactly one family — the deck's play buttons — met the 44px
   guideline, and only above phone landscape. The worst were the ones a scorer
   pokes at mid-game.

   The rule taken, rather than "make everything 44px": grow each target until it
   would start stealing its neighbour's taps, and never at the cost of the
   one-page fit, because the fit is the feature. Where that was not enough, the
   control is on the list below with the measurement rather than quietly left.
   Three fixes came out of it and each is commented at its own site — the lineup
   controls filling their cell (above), the linescore's two editable rows taking
   the height its column was already holding open (the 835px and phone-landscape
   blocks), and the deck handle's overlay (the foldable-deck block).

   Measured after, smallest of each kind, on the deployed site with the fit
   engine actually running — which matters more than it sounds. The lineup
   controls are as tall as their row, and the row height is not a constant in
   this stylesheet: `fit()` measures the space left over and writes --cell-h,
   which lands between its 44px floor and 62px ceiling depending on what the
   deck is doing. So these are the *fitted* numbers, and the first column is the
   worst case rather than the best one — 1194x834 is where the drawer lies flat
   and permanently open (see the F9 block), which reserves 171px and pins the
   rows to the floor. Fold that drawer and the same column reads 37x52.
   (171px, not the 177px this said before F44 trimmed the drawer's padding. The
   row stayed on its floor either way, so every figure below still holds.)

   An earlier version of this table was taken with `fit()` never having run, so
   it recorded the CSS ladder's resting 60px row instead of the fitted 44px one
   and overstated every lineup figure by about 16px. Numbers here have to come
   from a fitted layout or they are describing a page nobody sees.

                          1194x834  1024x768  834x1194   390x844   844x390
     --cell-h                 44px      48px      55px      48px      48px
     lineup name              151x42    121x46     72x53     87x46     87x46
     lineup # / AVG            27x42     24x46     20x53     25x46     25x46
     position select           37x42     34x46     30x53     33x46     33x46
     linescore R/H/E           25x31     25x31     31x39     16x23     25x24
     deck play buttons         45x46     45x46     45x46     45x46     40x40
     deck handle (target)          -         -         -    350x44    789x38
     +EI (target)              44x40     44x40     52x42     44x40     44x40
     team tabs                 97x34     97x34     97x34     86x36     88x30
     section buttons          142x32    142x32    146x34    129x36    129x30

   Before, for the two families this block moved: the lineup controls were 24px
   tall at every one of those widths (28px for the text inputs), and the
   linescore was 21 / 21 / 26 / 23 / 17.

   --- The popups app.js builds (I11) ---

   This table covered the deck, the lineup and the linescore, and stopped there.
   The thirteen popups were the family it never reached, and they ran 22-30px —
   on controls a scorer uses mid-play, with a runner on the move. H2.

   They are on the standard now, and the standard is 40px: the same figure the
   deck's own play buttons hold at phone landscape. Height is 40 at every one of
   the five sizes, because nothing about it is width-derived — so only the two
   ends of the range are worth recording, and the middle three read the same as
   the first.

                                 1194x834   844x390     before
     chip (Hold 3rd)                73x40     73x40      67x22
     primary (CONFIRM)             268x40    243x40     162x30
     minor (Cancel)                148x40    135x40      63x30
     list row (a pitcher)          242x40    242x40     180x29
     field-map key                  44x40     44x40          -
     fielder readout               300x45    300x40     300x45
     fielder DONE                  187x46    187x40     187x35
     strikeout K / ꓘ               115x53    115x53     115x43
     summary W/L/SV "change"            -         -      33x13

   Two of those are deliberately not 40 flat. The strikeout glyphs are 53px
   because that popup opens by itself at the third strike and the two marks are
   the whole target. The fielder popup's readout and DONE are 45 and 46 at rest
   and give the 5-6px back in phone landscape only, where the field map has to
   fit whole beside them — the arithmetic is in that block.

   What that costs in popup height, measured: the worst popup (runner outcomes,
   bases loaded) goes 426 -> 559px. At 1194x834, 1024x768, 834x1194 and 390x844
   it fits whole with room to spare and scrolls 0. At 844x390 it caps at 374 in
   a 390px viewport and its *body* scrolls 185px with the action row pinned
   outside the scroller — which is strictly better than what it did before,
   which was to stand 426px tall in that viewport and clip 18px off each end,
   title cut in half and Confirm unreachable. Two others scroll there and
   nowhere else: the pitcher list by 82px, and the runner popup by 31px.

   Still short, on purpose:

   - The linescore cells, 25px wide at four of the five sizes. Fifteen columns
     share the strip with the live readout and the widths are what let a
     nine-inning card sit on one row; a wider cell is a narrower readout or a
     second row.

     Re-measured for I12 on a fitted card, and there was a little more height
     than F27 found: the landscape sizes took 29 -> 32 with all nine batting
     rows still on screen (36 is where the ninth falls off, so the fourth pixel
     is the one that costs a batting slot). 834x1194 turned out to be the one
     viewport not fighting anything — caption on its own line, 301px of table
     column genuinely spare, card ending above the deck — so it reaches the
     app's 40px standard outright at 31x39, and has its own block.
   - The play buttons in phone landscape, 40x40. That width is measured, not
     chosen: line one comes to 774px against a 798px box, so 4px more per key
     overflows into a second row — and a second row costs 45px of a screen that
     has about 150px of card on it. The note in that block has the arithmetic.
   - The team tabs and section buttons, 30-36px tall. They share the single
     control row the layout was collapsed to; every pixel they gain pushes the
     grid down at every viewport, including the two that have nothing to spare.
   - The masthead's own buttons, 34-38px. On a phone they are behind the menu,
     whose items are 42px, so the small ones are the ones a finger rarely wants.

   The linescore at 390x844 is the one place nothing was gained, and I12 went
   back and confirmed it rather than taking it on trust. The table looks like it
   has 52px going spare in its column; it does not. The caption carrying +EI
   sits on the *same line* there — the table already runs flush to the box's
   right edge, and widening the cells moves nothing. Height is spent too: 7
   batting rows today, 6 at 28px. 16x23, and it is the honest floor.

   844x390 is the other one, for the height reason alone: 2 batting rows today
   and 1 at 32px. Half the visible card for 7px of target is not a trade this
   app makes. */
@media (pointer: coarse) {
  html { touch-action: manipulation; -webkit-tap-highlight-color: rgba(0,50,120,0.14); }
}

@media (max-width: 1280px) {
  .app { padding: 0 12px 24px; }
}
@media (max-width: 1024px) {
  .header-bar h1 { font-size: 18px; }
  .scoring-grid td.player-cell { width: 150px; }
  #live-stats { overflow-x: auto; }
}
@media (max-width: 834px) {
  .scoring-grid td.player-cell { width: 128px; }
  .linescore th, .linescore td { width: 26px; }

  /* +EI was a 36x23 target where the entry deck's own buttons are 44x40
     (.quick-btn). On the stacked strip the chip sits in the caption's line
     box and nothing else is on that line, so a real 44x40 box would grow the
     line by 17.5px and take every one of them off the batting order — on a
     390px phone that has ~109px of card to begin with. Same trade M3 was
     closed for, so the painted chip stays put and only the target grows.

     Downward, not centred: at 560 and below .ls-table-col is the table's
     horizontal scroller, and an overflow that is not `visible` clips the
     top edge the caption sits against — anything above the chip would be
     cut. Below it there is the caption's 6px margin and then the linescore's
     header row, which carries no handler, no input and no hit test of its
     own, so the overlay can take it. 52x42, and the chip is its top edge.

     The h3 has to be lifted for any of it to land: a table hit-tests above a
     positioned box that is a descendant of an earlier sibling, whatever the
     overlay's own z-index says, so the pseudo-element paints over the header
     row but the taps still went to the th. Raising the caption itself is what
     puts the button on top. Nothing overlaps here visually, so the lift is
     hit-testing only. */
  .linescore-wrap h3 { position: relative; z-index: 1; }
  .ei-btn { padding: 4px 12px; }
  .ei-btn::after { content: ''; position: absolute; inset: 0 -3px -20px; }

  /* The live readout is a row at every width. `display: flex` was an inline
     style on #live-stats until 60c6340 moved it into the 835px block, and
     below that breakpoint the five cells fell back to full-width blocks and
     stacked: 262px of a 390px phone spent on the readout, which started the
     batting order 742px down an 844px screen with the docked deck already
     covering the bottom 174px. No part of the card was on the page.

     The row does not fit a phone either, so it scrolls sideways — the same
     trade the linescore table above it makes. `overflow-x: auto` is already
     set at 1024px, from when the inline style was still doing this job. */
  #live-stats { display: flex; align-items: center; min-width: 0; }
  /* Or the cells shrink to fit rather than scrolling, and the two names in
     the matchup cell are what give way first. */
  #live-stats > .live-cell { flex: 0 0 auto; }
  /* The dividers separate cells from each other. At 835px and up the first
     one also parts the readout from the linescore table beside it; on the
     stacked strip it is a stray rule against the padding edge. */
  #live-stats > .live-cell:first-child { border-left: none; padding-left: 0; }
  /* Keep entry in reach: dock the quick bar to the bottom edge */
  .tab-content.active .quick-bar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-deck-docked);
    flex-direction: column-reverse; background: var(--cream);
    border-top: 1px solid var(--text); padding: 0 12px;
    box-shadow: 0 -6px 20px rgba(45,43,43,0.16);
    max-height: 82vh; overflow-y: auto;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  body { padding-bottom: calc(140px + env(safe-area-inset-bottom, 0px)); }
}

/* Phone: the scoreboard strip is a flex row of a fixed-width table and the
   live cells, and the table half is `flex: 0 0 auto` — a team name plus 13
   columns that cannot shrink to fit, and +EI only adds more. Below ~530px it
   pushed the whole document sideways. Stack the strip so the table gets the
   full width, tighten the columns enough that a nine-inning card fits, and
   let the column scroll on its own for the extra-innings case rather than
   dragging the page with it. */
@media (max-width: 560px) {
  .linescore-wrap { flex-direction: column; }
  .linescore-wrap > div:not(#live-stats) { min-width: 0; overflow-x: auto; }
  /* Auto table layout treats a cell `width` as a suggestion and will squeeze
     the columns to whatever the box allows — on an extra-innings card that
     clips the two-digit totals. Hold the intrinsic width so the columns keep
     their size and the column scrolls under them instead. */
  .linescore { width: max-content; }
  .linescore th, .linescore td { width: 19px; }
  .linescore th.team-col, .linescore td.team-col { width: 58px; padding-left: 6px; }
  /* The table is the thing that scrolls here, so a wide name column costs
     columns on screen rather than readout width. A one-line name is still
     worth it: it takes 20px off the stacked strip on a 390px phone, and that
     is 20px of batting order on a screen that only has ~109px of it (see the
     H2 note above the 834px block). The cap is set so the one column that
     lands past the edge is LOB — R, H and E stay on screen, and the column
     already scrolls for the extra-innings case. */
  .linescore td.team-col > span { max-width: 92px; }
}

/* Phone: the game summary spends 104px of a 390px screen on modal and card
   padding, which is most of what the linescore is short by. Give the box the
   width back and take the cell padding down to what a mono digit actually
   needs, and the nine-inning line — every inning plus R, H and E — fits on
   screen with room for three extra innings before the section has to scroll.
   Same trade the strip above makes: tighten enough that a regulation game
   fits, and scroll for the rest. */
@media (max-width: 560px) {
  .game-summary-modal { padding: 10px; }
  .game-summary-inner { padding: 20px 16px; width: 100%; margin-top: 10px; }
  .gs-table th, .gs-table td { padding: 5px 3px; }
  /* The banner's 28px gutters are what push the two club names into the score
     between them once the box is this narrow. */
  .gs-score-banner { gap: 14px; padding: 14px; }
}


/* ============================================================
   RANGERS DAY GAME — theme layer. Overrides only; everything
   above is the shared base, and this block has to stay ahead of
   the two layout blocks that close the file.

   Three colours only:  Blue #003278   Red #C0111F   White #FFFFFF
   Cells stay pure white; the header row and order column go solid
   navy so the card reads like a program cover.

   The palette itself is no longer here. It was a second `:root` that
   redeclared most of the first one unconditionally, which is not a
   theme switch — it is one palette shadowing another and winning on
   source order. Both are merged into the single block at the top of
   the file (F30); what is left below is rules.
   ============================================================ */

body { background: var(--paper); color: var(--rb-blue); letter-spacing: .01em; }
a { color: var(--rb-blue); }
a:hover { color: var(--rb-red); }
:focus-visible { outline: 2px solid var(--rb-red); outline-offset: 2px; }
::selection { background: rgba(192,17,31,0.2); }
.app { padding: 0 20px 140px; }

/* ---------- Masthead ---------- */
.header-bar {
  border: none; padding: 14px 20px; margin: 0 -20px;
  background: var(--rb-blue);
  border-bottom: 4px solid var(--rb-red);
  align-items: center;
}
.masthead .kicker { color: var(--rb-white); opacity: .7; letter-spacing: .34em; font-weight: 400; }
.header-bar h1 { font-size: 30px; font-weight: 400; letter-spacing: .04em; color: var(--rb-white); }
.header-bar h1 span { color: var(--rb-white); font-style: normal; opacity: .55; }

.header-actions button, .game-timer button {
  border-color: rgba(255,255,255,0.32); color: var(--rb-white); border-radius: 3px; letter-spacing: .06em;
}
.header-actions button:hover, .game-timer button:hover {
  background: rgba(255,255,255,0.16); border-color: var(--rb-white); color: var(--rb-white);
}
.header-actions button.primary { background: var(--rb-red); border-color: var(--rb-red); color: var(--rb-white); }
.header-actions button.primary:hover { background: #d9202f; color: var(--rb-white); }
.header-actions button.danger { color: var(--rb-white); border-color: var(--rb-red); }
.header-actions button.danger:hover { background: var(--rb-red); color: var(--rb-white); }
.game-timer .timer-display { color: var(--rb-white); }

.ghost-btn { border-color: var(--line); color: var(--rb-blue); border-radius: 3px; }
.ghost-btn:hover { background: var(--paper-2); border-color: var(--rb-blue); }

details.info-details > summary::before { color: var(--rb-red); }
.info-row input, .ump-row input, .info-row select { border-bottom-color: var(--line); }
.info-row input:focus, .ump-row input:focus, .info-row select:focus { border-bottom-color: var(--rb-red); }

/* ---------- Scoreboard ---------- */
.linescore-wrap {
  background: var(--rb-blue); border-radius: 4px; padding: 12px 18px; margin: 12px 0;
}
.linescore-wrap h3 { color: var(--rb-white); opacity: .6; letter-spacing: .22em; }
.linescore th, .linescore td { border-color: rgba(255,255,255,0.16); height: 27px; }
.linescore th { color: var(--rb-white); opacity: .55; font-weight: 400; }
.linescore th.team-col { opacity: .85; }
.linescore td { background: rgba(255,255,255,0.05); }
.linescore td.team-col { font-size: 15px; letter-spacing: .05em; }
.linescore input { color: var(--rb-white); font-size: 16px; }
.linescore td.total { background: var(--rb-red); }
.linescore td.total input { color: var(--rb-white); }
.linescore td.ls-active { background: var(--rb-red) !important; box-shadow: inset 0 0 0 2px var(--rb-white); }
.ei-btn { color: var(--rb-white); border-color: rgba(255,255,255,0.5); }
.ei-btn:hover { background: var(--rb-red); border-color: var(--rb-red); }

.live-cell { border-left-color: rgba(255,255,255,0.18); }
.live-cell .live-label { color: var(--rb-white); opacity: .55; }
#ls-inning, #ls-count {
  color: var(--rb-white) !important; font-family: var(--heading) !important;
  font-size: 28px !important; font-weight: 400 !important;
}
#ls-batter { color: var(--rb-white) !important; }
.ls-out-dot { border-color: rgba(255,255,255,0.45); }
.ls-out-dot.active { background: var(--rb-red); border-color: var(--rb-red); }

/* ---------- Tabs ---------- */
.tab-bar { border-bottom-color: var(--line); }
.tab-btn { font-weight: 400; font-size: 19px; letter-spacing: .05em; }
.tab-btn.active { color: var(--rb-blue); border-bottom-color: var(--rb-red); }
.sec-btn, .qbg-tab { font-weight: 400; letter-spacing: .07em; border-color: var(--line); border-radius: 3px; }
.sec-btn:hover, .qbg-tab:hover { border-color: var(--rb-blue); color: var(--rb-blue); }
.sec-btn.active, .qbg-tab.active { background: var(--rb-red); border-color: var(--rb-red); color: var(--rb-white); }

/* ---------- Entry deck, docked at every size ---------- */
.tab-content.active .quick-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-deck-docked);
  flex-direction: column-reverse;
  background: rgba(255,255,255,0.97); backdrop-filter: blur(12px);
  border-top: 3px solid var(--rb-red); padding: 0 20px;
  box-shadow: 0 -8px 30px rgba(0,50,120,0.16);
  max-height: 78vh; overflow-y: auto;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.qb-core { padding: 10px 0; border-bottom-color: var(--line-soft); }
.qb-drawer.open { border-bottom-color: var(--line-soft); }
.qb-sep { background: var(--line-soft); }
.qb-label { font-weight: 400; letter-spacing: .18em; }

/* Red as a fill, blue as the outline, white as the ground */
.quick-btn {
  background: var(--rb-white); border-color: var(--line); color: var(--rb-blue);
  border-radius: 4px; min-height: 46px; font-size: 15px; font-weight: 400; letter-spacing: .04em;
}
.quick-btn:hover { background: var(--paper-2); border-color: var(--rb-blue); }
.quick-btn:active { background: var(--rb-red); border-color: var(--rb-red); color: var(--rb-white); }
.quick-btn.hit { background: var(--rb-red); border-color: var(--rb-red); color: var(--rb-white); }
.quick-btn.hit:hover { background: #d9202f; border-color: var(--rb-blue); }
.quick-btn.walk { background: var(--rb-blue); border-color: var(--rb-blue); color: var(--rb-white); }
.quick-btn.walk:hover { background: #00408f; }
.quick-btn.runner { color: var(--rb-blue); border-color: var(--rb-blue); }
.quick-btn.runner:hover { background: var(--blue-light); }
.quick-btn.out { color: var(--rb-blue); }
.quick-btn.undo-play, .quick-btn.pitcher-change {
  color: var(--rb-red); border-color: var(--rb-red); background: var(--rb-white);
}
.quick-btn.undo-play:hover, .quick-btn.pitcher-change:hover { background: var(--rb-red); color: var(--rb-white); }
/* The keyboard letter on each play button, at 9px. The hint takes its colour
   from the button, so there are four pairings here, not one — and F34 measured
   only navy-on-white. Remeasured at 1194x834, worst case first:

     opacity   white/red   red/white   navy/white   white/navy
       .5        2.42        2.60         2.93         4.14
       .7        3.63        3.92         5.10         6.69
       .85       4.82        5.15         7.98         9.16

   So .7 — the number F34 proposed — leaves the four pitch buttons and the four
   hit buttons still under AA. .85 is the lowest step that clears 4.5:1 on all
   four, and at 9px against a label three sizes larger the letter still reads as
   subordinate (F34). */
.quick-btn small { opacity: .85; }
.qb-more-btn { border-color: var(--line); color: var(--rb-blue); border-radius: 4px; }
.qb-more-btn.open { background: var(--rb-red); border-color: var(--rb-red); color: var(--rb-white); }

/* ---------- Grid — white cells ---------- */
.scoring-grid th, .scoring-grid td { border-color: var(--border-light); }
.scoring-grid thead th {
  background: var(--rb-blue); color: var(--rb-white);
  font-weight: 400; letter-spacing: .12em; border-bottom: 3px solid var(--rb-red);
  border-color: rgba(255,255,255,0.2); border-bottom-color: var(--rb-red);
}
.scoring-grid thead th.inn-col { font-size: 18px; color: var(--rb-white); font-weight: 400; }
.scoring-grid td.order-cell {
  background: var(--rb-blue); color: var(--rb-white); font-weight: 400; font-size: 18px;
}
.scoring-grid td.stat-cell { background: var(--paper-2); color: var(--rb-blue); font-weight: 400; }
.scoring-grid tr.pos-starter > td { border-top: 2px solid var(--rb-blue); }
.pos-starter td { background: var(--rb-white); }
.pos-sub td { background: var(--row-alt); }
.pos-sub td.player-cell input { font-style: normal; opacity: .75; }
.scoring-grid td.player-cell input { font-weight: 400; letter-spacing: .02em; }
.scoring-grid td.pos-cell select option { background: var(--rb-white); color: var(--rb-blue); }

.at-bat-cell { background: var(--rb-white); }
.at-bat-cell.selected { background: var(--select) !important; outline: 2px solid var(--rb-red); }
.at-bat-cell .play-text { color: var(--rb-blue); text-shadow: 0 0 4px #fff, 0 0 8px rgba(255,255,255,.95); }
.at-bat-cell .out-num.active { color: var(--rb-blue); }

/* The home run used to be the only cell with real colour in it and the other four
   were 7-14% tints of the two brand values, which put hit, walk and double play in
   the same navy-or-red family and left the card with one loud cell and four quiet
   ones. All five now carry their own hue at roughly three times that strength —
   see the --play-* block for why those particular alphas. */
.at-bat-cell.play-hit { background: var(--play-hit) !important; }
.at-bat-cell.play-hr  { background: var(--play-hr)  !important; }
.at-bat-cell.play-k   { background: var(--play-k)   !important; }
.at-bat-cell.play-bb  { background: var(--play-bb)  !important; }
.at-bat-cell.play-dp  { background: var(--play-dp)  !important; }
.at-bat-cell.play-go  { background: rgba(0,50,120,0.04) !important; }

/* A strike mark is 6px, and it is the smallest thing on the card that has to
   survive a play colour — so it, not the play text, is what sets how bold those
   colours can be. #ae0f18 (the brand red taken down ~7%) held 5.12:1 on the old
   4-14% tints but only 3.06:1 on the strengthened ones, which would have capped
   every hue at about half its current value. #8f0c14 is the same red darkened
   again and buys that strength back: 4.50:1 on the green hit, 4.53:1 on the red
   strikeout, 4.50:1 on the navy walk, 4.54:1 on the purple double play, 7.04:1 on
   the gold homer, 7.78:1 on a .play-go cell. Still unmistakably red against the
   foul's navy, which is the distinction that matters at this size. Local to the
   mark on purpose — --rb-red itself stays the brand value, since everywhere else
   it paints (buttons, the masthead rule) it is a large solid or a background, and
   darkening it there would dull the card. */
.pitch-mark.strike { color: #8f0c14; }
/* 0.5 measured 2.65:1, and a foul mark is scoring data read at 6-7px in daylight,
   not decoration. Same story as the strike above: a play colour is now the hard
   ground rather than the paper, and 0.78 fell to 4.30:1 on the strongest of them.
   0.88 clears every one — 4.77:1 on the green hit, 4.85:1 on the red strikeout,
   4.72:1 on the navy walk, 4.76:1 on the purple double play, 7.01:1 on the gold
   homer. Deliberately short of the full navy a ball gets, because a foul reading
   lighter than a ball is the only colour cue separating them; the glyphs carry the
   rest — the ball is a ● and the foul an ✕. */
.pitch-mark.foul { color: rgba(0,50,120,0.88); }
.pitch-mark.ball { color: var(--rb-blue); }
.rbi-dot-mark { background: var(--rb-red); }

.diamond-svg .base-line { stroke: rgba(0,50,120,0.25); }
.diamond-svg .base-dot { fill: rgba(0,50,120,0.25); }
.diamond-svg .home-dot { fill: rgba(0,50,120,0.25); }
.diamond-svg .seg.reached .base-line { stroke: var(--rb-blue); }
.diamond-svg .seg.reached .base-dot { fill: var(--rb-blue); }
.diamond-svg .seg.reached ~ .home-dot { fill: var(--rb-blue); }
.diamond-svg.scored .base-line { stroke: var(--rb-blue); }
.diamond-svg.scored .diamond-fill { fill: var(--rb-blue); opacity: .9; }
.diamond-svg .out-on-path, .diamond-svg .out-on-cross { stroke: var(--rb-blue); }
.diamond-svg .adv-label { fill: var(--rb-blue); }
/* Was `color: var(--rb-white)`, and that was only ever right on the home run. The
   rule reads as "text sitting on a scored diamond's navy fill needs white ink", but
   it cannot be doing that: the centred text is `.play-text.play-out`, which
   renderPlay adds only for an out play, and a batter who is out never touches all
   four bases — so .scored and .play-out cannot co-occur. What it actually catches
   is every time `.play-on` (bottom-right, clear of the diamond) sits on the cell's
   own ground, where white is wrong. On the old 7-14% tints that was white on
   near-white: every single or walk that later scored had an invisible label, and
   nobody saw it because the homer — the one solid cell — was the only place the
   rule looked correct. Navy on the strengthened tints: 5.81:1 on the green hit
   through 9.09:1 on the gold homer. The glow has to be restated rather than just
   dropped: the base-block copy of this same selector sets `text-shadow: none` and
   outweighs `.play-text.play-on` four classes to two, so leaving it out here would
   single these labels out as the only unglowed ones on the card. */
.diamond-wrap:has(.diamond-svg.scored) ~ .play-text {
  color: var(--rb-blue); text-shadow: 0 0 3px #fff;
}

/* The UE glyph carries `fill="var(--accent)"` as a presentation attribute, so this
   rule outweighs it. It renders only on a runner who *scored* (the
   `scored && reachedOnError` test in renderDiamond), and .scored fills the diamond
   navy, so the glyph is always on navy and never on the cell ground — where its own
   red measures 1.93:1 and has never been legible. White is 12.16:1. Unrelated to
   the tints above; found while checking them and fixed here because it is one line
   and the same misreading of what the ink sits on. */
.diamond-svg.scored .ue-mark { fill: var(--rb-white); }

/* ---------- Pitchers, notes, modals ---------- */
.pitcher-grid th {
  background: var(--rb-blue); color: var(--rb-white); font-weight: 400;
  border-bottom: 3px solid var(--rb-red); border-color: rgba(255,255,255,0.2); border-bottom-color: var(--rb-red);
}
.pitcher-grid td.p-era, .pitcher-grid td.p-bs { background: var(--paper-2); }
.pitcher-grid td.p-stat.er-review { background: rgba(192,17,31,0.14); }
.pitcher-grid td.p-stat.er-review::after { border-right-color: var(--rb-red); }
.notes-box textarea, .spray-mini-svg { border-color: var(--line); }

.modal-box, .hotkey-modal-inner, .game-summary-inner, .spray-popup { border-color: var(--rb-blue); }
.modal-box h2, .hotkey-modal h2 { border-bottom-color: var(--rb-red); font-weight: 400; }
.hk-table td:first-child { background: var(--rb-blue); color: var(--rb-white); }
.modal-box .game-list li button.load-btn { color: var(--rb-blue); border-color: var(--rb-blue); }
.modal-box .game-list li button.del-btn { color: var(--rb-red); border-color: var(--rb-red); }
.modal-close:hover { background: var(--paper-2); border-color: var(--rb-blue); }
.gs-header { border-bottom-color: var(--rb-red); }
.gs-score-banner { background: var(--rb-blue); }
.gs-team-score .gs-team-name, .gs-vs, .gs-final-tag { color: var(--rb-white); opacity: .65; }
.gs-team-score .gs-score-num { color: var(--rb-white); }
.gs-plays span { background: var(--paper-2); }
.spray-popup #spray-marker { fill: var(--rb-red); }

/* The cell ladder used to be restated here, because the theme's own `:root` sat
   after the base breakpoints and an unconditional --cell-w/--cell-h would
   otherwise win at every width. There is one `:root` now and one ladder, both
   at the top of the file (F30) — nothing to restate. */

/* Same trap for the masthead: the theme takes the title from 25px to 30px
   unconditionally, which overrode the base's step down to 18px and — the
   title being `white-space: nowrap` — pushed the page sideways on a small
   phone. Restate that step too, scaled to 30. */
@media (max-width: 1024px) { .header-bar h1 { font-size: 22px; } }

/* The entry deck is fixed at every size now, and .app already reserves the
   140px it occupies — so the base's mobile body padding would double up. */
@media (max-width: 834px) { body { padding-bottom: 0; } }

/* ---------- I12: tablet portrait, the one size with room on both axes -------

   834x1194 is the only viewport where the linescore is not fighting anything.
   The caption sits on its own line rather than beside the table, so the table
   has the full 758px column and wants 457 of it — 301px genuinely spare, which
   is not true at any other size (at 1194x834 the live readout is flush against
   it, and at 390x844 the +EI caption takes the 52px that looks free). And the
   card ends well above the deck, so height is free too: measured on a fitted
   card, the editable rows go to the full 40px standard with all nine batting
   rows still on screen.

   So this is the one place the linescore reaches the figure the rest of the app
   holds. 25x26 -> 32x39.

   The head row stays where it is — a label, not a target, which is the same
   call F27 made in the landscape block. The 32px width is capped by the
   extra-innings case rather than by the spare: nineteen data columns plus the
   name column have to fit when +EI is pressed, and (758 - 118) / 19 = 33.7. */
@media (min-width: 561px) and (max-width: 834px) {
  .linescore th, .linescore td { width: 32px; }
  .linescore td { height: 40px; }
}

/* The theme sizes the scoreboard up (15px team names and figures), which is
   what overran the phone in the first place. Take it back down inside the
   stacked strip — this has to live here, after the theme's own linescore
   rules, or those would win over the base breakpoint above. */
@media (max-width: 560px) {
  .linescore-wrap { padding: 10px 12px; }
  .linescore th, .linescore td { height: 24px; }
  .linescore td.team-col { font-size: 12px; letter-spacing: .01em; }
  .linescore input { font-size: 12px; }
}


/* ============================================================
   ONE-PAGE FIT — the whole card on an iPad in landscape with no
   scrolling. Keep this block last; it is layout only, and it
   overrides the theme's spacing rather than its colours.

   The budget on an 11" iPad (1194x834) is: scoreboard strip ~100,
   the merged control row 40, the docked entry deck ~70, and the
   rest is the batting grid. Row height is not guessed from the
   viewport *width* any more — ui.js measures the leftover box and
   writes --cell-h, so the same rules fit Safari-with-chrome and
   the home-screen PWA. See fitGrid() in ui.js.
   ============================================================ */

/* Reserve exactly what the docked deck occupies, no more. The old
   flat 140px was double a 70px deck. --deck-h is measured. */
.app { padding-bottom: calc(var(--deck-h, 78px) + 14px + env(safe-area-inset-bottom, 0px)); }

/* The details panel is a dropdown off the masthead now, not a row of
   its own. The inputs stay in the document at all times either way —
   app.js's collectState() scrapes them straight from the DOM. */
.game-info-card { position: relative; }
details.info-details > summary { padding: 8px 14px; min-height: 40px; justify-content: center;
  border: 1px solid rgba(255,255,255,0.32); border-radius: 3px; color: var(--rb-white);
  letter-spacing: .06em; }
details.info-details > summary:hover { background: rgba(255,255,255,0.16); border-color: var(--rb-white); color: var(--rb-white); }
.info-body {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: var(--z-menu);
  width: max-content; max-width: min(680px, calc(100vw - 32px));
  background: var(--paper, #fff); color: var(--rb-blue, #003278);
  border: 1px solid var(--line, rgba(0,0,0,0.2)); border-top: 3px solid var(--rb-red, #c0111f);
  border-radius: 3px; box-shadow: 0 12px 34px rgba(0,50,120,0.22);
  padding: 14px 18px; margin: 0;
}

/* Summary / Games / New Game move behind one button so the linescore
   and the live readout get the width they need on one line. */
.hdr-menu { position: relative; }
.hdr-menu-panel {
  display: none; position: absolute; top: calc(100% + 8px); right: 0; z-index: var(--z-menu);
  min-width: 190px; padding: 6px;
  background: var(--paper, #fff); border: 1px solid var(--line, rgba(0,0,0,0.2));
  border-top: 3px solid var(--rb-red, #c0111f); border-radius: 3px;
  box-shadow: 0 12px 34px rgba(0,50,120,0.22);
}
.hdr-menu.open .hdr-menu-panel { display: block; }
.hdr-menu-panel button {
  display: block; width: 100%; text-align: left;
  padding: 10px 12px; min-height: 42px; border: none; border-radius: 2px;
  background: transparent; color: var(--rb-blue, #003278); cursor: pointer;
  font-family: var(--heading); font-size: 13px; font-weight: 600; letter-spacing: .06em;
}
.hdr-menu-panel button:hover { background: var(--paper-2, rgba(0,50,120,0.07)); }
.hdr-menu-panel button.danger { color: var(--rb-red, #c0111f); }

/* A menu item that reports a state rather than doing a thing (I1).

   The tick trails the label rather than leading it. Led, it needs a reserved
   column so the label does not jump when it appears — and that column indents
   this one item out of line with Summary, Games and Shortcuts, which is a
   worse thing to look at than the problem it solves. Trailing, nothing shifts
   and nothing else in the menu has to move.

   The checked row also goes navy-on-tint, so the state is not carried by one
   small mark: same reasoning as the popup chips under H4, and `aria-pressed`
   says it a third way for anything not looking. */
.hdr-menu-panel button.menu-check {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.hdr-menu-panel button.menu-check::after {
  content: '✓'; flex: 0 0 auto;
  font-weight: 700; color: var(--rb-red, #c0111f); visibility: hidden;
}
.hdr-menu-panel button.menu-check[aria-pressed="true"]::after { visibility: visible; }
.hdr-menu-panel button.menu-check[aria-pressed="true"] { background: var(--blue-light); }

/* The team switch lives in the section bar now — one control row
   instead of two. Only the active tab's copy is ever on screen. */
.sec-bar > .tab-bar { border: none; gap: 4px; padding-right: 12px; margin-right: 6px;
  border-right: 1px solid var(--line, rgba(0,0,0,0.16)); }
.sec-bar .tab-btn { min-height: 34px; padding: 5px 12px; font-size: 15px; border-bottom-width: 2px; }

/* Kept mutually exclusive with the phone-landscape block that closes the file
   (F28). This was `(min-width: 835px)` alone, and at 844x390 — a Pro Max in
   landscape, the exact device that block was written for — both queries
   matched. The phone block is later so it won where the two collided, but only
   where they collided: every rule here that it did not restate stayed in force
   on a 390px-tall window, including a sticky `.deck-strip` and a caption set to
   `font-size: 0`. Neither was written with 390px of height in mind.

   `min-height: 501px` is the exact complement, not an approximation of one: the
   phone block asks for `max-height: 500px`, and anything 835px wide and under
   500px tall is landscape by arithmetic, so no third case falls between them.
   Written as a height floor rather than `not (...)`, which needs Safari 16.4
   and buys nothing here. */
@media (min-width: 835px) and (min-height: 501px) {
  /* --- The strip: masthead, linescore and live readout on one deck --- */
  .deck-strip {
    position: sticky; top: 0; z-index: var(--z-deck);
    margin: 0 -20px; padding: 0 20px 8px;
    background: var(--paper, #fff);
  }
  .header-bar { padding: 8px 20px; gap: 14px; }
  .masthead .kicker { font-size: 9px; margin-bottom: 1px; }
  .header-bar h1 { font-size: 19px; }
  .header-actions { gap: 6px; }
  .header-actions button, .game-timer button, details.info-details > summary {
    padding: 7px 12px; min-height: 36px; font-size: 12px;
  }
  .game-timer .timer-display { min-width: 46px; font-size: 13px; }

  .linescore-wrap {
    display: flex; align-items: stretch; gap: 14px;
    padding: 8px 14px; margin: 8px 0 0;
  }
  .ls-table-col { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
  /* "Score by Innings" is what a linescore looks like — the caption is
     pure height. Only the +EI control survives, floated alongside. */
  .linescore-wrap h3 { margin: 0; font-size: 0; letter-spacing: 0; }
  /* On the deck the chip can just be the right size. The strip's height is
     the live readout's (80.7px at 1194x834, and the linescore column is
     stretched to it), so anything up to that is free — a 44x40 box, the
     .quick-btn standard, costs the strip nothing and the table 8px of the
     width it was not using. No overlay here: the caption is font-size: 0,
     and reaching below the chip would reach into the table itself (the
     overlay is scoped to the 834px block for that reason). */
  .linescore-wrap h3 .ei-btn {
    font-size: 11px; letter-spacing: .06em; padding: 4px 7px;
    min-width: 44px; min-height: 40px;
  }
  /* The R/H/E and inning cells are edited with a fingertip and were 25x21 —
     the smallest editable control in the app, and it got smaller as the screen
     did. They cannot be widened: fifteen columns share the row with the live
     readout, and the fit is the feature. But there is height going spare and
     nobody was using it. `.ls-table-col` is stretched to the readout's 81px and
     the table only wants 67, so 14px inside this column belongs to nothing.
     Spend it on the two editable rows and leave the head row alone — it is a
     label, not a target. 22px -> 29px, and the table lands at 80px inside the
     81px it already had, so the strip does not grow by a pixel and neither does
     anything below it. (F27) */
  /* I12, taking the last of it. F27 spent the 14px that was going spare inside
     `.ls-table-col` and stopped there, correctly — at the time the next pixel
     came off the batting order. Re-measured on a fitted card with the drawer
     flat: 29 -> 32 still shows all nine rows at 1194x834, and 36 is where the
     ninth falls off. So three more pixels are there and the fourth is not.
     1024x768 would take 34; one rule covers both sizes and 32 is the number
     that is safe at both. Width is still untouchable here for F27's reason —
     the live readout is flush against the table. */
  .linescore th, .linescore td { width: 26px; }
  .linescore th { height: 22px; }
  .linescore td { height: 32px; }
  .linescore th.team-col, .linescore td.team-col { width: 92px; }
  .linescore td.team-col { font-size: 13px; }
  /* Every pixel the name column takes here comes off the live readout beside
     it, which is already scrolling below 1024. On an 11" iPad landscape the
     deck can afford the full 150 and the readout still fits whole; at the
     835 edge it cannot, and a name shrunk a size is the cheaper loss. */
  .linescore td.team-col > span { max-width: min(150px, 13vw); }
  .linescore input { font-size: 16px; }

  #live-stats { flex: 1; display: flex; align-items: center; min-width: 0; }
  .live-cell { padding: 0 11px; gap: 2px; }
  .live-cell .live-label { font-size: 9px; letter-spacing: .11em; }
  #ls-inning, #ls-count { font-size: 22px !important; }
  .live-cell svg { width: 34px; height: 34px; }
  .live-cell.live-matchup { min-width: 0; flex: 1; }
  .live-matchup .ls-name { font-size: 14px; }
  .live-matchup .ls-name-sm { font-size: 13px; }
  .live-matchup .ls-label-2 { margin-top: 2px; }
  /* Hold the pitch-count line open. It is empty until the first pitch, and
     letting it appear grew the strip 15px mid-at-bat — which moved the whole
     grid under the scorer's finger, and cost a row height to claw back. */
  .live-matchup .ls-sub { min-height: 14px; }

  /* --- The control row --- */
  .tab-content.active { padding-top: 0; }
  .sec-bar { margin: 6px 0 4px; flex-wrap: nowrap; }
  .sec-btn { min-height: 32px; padding: 4px 12px; }
  .ghost-btn { min-height: 32px; padding: 5px 12px; font-size: 12px; }

  /* --- The grid --- */
  /* Leave the head row's `top: 0` alone. `.grid-wrap` is an overflow-x
     scroll container, so the sticky head resolves against *that* box,
     not the viewport — any other `top` just pushes the head row down
     into the middle of the batting order. */
  .scoring-grid thead th { padding: 5px 2px; }
  .grid-wrap { padding-bottom: 0; }

  /* The deck is a fixed overlay; it needs no bottom margin in flow. */
  .quick-bar { margin-bottom: 0; }
  .qb-core { padding: 8px 0; }
}

/* ---------- F9 — at iPad-landscape width the drawer stops being a drawer ----

   The More-plays drawer expanded *over* the card and `fit()` deliberately did not
   reserve it ("it must not push 400px of padding in"), so at 1194x834 with the
   drawer open the grid ran to 753 and the deck began at 657: batting slots 8 and 9
   sat behind it, and `pageScroll` was 0, so they could not be brought into view.
   That bites exactly when it matters — SB, CS, WP, SUB and PR all live in there,
   and the bottom of the order comes up as often as any other part of it.

   Wide enough and there is no need for a drawer at all: all 33 buttons lay out in
   two rows at 1194px. The accordion goes, and with it two other annoyances — only
   one group reachable at a time, and the group resetting to Outs on every
   half-inning change, because each tab owns its own copy of the drawer.

   `display: contents` on the panels drops them out of the box tree so their buttons
   wrap as one set, rather than each panel wrapping as an unbreakable block (which
   put them in three ragged rows). The cost is real and was measured before it was
   taken: the deck goes 63px to 171px, and the batting rows 57px to 44px.

   That 171 read 164 here until F44. Nothing had grown — the note counted the two
   46px button rows and the 6px between them (98px) plus the core's 63 and the
   deck's 3px rule, and left out the drawer's own padding and bottom rule, which
   at the time was another 13px. F44 is what that 13px cost: 9 rows at the 44px
   floor plus this deck came to 836px on an 834px screen, so the page scrolled by
   2px at the one viewport the whole layout is built around. See the padding
   below. */
@media (min-width: 1100px) {
  .tab-content.active .qb-drawer {
    max-height: none; opacity: 1; overflow: visible;
    /* 4/2 rather than the 8/4 the opening drawer uses below this width, and the
       6px is not cosmetic — it is the fix for F44. Laid flat there is nothing
       being revealed, so this padding is not framing a panel that just appeared;
       it is dead space inside a bar that is always on screen, and the fit had
       nowhere else to come from. 1194x834, resting state:

                        --deck-h   --cell-h   page scroll
         8px 0 4px        177px      44px         2px
         4px 0 2px        171px      44px         0

       The card does not move — the rows were already pinned to their 44px floor
       and stay there; the deck's top edge drops 6px, so the gutter between the
       last row and the deck goes 12px to 18px. Grow this back and the 2px comes
       back with it; a case in run-tests.js asserts the 6px total. */
    padding: 4px 0 2px; border-bottom: 1px solid var(--line-soft);
  }
  .qb-drawer .qbg-tabs { display: none; }
  .qb-drawer .qbg-panel { display: contents; }

  /* Flat is the resting state, so the button folds the two rows *away* rather than
     revealing them — the inverse of what `.open` means below this width, which is why
     it keys off its own class. Worth having: `fit()` measures the whole `.quick-bar`
     up here, so collapsing hands its ~114px straight back to the batting rows. The
     button is no longer hidden; there is something to close. (F21) */
  .tab-content.active .qb-drawer.collapsed {
    max-height: 0; opacity: 0; overflow: hidden; padding: 0; border-bottom: none;
  }
}

/* When the card is taller than the space it has, the *grid* scrolls and the page
   does not. `--grid-max-h` is set by `fit()`, which is the only thing that knows
   where the box's bottom edge lands, and removed again the moment the card fits —
   so at rest this rule resolves to `none` and changes nothing.

   Two cases reach it. The drawer opening over the card is the older one (F9-A): its
   height is not reserved, so the rows it covers need a way to be got at. A shown sub
   row is the other, and it is why this stopped being gated on `body.drawer-open`.
   Sub rows have real height now, and at 1194x834 the starters are already on their
   44px floor with nothing left to give — so ~22px per open sub has to go somewhere,
   and before this it went into scrolling the whole page. That takes the scoreboard,
   the inning numbers and the count off the top of the screen: three things the
   scorer is reading *while* he needs the row he scrolled to. Scrolling the grid
   under its own sticky head row costs him none of them.

   This is what the phone block already does full-time, for the same reason. */
.tab-content.active .grid-wrap {
  max-height: var(--grid-max-h, none);
  overflow-y: auto;
}


/* ============================================================
   PHONE — keep this block last. Everything above it is the iPad
   card and none of it changes.

   A 9-batter x 9-inning card does not fit a phone in either
   orientation, and the two orientations fail in opposite
   directions, so they get opposite answers rather than one
   compromise that suits neither:

     portrait  440x956   the height is there, the width is not.
                         All nine batting slots on screen — even
                         with the deck up — and the innings scroll
                         sideways under a frozen name block.

     landscape 956x440   the width is there, the height is not.
                         All nine innings on screen at once; the
                         batting slots scroll. Nine rows plus a head
                         row is 396px and landscape has 419px of
                         usable height in total, so no arrangement
                         of the chrome fits the card here. Stop
                         trying, and make the scroll a good one.

   In both, the *page* never scrolls — the grid takes its own scroll
   box (--grid-max-h, measured by fit() in ui.js) so the scoreboard,
   the inning headers and the entry deck never move under the
   scorer's finger. That mechanism already existed for the
   drawer-open case at F9-A; the phone just uses it always.

   The discriminator for landscape is *height*, not width. At 956px
   wide a phone in landscape was matching the `min-width: 835px`
   iPad block, which budgets its whole layout against 834px of
   height and was handed 440. No iPad is under 768px tall, so 500px
   keeps them apart.

   The 740px floor is the other half of that. Landscape only earns
   its own layout on a phone wide enough to hold the strip and the
   deck on one row each — a Pro Max is 956 and a 15/16 Pro is 852 to
   874, but an SE is 667, and at that width the one-row strip, the
   section bar and the deck all wrap and the batting order is left
   with the head row and nothing else. Below 740 it falls through to
   the stacked `max-width: 834px` layout, which is what it had before
   any of this and which at least scrolls honestly. A short window
   narrower still belongs to the portrait block.
   ============================================================ */

/* The four `--sa-*` insets were declared here, a third `:root` 1400 lines below
   the first two. They are in the single block at the top now (F30), minus
   `--sa-b`, which nothing ever read. */

/* `viewport-fit=cover` plus a black-translucent status bar hands the app the
   whole screen, insets included. Only the bottom inset was ever honoured, so on
   an installed iPhone the masthead's top 59px sat under the clock. The navy bar
   keeps painting into the inset — it is the bar's content that moves down.
   Zero wherever there is no inset, so both of these are safe at every width.

   The second one carries the same height floor as the block above for the same
   reason (F28), so that no `min-width: 835px` in this file means "iPad" in one
   place and "iPad or a phone on its side" in another. It changes nothing on its
   own — the phone-landscape block sets `.header-bar`'s padding as a shorthand
   and is later, so it already won here — but a rule that is right by ordering
   alone is one edit away from being wrong. */
.header-bar { padding-top: calc(14px + var(--sa-t)); }
@media (min-width: 835px) and (min-height: 501px) { .header-bar { padding-top: calc(8px + var(--sa-t)); } }

/* The deck handle is phone-only. On an iPad the resting deck is 63-109px and
   folding it would buy a row and a half; at >=1100px there is already a fold,
   for the flat drawer, and it is the More-plays button that does it. */
.deck-handle { display: none; }


/* ---------- Phone, both orientations ---------- */
@media (max-width: 560px),
       (orientation: landscape) and (max-height: 500px) and (min-width: 740px) {

  /* --- The grid --- */
  /* AVG / AB / H / R / RBI / BB are 145px and not one of them is written to
     during an at-bat — they are a read-out of what the card already says. They
     stay in the DOM (app.js writes every `st-*` cell by id on every play, and
     the game summary reads the same figures); it is only the columns that are
     gone. Hiding them is what makes the frozen block below affordable: freezing
     all ten left-hand columns would be 315px of a 440px screen, and the card
     would be 1.4 innings wide. */
  .scoring-grid thead th.avg-col,  .scoring-grid td.avg-cell,
  .scoring-grid thead th.stat-col, .scoring-grid td.stat-cell { display: none; }

  /* One set of numbers for the frozen block, because the `left` offsets below
     have to add up to exactly the widths above them.

     `width: max-content` is load-bearing, not tidiness. The table is `width:
     auto`, and an over-constrained fixed-layout table compresses its columns to
     the container instead of overflowing it — so the name column resolved to
     60px while the `left` offsets below still pointed at 92px of it, and the
     four frozen columns sat with gaps between them that the scrolling at-bat
     cells showed straight through. Holding the intrinsic width is the same fix
     the stacked linescore needs at 560px, for the same reason. */
  .scoring-grid { width: max-content; --frz-ord: 20px; --frz-num: 26px; --frz-name: 88px; --frz-pos: 34px; }
  /* Both the head cell and the body cells, unlike everywhere else in this
     stylesheet. `table-layout: fixed` ignores a body cell's width and resolves
     the columns from the head row — but `width: max-content` above computes the
     table's own width from every row first, so a body cell left at its
     170px/128px default drags the column back out with it. */
  .scoring-grid thead th.ord-col,  .scoring-grid td.order-cell  { width: var(--frz-ord); }
  .scoring-grid thead th.num-col,  .scoring-grid td.num-cell    { width: var(--frz-num); }
  .scoring-grid thead th.name-col, .scoring-grid td.player-cell { width: var(--frz-name); }
  .scoring-grid thead th.pos-col,  .scoring-grid td.pos-cell    { width: var(--frz-pos); }
  /* The head row's 0.1em tracking is what clipped POS in a 34px column — the
     letters fit, the space after the last one did not. */
  .scoring-grid thead th { padding: 4px 1px; letter-spacing: .02em; }

  /* Nothing in this table was ever sticky *horizontally* — all 25 sticky cells
     are `top: 0, left: auto` — so swiping out to inning 6 took the batting
     order off the screen and left the scorer tapping a cell for a name they
     could no longer read. Freeze the slot number, the jersey, the name and the
     position; the innings scroll under them.

     In both orientations, not just the portrait one that scrolls by default:
     nine innings fit across a landscape phone but fifteen do not, and an
     extra-innings card is exactly when the bottom of the order matters. */
  .scoring-grid th.ord-col,  .scoring-grid td.order-cell  { position: sticky; left: 0; z-index: var(--z-frozen-col); }
  .scoring-grid th.num-col,  .scoring-grid td.num-cell    { position: sticky; z-index: var(--z-frozen-col);
    left: var(--frz-ord); }
  .scoring-grid th.name-col, .scoring-grid td.player-cell { position: sticky; z-index: var(--z-frozen-col);
    left: calc(var(--frz-ord) + var(--frz-num)); }
  .scoring-grid th.pos-col,  .scoring-grid td.pos-cell    { position: sticky; z-index: var(--z-frozen-col);
    left: calc(var(--frz-ord) + var(--frz-num) + var(--frz-name)); }
  /* thead is already sticky vertically at --z-grid-sticky. These four are
     sticky on both axes and have to beat their own row as well as the inning
     cells sliding underneath them. */
  .scoring-grid thead th.ord-col, .scoring-grid thead th.num-col,
  .scoring-grid thead th.name-col, .scoring-grid thead th.pos-col { z-index: var(--z-frozen-head); }
  /* `border-collapse: collapse` gives a cell's right-hand border to its
     neighbour, so the frozen block's trailing edge belongs to the inning column
     that is scrolling away underneath it and cannot be drawn there. A shadow on
     the last frozen column instead — it sits outside the border box, so it
     costs the table no width. */
  .scoring-grid th.pos-col, .scoring-grid td.pos-cell { box-shadow: 2px 0 0 var(--text); }

  /* And the frozen block needs a ground of its own to sit on.

     A table cell's background is painted in the *table's* background layer
     (CSS 2.1 §17.5.1), not inside the cell's own stacking context — so
     `--z-frozen-col` lifts a frozen cell's content above the innings sliding under
     it but leaves its background down in that shared layer, where the at-bat
     cells, later in the row, paint over it. Normally invisible, because both are
     opaque white. Not invisible over a scored cell: the five play tints are
     deliberately alpha rather than solid fills, so scrolling out to inning 12
     dragged a 40%-green single and a 40%-gold home run right through the batting
     order's names.

     A pseudo-element is content, so it lands inside the cell's --z-frozen-col
     stacking context, above the tint; `z-index: -1` keeps it behind the cell's
     own input. `background: inherit` so the ground stays whatever the cascade
     already decided for that cell — navy for the order stripe, white for a
     starter, --row-alt for a sub — rather than three more hard-coded colours
     for the theme layer to drift away from. */
  .scoring-grid td.order-cell::before, .scoring-grid td.num-cell::before,
  .scoring-grid td.player-cell::before, .scoring-grid td.pos-cell::before {
    content: ''; position: absolute; inset: 0; z-index: -1; background: inherit;
  }
  /* The block's own dividers are borders, so they are down in that same table
     layer and wash out under a tint — where the 2px navy slot rules, being dark
     enough to survive 40% of anything, do not. Redrawn on the lifted ground as
     an inset shadow, which draws inside the padding box and so costs no width.
     Only the two boundaries inside the block: the order stripe is navy against
     white and needs no line, and the block's trailing edge is the 2px shadow
     above. */
  .scoring-grid td.num-cell::before,
  .scoring-grid td.player-cell::before { box-shadow: inset -1px 0 0 var(--border-light); }

  /* The page does not scroll on a phone; the grid does. --grid-max-h is
     measured by fit(), the only thing that knows where the deck's top edge
     lands. */
  .tab-content.active .grid-wrap {
    max-height: var(--grid-max-h, none); overflow-y: auto;
  }
  /* --grid-max-h already stops the grid clear of the deck, so a bottom padding
     on .app would reserve the deck twice over and put the page 1-3px into
     scroll — which on a touch screen is as annoying as 300px. The other two
     panels are not height-capped and do still need the clearance. */
  .app { padding-bottom: 0; }
  .sec-panel:not([data-panel="bat"]) { padding-bottom: calc(var(--deck-h, 78px) + 14px); }

  /* --- The foldable deck --- */
  .deck-handle {
    display: block; position: relative; width: 100%; border: none; background: transparent;
    cursor: pointer; line-height: 1;
    font-family: var(--heading); font-size: 10px; font-weight: 600;
    letter-spacing: .16em; text-transform: uppercase; color: var(--text-light);
  }
  .deck-handle::before {
    content: ''; display: block; height: 4px; border-radius: 2px;
    width: 42px; margin: 0 auto 5px; background: var(--line, var(--rule));
  }
  /* 350x32 in portrait and 789x26 on its side — wide enough to hit blind,
     short of 44 either way. It cannot grow: every pixel it takes is a pixel of
     the batting order, which is the whole reason the deck folds. But it is
     bracketed by dead space on both sides — `fit()` leaves an 18px pad between
     the grid's last row and this handle, and `.qb-core` opens with 6-8px of
     padding before the first play button — so the target can take 6px in each
     direction without touching a control. 32 -> 44 in portrait, 26 -> 38 on its
     side, and the painted bar does not move a pixel. Same trick as the +EI chip
     in the 834px block: grow the target, not the thing. (F27) */
  .deck-handle::after { content: ''; position: absolute; inset: -6px 0; }
  .deck-handle:active::before { background: var(--accent); }
  /* Folded by max-height rather than `display: none`, so the fold animates and
     — the part that matters — fires a transitionend for fit() to re-measure
     against. Measuring mid-animation and baking in the stale height is the F21
     mistake; ui.js already listens for it on the drawer. */
  .tab-content.active .qb-core {
    max-height: 70vh;
    transition: max-height .2s ease, opacity .16s ease, padding .2s ease;
  }
  body.deck-folded .tab-content.active .qb-core,
  body.deck-folded .tab-content.active .qb-drawer {
    max-height: 0; opacity: 0; overflow: hidden; padding: 0; border-bottom: none;
  }

  /* --- Chrome common to both --- */
  .masthead .kicker { display: none; }
  /* The Details panel hangs `right: 0` off its own summary button. On an iPad
     that button is at the right end of a wide masthead, so the panel opens
     leftward into the bar and there is room. On a phone it is not: in portrait
     the button is mid-row, its right edge 305px in from the left, and a panel
     408px wide (its own `max-width: calc(100vw - 32px)`) ran from -103px with
     every field label off the left edge of the screen. Landscape was the same
     bug 2px deep — 680px of panel anchored 678px in.

     Which used to be survivable and is not any more: the page does not scroll on
     a phone now, so anything outside the viewport is unreachable rather than
     merely off the fold.

     So the panel is anchored to the masthead rather than to the button.
     `.game-info-card` stops being the containing block and `.header-bar` becomes
     it, which is the right box on both counts: it spans the whole screen (its
     negative margins bleed it past .app's gutters), so the horizontal rules per
     orientation below can hold the panel inside the safe area without knowing
     where the button is; and `top: 100%` is its own height, so the panel sits
     under the bar at whatever height it settles — 45px in landscape, 74px in
     portrait, ~110px if the actions wrap. */
  .header-bar { position: relative; }
  .game-info-card { position: static; }
  .info-body { top: calc(100% + 6px); }

  /* Fixed content — eight fields and four umpires, ~298px — so this never bites
     today. It is here because the panel is the one piece of chrome whose height
     is not budgeted anywhere, and the page no longer scrolls to rescue it. `vh`
     first for the rare engine without `dvh`. */
  .info-body { overflow-y: auto; -webkit-overflow-scrolling: touch; }
  .info-body { max-height: calc(100vh - 130px); }
  .info-body { max-height: calc(100dvh - 130px); }
  .live-cell .live-label { font-size: 8px; letter-spacing: .08em; }
  .ls-out-dot { width: 9px; height: 9px; }

  /* --- The readout: four things, not six --- */
  /* On a phone the strip is asked for the count, who is at bat, who is pitching
     and what he has thrown — and nothing else. The two cells that go are the two
     the card itself already answers: the inning is the highlighted column in the
     linescore directly above (and the arrow beside the team names), and the
     runners are drawn in the at-bat cells. Outs are not written anywhere else, so
     the dots stay.

     Both cells stay in the DOM — app.js writes `ls-inning` and the three bases on
     every repaint, `renderFinalReadout` puts FINAL in the inning cell, and the
     tests read all of it. It is only the columns that are gone, the same trade
     the six stat columns make below. */
  .live-inning, .live-bases { display: none; }
  /* Except the one thing the inning cell says that nothing else does. See
     markFinalReadout in app.js. */
  .linescore-wrap.is-final .live-inning { display: flex; }
  /* The width those two were taking goes to the names, which are what fitName
     shrinks when the cell is short. */
  .live-cell.live-matchup { flex: 1 1 auto; min-width: 0; }
  /* Same trade the iPad deck makes at 835px: "Score by Innings" is what a
     linescore looks like, and the caption is 28px of pure height.

     Dropping the words is not enough on its own, though — +EI is not the rare
     control it looks like. updateInningVisibility() shows it whenever the game
     is under fifteen innings, so on every regulation card it is on screen for
     the whole game, and a caption of `font-size: 0` still left it a 31px line
     of its own. It goes *beside* the table instead, which is what the 835px
     deck does with it. */
  .ls-table-col { display: flex; align-items: center; gap: 8px; }
  .linescore-wrap h3 { font-size: 0; letter-spacing: 0; margin: 0; flex: 0 0 auto; }
  /* Beside the table the chip can just be the right size, so the hit-area
     overlay the 834px block hangs 20px *below* it goes with the caption. That
     overlay existed because the chip shared the caption's line box and had
     nowhere to grow into; here, 20px below it is the linescore's own rows. */
  .linescore-wrap h3 .ei-btn {
    font-size: 11px; letter-spacing: .06em; padding: 4px 7px;
    min-width: 44px; min-height: 40px;
  }
  .linescore-wrap h3 .ei-btn::after { content: none; }
}


/* ---------- Phone, portrait ---------- */
@media (max-width: 560px) {

  /* --- Masthead: 132px -> 74px --- */
  /* 12px gutters rather than 20: 16px of a 440px screen is a quarter of an
     inning column. */
  .app { padding-left: 12px; padding-right: 12px; }
  .header-bar {
    margin: 0 -12px; padding: 6px 12px; padding-top: calc(6px + var(--sa-t));
    gap: 8px; row-gap: 4px; align-items: center; border-bottom-width: 3px;
  }
  .header-bar h1 { font-size: 17px; letter-spacing: .03em; }
  .header-actions { gap: 5px; }
  .header-actions button, .game-timer button, details.info-details > summary {
    padding: 6px 10px; min-height: 38px; font-size: 12px; letter-spacing: .04em;
  }
  .game-timer .timer-display { min-width: 38px; font-size: 12px; }

  /* Both gutters, so the panel spans the screen — see the anchoring note in the
     shared block. There is no width to spare in portrait for it to be narrower
     and still hold twelve fields. */
  .info-body { left: 12px; right: 12px; width: auto; max-width: none; }

  /* --- Scoreboard strip: 197px -> 127px --- */
  .linescore-wrap { padding: 8px 10px; margin: 8px 0 0; }
  /* The readout is the tallest thing left in the strip and most of its height
     is label. */
  #live-stats > .live-cell { padding: 0 9px; gap: 1px; }
  #ls-inning, #ls-count { font-size: 20px !important; }
  .live-cell svg { width: 26px; height: 26px; }
  /* The 834px block strips the leading divider off `:first-child`, which is now a
     cell that isn't painted. The rule belongs to whichever cell leads the row —
     Outs while the game is live, Inning once it reads FINAL. Style and width
     only, so the theme layer keeps the colour. */
  #live-stats > .live-cell.live-outs { border-left: none; padding-left: 0; }
  .is-final #live-stats > .live-cell.live-outs {
    border-left-style: solid; border-left-width: 1px; padding-left: 9px;
  }
  /* Four stacked lines in the matchup cell set the row height for the figure
     cells beside it. Labels beside the names instead of above them: same
     information, half the height.

     Two rows and three columns — label, name, and the pitch count riding the
     pitcher's own row. The count used to be hidden here because a fifth line was
     28px the portrait card did not have; beside the name it costs no height at
     all, and there is width for it now that the inning and the diamond are gone
     (the three cells come to ~305px of a 440px screen).

     Placed explicitly, every item: auto-flow would put the second label in the
     third column of the first row. */
  .live-cell.live-matchup {
    min-width: 150px; display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 6px; align-items: baseline;
  }
  .live-matchup .live-label { grid-area: 1 / 1; }
  .live-matchup .ls-label-2 { grid-area: 2 / 1; margin-top: 0; }
  .live-matchup .ls-name { grid-area: 1 / 2; font-size: 13px; }
  .live-matchup .ls-name-sm { grid-area: 2 / 2; font-size: 12px; }
  /* Right-hand column, and held at a width the longest form of this line fits —
     the column is `auto`, so a count appearing, growing a digit, or gaining its
     balls-strikes split would otherwise take the width out of the name beside it
     and re-shrink it mid-at-bat. Same reasoning as the 835px block's min-height on
     this line, in the other axis.

     The word goes, and only here. Spelling it out costs 8 characters — 48px at this
     size — and the strip does not have them: the split at full length wanted 19ch
     against the 62px this held for "999 pitches", and overflowed a 375px strip by
     43px. What is left is "142 (52-90)", 11 characters, under a row whose own label
     two columns to the left already says Pitcher. The count is what the scorer came
     for; the word was only ever naming it.

     `ch` rather than px because the line is `--mono`, where a ch *is* the character
     cell — so 11ch is exactly the longest string this can hold, not an estimate of
     one. (62px was that same measurement made by hand.) */
  .live-matchup .ls-sub {
    display: block; grid-area: 2 / 3; font-size: 10px;
    min-width: 11ch; text-align: right;
  }
  .live-matchup .ls-sub .lsp-w { display: none; }

  /* --- Control row: 124px (three wrapped rows) -> 38px (one) --- */
  .sec-bar {
    flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
    gap: 6px; margin: 6px 0 4px; padding-bottom: 2px; scrollbar-width: none;
  }
  .sec-bar::-webkit-scrollbar { display: none; }
  .sec-bar > * { flex: 0 0 auto; }
  /* `margin-left: auto` is an inline style on the Show-sub-rows button. On a row
     that wraps it parked the button at the right edge; on a row that scrolls it
     pushes the scroll width out by whatever is left over, so the row scrolls
     past its own content. It has no job here. */
  .sec-bar > .ghost-btn[data-ui-act="subs"] { margin-left: 0 !important; }
  .sec-bar > .tab-bar { gap: 2px; padding-right: 8px; margin-right: 4px; }
  .sec-bar .tab-btn { min-height: 36px; padding: 4px 9px; font-size: 14px; }
  .sec-bar .sec-btn, .sec-bar .ghost-btn { min-height: 36px; padding: 4px 10px; font-size: 12px; }

  /* --- The grid --- */
  /* 69px -> 62px buys back most of the width the frozen block takes, so the
     card still shows 4.4 innings at a time rather than 3.6. Safe for the pitch
     marks in a way that shrinking --cell-h is not — `.pitch-mark` sizes off
     --cell-h — and the 52px diamond still clears a 62px cell. */
  :root { --cell-w: 62px; }

  /* The last 12px of the vertical budget, and where it came from matters. At
     --cell-h 48 the nine starters need 432px and the box measured 466 less a
     36px head row: 11px short, so the ninth slot hung over the edge with the
     deck up. Taken off the head row (above) and the deck's own padding rather
     than off --cell-h, because the pitch marks divide it: 48px gives a resting
     five-mark column 8.6px, and every pixel off --cell-h comes off them first.
     (It read "a seven-mark column at 6.1px" while the track wrapped at 7. The
     track is two columns now and 5 is the resting divisor, so the figure moved;
     the reason for not spending --cell-h here did not.) */
  .qb-core { padding: 7px 0; }

  .deck-handle { min-height: 32px; padding: 5px 0 4px; }
}


/* ---------- Phone, landscape ---------- */
@media (orientation: landscape) and (max-height: 500px) and (min-width: 740px) {

  /* --- What this block used to get for free (F28) ---------------------------

     Until the height floor went on the 835px block, a landscape phone matched
     that block *as well as* this one, and this block was written against the
     result without ever saying so — the note below the strip reads "stays one
     row, as it is above 835px", which is only true because the rule that makes
     it a row was arriving from the iPad. Separating the two took it away: the
     strip went from 137px to 244px, the scoreboard stopped being sticky, and
     the batting grid started 113px further down a screen that has 390.

     So the pieces it genuinely wants are restated here, where they can be read
     and where they now say a phone's numbers rather than an iPad's. This is the
     part of F28 that was the actual work; the media query was one line.

     Two of them differ from what was being inherited, and deliberately:

     - `.deck-strip`'s bleed pays back the safe-area insets, the same way
       `.header-bar` above and the docked deck below already do. The iPad's flat
       `margin: 0 -20px` under-pulled by 59px on a Pro Max, so the sticky
       scoreboard's background stopped short of the screen edge on exactly the
       device this block exists for. Invisible in a desktop window, which has no
       insets — which is how it survived being inherited.
     - The name column is 78px, not the iPad's 92px. This block narrows every
       other linescore column and holds the whole card to nine innings across;
       the cap on the name span here is `min(110px, 12vw)`, which at 844px is
       101px and was overrunning a 92px cell. 78px is what the column needs at
       these figure sizes, and the span cap now binds instead of fighting it. */
  .deck-strip {
    position: sticky; top: 0; z-index: var(--z-deck);
    margin: 0 calc(-20px - var(--sa-r)) 0 calc(-20px - var(--sa-l));
    padding: 0 calc(20px + var(--sa-r)) 8px calc(20px + var(--sa-l));
    background: var(--paper);
  }
  .masthead .kicker { font-size: 9px; margin-bottom: 1px; }
  .linescore-wrap { display: flex; align-items: stretch; }
  .ls-table-col { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
  /* "Score by Innings" is what a linescore looks like — the caption is pure
     height, and there is less of it to spend here than anywhere. Only the +EI
     control survives, floated alongside at the size the base rule gives it. */
  .linescore-wrap h3 { margin: 0; font-size: 0; letter-spacing: 0; }
  .linescore th, .linescore td { width: 26px; }
  .linescore th.team-col, .linescore td.team-col { width: 78px; }
  #live-stats { flex: 1; display: flex; align-items: center; min-width: 0; }
  /* Not `.live-cell .live-label`. The iPad sets it to 9px, but a narrower block
     further up already had it at 8px here and was winning on order, so copying
     the iPad's value down would not have been a restatement — it would have
     grown the label and taken 2.7px of the strip with it. Measured, not assumed:
     restating everything the iPad block set is the wrong instinct, and this is
     the one line where the two answers differ. */
  .live-cell.live-matchup { min-width: 0; flex: 1; }
  .tab-content.active { padding-top: 0; }
  .grid-wrap { padding-bottom: 0; }
  .quick-bar { margin-bottom: 0; }

  /* --- Masthead: 56px -> 45px, one row. There is width to spare here. --- */
  /* The side insets are 59px each on a Pro Max in landscape — the sensor
     housing on one edge, the rounded corner on the other. The masthead and the
     docked deck both bleed to the screen edge, so each pays the inset back in
     its own padding. */
  .app { padding-left: calc(20px + var(--sa-l)); padding-right: calc(20px + var(--sa-r)); }
  .header-bar {
    margin-left: calc(-20px - var(--sa-l)); margin-right: calc(-20px - var(--sa-r));
    padding: 4px calc(20px + var(--sa-r)) 4px calc(20px + var(--sa-l));
    gap: 12px; flex-wrap: nowrap; align-items: center; border-bottom-width: 3px;
  }
  .header-bar h1 { font-size: 16px; letter-spacing: .03em; }
  .header-actions { gap: 5px; flex-wrap: nowrap; }
  .header-actions button, .game-timer button, details.info-details > summary {
    padding: 5px 10px; min-height: 34px; font-size: 12px;
  }
  .game-timer .timer-display { min-width: 38px; font-size: 12px; }

  /* Right-aligned to the safe gutter, and the base rule's 680px cap goes with it
     — which is worth more than it looks. Landscape is the orientation with 440px
     of height and none to spare, and the full 798px lets the twelve fields lay
     out in fewer rows: the panel is 160px tall instead of the 193px it was at
     680. The insets are in the cap so the sensor housing cannot take a corner. */
  .info-body {
    left: auto; right: calc(20px + var(--sa-r));
    max-width: calc(100vw - 40px - var(--sa-l) - var(--sa-r));
  }

  /* --- Strip: 97px -> 71px. Stays one row, as it is above 835px. --- */
  .linescore-wrap { padding: 5px 10px; margin: 5px 0 0; gap: 10px; }
  /* Same free 14px as the iPad block, and the same split — this column is
     stretched to a 69px readout and the table wants 55. 18px -> 25px on the two
     editable rows, table 68px inside 69px, strip unchanged. (F27) */
  .linescore th { height: 18px; }
  .linescore td { height: 25px; }
  .linescore td.team-col { font-size: 12px; }
  .linescore input { font-size: 12px; }
  .linescore td.team-col > span { max-width: min(110px, 12vw); }
  .live-cell { padding: 0 9px; gap: 1px; }
  #ls-inning, #ls-count { font-size: 18px !important; }
  .live-cell svg { width: 24px; height: 24px; }
  .live-matchup .ls-name { font-size: 12px; }
  .live-matchup .ls-name-sm { font-size: 11px; }
  .live-matchup .ls-label-2 { margin-top: 1px; }
  /* The 835px block holds this line open so the strip cannot grow mid-at-bat.
     Same reason here, and less height to give it. */
  .live-matchup .ls-sub { min-height: 11px; font-size: 10px; }

  /* --- Control row --- */
  .sec-bar { flex-wrap: nowrap; margin: 4px 0 3px; gap: 6px; }
  .sec-bar .tab-btn { min-height: 30px; padding: 3px 10px; font-size: 14px; }
  .sec-bar .sec-btn, .sec-bar .ghost-btn { min-height: 30px; padding: 3px 10px; font-size: 12px; }

  /* --- The grid --- */
  .scoring-grid thead th.inn-col { font-size: 13px; }

  /* All nine innings across, which is the entire point of the landscape layout
     and was not actually true on the device. --cell-w was left at the 1024px
     ladder's 75px, so the table wanted 168 + 9x75 = 843px against 798px of grid
     box and clipped the ninth inning — measured at 956px with no insets, where
     the table happened to fill the box exactly, and so missed.

     Derived rather than picked, because the number depends on insets this
     stylesheet cannot see: 100vw is the whole screen including them, so taking
     both insets and .app's two 20px gutters off it gives the real grid box, and
     what is left after the 168px frozen block divides by nine. Self-correcting
     for any landscape phone — 70px on a Pro Max (with or without the insets
     applied, which is the same 630px of innings either way), 74px on a 16, 51px
     on an SE.

     The 20px is the grid's own vertical scrollbar, which 100vw does not know
     about: it takes layout width on a desktop (15px on macOS, 17 on Windows)
     and none on iOS, where it overlays. Without the allowance the table came out
     at 799px against a 783px content box here, and would have landed on exactly
     798 of 798 on the device — no slack for a rounding error in either. 20px
     covers the widest of them and costs two thirds of a pixel per inning.

     Clamped because the arithmetic has no floor of its own: 52px is the
     diamond's own width, below which the cell would clip it, and 82px stops a
     wide short window from inflating the cells past the iPad's own ceiling.
     Extra innings overflow and scroll, as everywhere else. */
  :root {
    --cell-w: clamp(52px, calc((100vw - var(--sa-l) - var(--sa-r) - 40px - 168px - 20px) / 9), 82px);
  }

  /* --- The deck --- */
  /* One row, not two. A second row costs 45px of a screen that has ~150px for
     the batting order — it is the difference between three slots and one.

     The width to fit is 798px, not 956px: the deck bleeds to both screen edges
     and so pays both side insets back in its own padding, and on a Pro Max in
     landscape those are 59px each. (Which makes an 838px-wide desktop window an
     exact stand-in for the device, insets being zero there — the deck comes out
     at 798px either way.)

     Against that, at the shared 46px button, line one came to 816px — and the
     18px it was over by was entirely the three separators' 4px side margins,
     which is why an earlier count of the button widths said it fit. Three things
     were shaved rather than one, so the fit is not resting on any single
     measurement being exactly right: 40px keys (the same 44x40 target the 835px
     block calls the .quick-btn standard), 1px separator margins, and a 4px flex
     gap. Line one measures 774px against the 798px box — 24px of slack, which
     the self-hosted woff2 faces should not move by more than a pixel or two.

     `flex-wrap: wrap` deliberately stays. If some future combination does
     overflow, a second row is ugly; a clipped row with unreachable play buttons
     is worse. */
  .qb-core { padding: 6px 0; }
  .quick-btn { min-height: 40px; min-width: 40px; padding: 5px 8px; font-size: 14px; }
  .qb-more-btn { min-height: 40px; padding: 5px 8px; font-size: 11px; }
  .qb-core, .qb-drawer { gap: 4px; }
  .qb-sep { margin: 0 1px; }
  .qbg-tab { min-height: 30px; }
  .tab-content.active .quick-bar {
    max-height: 88vh;
    padding-left: calc(20px + var(--sa-l)); padding-right: calc(20px + var(--sa-r));
  }
  .deck-handle { min-height: 26px; padding: 3px 0; }
  .deck-handle::before { margin-bottom: 4px; }

  /* I8's word is 8px everywhere, set where the rule is declared — it does not
     need a second figure here. What this block's numbers bought is recorded
     with the rule: line one at 844x390 is the tightest thing in the app, and
     the word had to fit inside 58px of slack in an 804px box without widening
     a key. It does: 746px -> 779px, and the row stays 53px. */

  /* --- The fielder picker, on the shortest screen it has to fit whole ---
     390px of height and the popup is fixed and centred, so anything it does
     not fit is clipped off both ends at once — DONE off the bottom, the
     label off the top, and no scroll to reach either.

     Measured at 844x390 before the field map: 352px tall, 19px clear above
     and below. The map is 180px against the 3x3 grid's 160, so this gives
     the 20px back rather than shrinking the map — the keys stay 44x40 here,
     where the deck's own buttons are 40x40. Padding 14px -> 8px is 12px and
     four row gaps at 8px -> 6px is 8px, measured back at 352px: 19px clear
     above and below, which is the fit the grid it replaces had exactly.

     Taken off the chrome and not off the map on purpose. The map is the only
     part of this popup that has to be read spatially; the padding is not
     doing any teaching.

     I11 then spent that 20px again, on its own terms: this popup's own
     controls were part of the 22-30px family H2 was about, and taking the two
     minor rows to 40px and DONE to 46px put it back at 375px against a 374px
     cap. So the two figures that are only *emphasis* give way here — DONE
     drops to the same 40px as everything else, and the readout sets at 18px
     rather than 22px — which measures 364px, 13px clear each side. The map and
     its 44x40 keys are untouched: they are the part that has to be read. */
  #pos-popup { padding: 8px 10px; gap: 6px; }
  #pos-popup .jsp-btn--lg { min-height: 40px; font-size: 13px; }
  #pos-popup .jsp-readout { font-size: 18px; }
}


/* ============================================================
   REDUCED MOTION — keep this block last, so it can reach every
   transition declared above it whatever else moves.

   Seven `transition` declarations ran unconditionally, including the
   220ms drawer, the 200ms deck fold on a phone, and the caret rotation
   on the details panel. Vestibular triggers aside, a scorer working an
   at-bat has asked their phone to be a pencil; the OS setting is where
   they say so, and the app was not listening.

   Duration, not `transition: none`. `fit()` re-measures the deck on
   `transitionend` (see ui.js) — it is how the 200ms fold hands its
   ~114px back to the batting rows without baking in a stale height —
   and a transition removed outright fires no event at all.

   The finding said to check that the event still fires rather than
   assume it. It could not be checked here: an undisplayed Browser pane
   composites no frames, so no transition runs in the first place and
   the question cannot be put to it (same limitation F40 records for
   focus and blur). So the reasoning is written down instead, and it is
   the stronger answer anyway — under this setting `fit()` does not
   need the event:

     ui.js re-measures on every `[data-ui-act]` click, and the deck
     handle is one. That refit runs on the next animation frame, ~16ms
     later. At 200ms the fold is still moving then, which is exactly
     why the transitionend listener was added (F21). At 0.01ms it has
     long since finished, so the click's own refit already measures the
     settled height and the event is redundant rather than load-bearing.

   Which leaves nothing resting on the event under reduced motion, and
   the 200ms path — where it is load-bearing — untouched. A displayed
   pane would still be worth a look, and the suite covers the half that
   does not need one: that this block exists, and that the handle
   re-measures on its own press.

   `backdrop-filter: blur(12px)` on the docked deck is deliberately left
   alone. It is a static effect, not motion — nothing about it moves —
   and dropping it would put the play buttons over unblurred card text.

   `backdrop-filter: blur(12px)` on the docked deck is deliberately left
   alone. It is a static effect, not motion — nothing about it moves —
   and dropping it would put the play buttons over unblurred card text.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}
