/* §11 look: near-black ink on paper white, no fills/glow/shading/textures.
   HTML overlay text uses the same palette, hairline rules, compact uppercase
   type — reads like a clean printed box score. Palette-driven via CSS custom
   properties (see modules/palette.js), default = ink. */

:root {
  --ink: #141414;
  --paper: #fafaf7;
  --field-opacity: 0.35;
  --mono: 'Courier New', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Dither/fine-grain overlay strength (see modules/dither.js's DITHER_OPACITY,
     kept in sync by hand — this is the one dial to tune). Plain alpha
     compositing (no mix-blend-mode) on purpose: blend modes like overlay/
     soft-light are identity at backdrop lightness 0 or 1 and would silently
     kill the effect on the darkest/lightest palettes (see dither.js). */
  --dither-opacity: 0.05;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
  overflow: hidden;
  overscroll-behavior: none;
}

#scene-container {
  position: fixed;
  inset: 0;
  background: var(--paper);
}
#scene-container canvas { display: block; }

/* Fine-grain dither over the 3D render only (not the HTML text overlay below
   it — see index.html/dev.html: this div is a child of #scene-container, not
   .overlay). Static 4x4 Bayer-ordered tile generated by modules/dither.js's
   ditherTileSVG() (pure, unit-tested) and pasted here by hand — a buildless
   app has no build step to generate it at load, and it never changes unless
   DITHER_AMPLITUDE does. Composited by the browser once as a cached GPU
   texture: zero per-frame JS/GL cost, however long the broadcast runs. A
   plain absolutely-positioned box always paints above the plain (statically
   positioned) canvas sibling, regardless of DOM order, so no z-index dance
   is needed. */
.dither-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--dither-opacity);
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%224%22%20height%3D%224%22%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23595959%22%2F%3E%3Crect%20x%3D%221%22%20y%3D%220%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23838383%22%2F%3E%3Crect%20x%3D%222%22%20y%3D%220%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23636363%22%2F%3E%3Crect%20x%3D%223%22%20y%3D%220%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%238d8d8d%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%221%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23989898%22%2F%3E%3Crect%20x%3D%221%22%20y%3D%221%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%236e6e6e%22%2F%3E%3Crect%20x%3D%222%22%20y%3D%221%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23a2a2a2%22%2F%3E%3Crect%20x%3D%223%22%20y%3D%221%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23787878%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%222%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23686868%22%2F%3E%3Crect%20x%3D%221%22%20y%3D%222%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23929292%22%2F%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%235e5e5e%22%2F%3E%3Crect%20x%3D%223%22%20y%3D%222%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23888888%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23a7a7a7%22%2F%3E%3Crect%20x%3D%221%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%237d7d7d%22%2F%3E%3Crect%20x%3D%222%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%239d9d9d%22%2F%3E%3Crect%20x%3D%223%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23737373%22%2F%3E%3C%2Fsvg%3E");
  background-size: 4px 4px;
}

.overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 14px;
  padding-top: calc(14px + env(safe-area-inset-top));
  padding-bottom: calc(14px + env(safe-area-inset-bottom));
  padding-left: calc(14px + env(safe-area-inset-left));
  padding-right: calc(14px + env(safe-area-inset-right));
}

.hairline { border-color: var(--ink) !important; }

/* ---- top bar: TV broadcast scorebug (White Sox / CHSN LAYOUT) ----
   Uses the compact top-left bar LAYOUT of the Chicago White Sox / Chicago
   Sports Network broadcast scorebug — the arrangement of network badge, stacked
   team rows, big inning, count and diamond — repainted with OUR game's data
   (Meridian Grays / Harborview Pilots, SSN/PPBL branding; see scoreboard.js's
   setTeams()/update(), which reads the same server config/state every prior
   scoreboard build read — nothing here is wired to new data). Replaces the old
   five-hairline-box "ink on paper" scoreboard and the dev-only "SSN ● LIVE"
   corner bug — this is now the single top-left readout.

   MONOCHROME, IN THE HOUSE PALETTE: the original was fixed black/silver TV
   chrome; per the owner it now reads as ink on paper like every other overlay —
   EXACTLY the two house colors (var(--ink)/var(--paper)), the house mono font,
   ink hairlines, secondary text dimmed via opacity (never a gray). The seven
   --sb-* vars below are the whole colour surface and route straight to
   --ink/--paper, so the bug follows the active palette (see modules/palette.js)
   along with the rest of the UI rather than fighting it. */
.scoreboard {
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  margin-top: 34px;
  margin-left: 0;
  /* Banner reserves the stacking context so it always paints ABOVE the
     right-anchored stats panel even if they ever meet — see the
     BANNER-OVERLAP FIX note on .stats-panel. Positioned so z-index applies.
     (Note for future editors: avoid writing a literal "z-index:N" pattern in
     prose here — a test once matched this comment instead of the real
     declaration below, because String.match takes the first hit in source
     order and a comment can appear before its own rule's declaration.) */
  position: relative;
  z-index: 2;
  /* EXACTLY the two house colors, arranged like every other panel: paper
     ground, ink text/rules/lit, ink hairlines — no grays, no accents. (Where
     an element needs to read as secondary it's dimmed with `opacity` in its own
     rule, the same way the rest of the UI does it — that's still the ink color,
     not a third one.) All bug color routes through these seven vars. */
  --sb-black: var(--paper);      /* panel ground */
  --sb-black-2: var(--paper);    /* strip / matchup ground (divided by hairline, not tone) */
  --sb-silver: var(--ink);       /* rules, team abbr, network badge fill */
  --sb-silver-dim: var(--ink);   /* secondary text (softened via opacity per-rule) */
  --sb-white: var(--ink);        /* primary text, scores, lit indicators */
  --sb-out-lit: var(--ink);      /* lit out dot = filled ink */
  --sb-hair: var(--ink);         /* hairline dividers = ink, like .hairline */
  font-family: var(--mono); /* house monospace, same as the rest of the UI */
  color: var(--sb-white);
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.4));
}

.scorebug-bar {
  display: flex;
  align-items: stretch;
  background: var(--sb-black);
  border-top: 2px solid var(--sb-silver);
  border-radius: 2px;
  overflow: hidden;
}

.scorebug-net {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--sb-silver);
  color: var(--sb-black);
  padding: 4px 8px;
  min-width: 38px;
}
.scorebug-net-abbr { font-size: 13px; font-weight: 800; letter-spacing: 0.02em; line-height: 1.15; }
.scorebug-net-league { font-size: 7px; font-weight: 700; letter-spacing: 0.08em; opacity: 0.75; line-height: 1.3; }

.scorebug-teams {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 3px 10px;
  border-left: 1px solid var(--sb-hair);
  min-width: 104px;
}
.scorebug-team-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 2px 0;
}
.scorebug-team-row + .scorebug-team-row { border-top: 1px solid var(--sb-hair); }
.scorebug-team-abbr {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--sb-silver);
}
/* No fixed width on the score itself — runs can run to five figures over
   this show's endless broadcast (2026-07-18), and a fixed narrow column is
   exactly how you clip a big number. tabular-nums keeps the digits from
   jittering the row width as they change without pinning a max. Same
   data-digits step-down as .scorebug-inning-num (see scoreboard.js's
   setDigitText) — a five-figure score shrinks a notch rather than pushing
   the bug wider than it needs to, and (critically, at mobile widths) rather
   than ever needing to be clipped to keep the row's fixed-width neighbors
   from getting crowded. */
.scorebug-team-score {
  font-size: 17px;
  font-weight: 800;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--sb-white);
}
.scorebug-team-score[data-digits="4"] { font-size: 15px; }
.scorebug-team-score[data-digits="5"] { font-size: 14px; }
.scorebug-team-score[data-digits="6"] { font-size: 12px; }

.scorebug-inning {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2px 12px;
  border-left: 1px solid var(--sb-hair);
  min-width: 44px;
}
.scorebug-half-arrow { font-size: 11px; color: var(--sb-silver); line-height: 1; }
/* EVER-GROWING COUNTER (2026-07-18): the show's signature — this inning
   number can run to 3, 4, 5+ digits over a long broadcast. No fixed width
   (white-space:nowrap only), so it's never clipped; scoreboard.js's update()
   stamps data-digits with the current digit count and these rules step the
   size down a notch per extra digit past 3 so a long run still reads as
   clearly and prominently as a fresh single-digit inning, just narrower. */
.scorebug-inning-num {
  display: block;
  font-size: 30px;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
.scorebug-inning-num[data-digits="4"] { font-size: 23px; }
.scorebug-inning-num[data-digits="5"] { font-size: 18px; }
.scorebug-inning-num[data-digits="6"] { font-size: 15px; }

.scorebug-count {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 3px 10px;
  border-left: 1px solid var(--sb-hair);
  min-width: 40px;
}
.scorebug-count-bs {
  font-size: 13px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.scorebug-count-sep { color: var(--sb-silver-dim); margin: 0 1px; opacity: 0.5; }
.outs-dots { display: flex; gap: 4px; }
.dot {
  width: 7px; height: 7px;
  border: 1px solid var(--sb-silver-dim);
  border-radius: 50%;
  background: transparent;
  opacity: 0.45; /* unlit reads faint; .dot.lit restores full ink */
}
.dot.lit { border-color: var(--sb-out-lit); background: var(--sb-out-lit); opacity: 1; }

.scorebug-bases {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 12px;
  border-left: 1px solid var(--sb-hair);
}
.diamond { position: relative; width: 22px; height: 22px; transform: rotate(45deg); }
.base {
  position: absolute;
  width: 8px; height: 8px;
  opacity: 0.4; /* empty base reads faint; .base.lit restores full ink */
  border: 1.5px solid var(--sb-silver-dim);
  background: transparent;
}
.base.lit { border-color: var(--sb-white); background: var(--sb-white); opacity: 1; }
/* ALIGNMENT FIX (2026-07-14): a 45deg rotation only turns a square's own
   CORNERS into the diamond's up/right/down/left points — a point sitting on
   an EDGE MIDPOINT pre-rotation (the old top:12/left:12/right:0 values,
   centered along one axis) lands on a DIAGONAL post-rotation instead, so the
   three bases came out skewed relative to one another rather than forming a
   clean triangle around home. Placing each base at a true corner of the
   (unrotated) .diamond box fixes it: top-left -> top point (2B), top-right ->
   right point (1B), bottom-left -> left point (3B); the unused bottom-right
   corner would be home. */
.base-second { top: 0; left: 0; }
.base-first { top: 0; right: 0; }
.base-third { bottom: 0; left: 0; }

.scorebug-strip {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  background: var(--sb-black-2);
  border-top: 1px solid var(--sb-hair);
  padding: 2px 10px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--sb-silver-dim);
}

.matchup-block {
  background: var(--sb-black-2);
  border-top: 1px solid var(--sb-hair);
  padding: 5px 10px 6px;
  font-size: 11px;
}
.matchup-row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--sb-white);
}
.matchup-row span:first-child { color: var(--sb-silver-dim); font-weight: 700; margin-right: 8px; flex: 0 0 auto; opacity: 0.6; }
/* Batter/pitcher names are arbitrary-length real data, not our copy — bound
   to one line (flex: 1 1 auto + min-width: 0 is what lets an ellipsis apply
   inside a flex item at all; the browser default min-width:auto would keep
   it at its content width and never actually clip). Keeps the whole bug's
   height PREDICTABLE (see the ui.js clearance comment on .stats-panel/
   .feed-panel, which is sized against this never wrapping). */
.matchup-row span:last-child {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
}
/* STAT-BOX follow-up (2026-07-15): real battingLineToday/recentForm/
   pitcherLineToday strings (see scoreboard.js update()) under each
   matchup-row — sentence-cased, so override the block's uppercase. Same
   one-line-only bound as the name above, same reason. */
.matchup-stat-line {
  color: var(--sb-silver-dim);
  opacity: 0.65;
  font-size: 9px;
  text-transform: none;
  letter-spacing: 0.02em;
  margin: 1px 0 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.matchup-stat-line:last-child { margin-bottom: 0; }

/* ---- bottom: captions ---- (the ticker that used to live here was
   consolidated into the single feed.js play-by-play panel — see its CSS
   block below — so there's exactly one log-like element on screen) */
.bottom-block {
  align-self: stretch;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: 14px;
}

.captions {
  flex: 1;
  text-align: center;
  font-size: 13px;
  min-height: 40px;
  /* ANNOUNCER TRANSCRIPT: a running history of the booth that can be scrolled
     back through (see scoreboard.js setCaptions). Height-bounded so it still
     reads as subtitles sitting at the bottom-center of frame rather than a
     wall of text; scroll up for everything Dutch and Sal have said. The
     overlay sets pointer-events:none, so re-enable them here to allow the
     scroll. */
  max-height: 96px;
  overflow-y: auto;
  overscroll-behavior: contain;
  pointer-events: auto;
  scrollbar-width: thin;
}
.caption-line { padding: 1px 0; }
.caption-pbp { font-weight: bold; }
.caption-color { opacity: 0.75; font-style: italic; }
/* Sideline reporter "Dot" (2026-07-18): a distinct caption register from the
   two booth voices — upright (not italic like Sal) but lighter than Dutch's
   bold, so a between-innings interview beat reads as its own thing. */
.caption-sideline { font-weight: 600; opacity: 0.9; }

/* ---- controls (interactive, pointer-events re-enabled) ----
   Moved to top-right (punchlist 2026-07-17) to avoid overlaying captions;
   grouped with a hairline border to read as a cohesive control cluster
   matching the ink-on-paper aesthetic. */
.controls {
  position: absolute;
  top: calc(14px + env(safe-area-inset-top));
  right: calc(14px + env(safe-area-inset-right));
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  justify-content: flex-end;
  align-items: flex-start;
  pointer-events: auto;
  max-width: 180px;
  padding: 6px;
  border: 1px solid var(--ink);
  background: var(--paper);
}
.chip {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-family: var(--mono);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 4px 8px;
  cursor: pointer;
  touch-action: manipulation;
  text-decoration: none;
}
.chip:hover { background: color-mix(in srgb, var(--ink) 8%, var(--paper)); }
.chip.active { background: var(--ink); color: var(--paper); }

/* Mind-view discoverability hint (PLAYER-INTERIORITY Wave 3) — sits among the
   real chip buttons in .controls but isn't one (no click handler): a plain
   <span>, so it needs its own look-alike rule rather than button.chip above
   (which is scoped to actual buttons). Same hairline card language, just
   non-interactive (default cursor, no hover state, slightly dimmed so it
   reads as ambient chrome rather than another control competing for taps). */
.mind-hint {
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-family: var(--mono);
  font-size: 9px;
  text-transform: none;
  letter-spacing: 0.06em;
  padding: 4px 8px;
  cursor: default;
  opacity: 0.7;
}

/* Icon-only chip buttons (compact, centered) */
.chip-icon {
  font-size: 13px;
  text-transform: none;
  letter-spacing: 0;
  padding: 4px 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
}

/* ---- hover-to-identify (punchlist 2026-07-15) ---- */
/* A small printed-caption-style label, not a generic browser tooltip: same
   ink-on-paper hairline box every other readout on this broadcast uses (see
   .stats-card/.matchup-row above), positioned by modules/hover.js's
   mountHoverLabel via left/top (CSS px, matching scene.js's pickHover
   screen-space output). pointer-events stays off (inherited from .overlay)
   — it must never itself become the thing under the cursor, or hovering the
   label would fight the actor it's naming. The fade is the ONLY concession
   to the "for a bit" dwell already gating when this class gets added: the
   label itself has no timer of its own. */
.hover-label {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, calc(-100% - 10px));
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 3px 7px;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.hover-label.visible { opacity: 1; }

/* ---- pitch-location marker velo readout (punchlist 2026-07-16) ---- */
/* Same ink-on-paper hairline-card family as .hover-label just above (see
   modules/strikezone.js's mountPitchMarkerLabel — identical show/hide
   contract, driven every frame by playback.js's `this.pitchMarker`). A
   smaller vertical lift than .hover-label's -10px: this sits just off a
   small in-scene dot, not above a full figure's head. No timer of its own —
   the fade is purely the opacity transition; visibility is driven entirely
   by whether playback.pitchMarker is set that frame. */
.pitch-marker-label {
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, calc(-100% - 6px));
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 3px 7px;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.pitch-marker-label.visible { opacity: 1; }

/* ---- suggestion box ('~' key, punchlist 2026-07-16 — see modules/
   suggestBox.js) ---- Deliberately NOT a full-screen blocking treatment
   like .join-overlay/.ident-overlay below: this is a lightweight "hit ~,
   jot a note, hit enter" utility, not a gate the viewer must clear, so the
   broadcast keeps running behind it. Same ink-on-paper hairline-card family
   as .hover-label/.stats-card/.pitch-marker-label above — a real HUD
   element, not a generic browser prompt(). The wrapper itself has
   pointer-events:none (so it never blocks clicks on the scene behind it
   when hidden or between interactions); only .suggest-card re-enables them,
   matching how .overlay's own pointer-events:none / .controls' auto
   split already works elsewhere on this page. Anchored bottom-center, above
   .controls' chip row so the two never overlap. */
.suggest-overlay {
  position: fixed;
  left: 50%;
  bottom: calc(64px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 25;
  pointer-events: none;
}
.suggest-overlay.hidden { display: none; }
.suggest-card {
  pointer-events: auto;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  padding: 10px 12px;
  width: min(360px, 86vw);
}
.suggest-label {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.7;
  margin-bottom: 6px;
}
.suggest-input {
  display: block;
  width: 100%;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  padding: 6px 8px;
  outline: none;
}
.suggest-input:disabled { opacity: 0.5; }
.suggest-status {
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  min-height: 12px;
  opacity: 0.8;
}

/* ---- join overlay ---- */
.join-overlay {
  position: fixed;
  inset: 0;
  background: var(--paper);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  z-index: 20;
}
.join-overlay.hidden { display: none; }
.join-btn {
  font-family: var(--mono);
  font-size: 16px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  background: var(--paper);
  color: var(--ink);
  border: 2px solid var(--ink);
  padding: 16px 28px;
  cursor: pointer;
  touch-action: manipulation;
  min-height: 44px;
}
.join-btn:hover { background: color-mix(in srgb, var(--ink) 8%, var(--paper)); }
.join-sub { margin-top: 10px; font-size: 10px; text-align: center; opacity: 0.6; letter-spacing: 0.1em; text-transform: uppercase; }

/* ---- full-screen network ident/bumper (punchlist 2026-07-15, camera/
   broadcast production: "full-screen network idents/bumpers ... in the same
   stylized monochrome style, used both as pure branding beats..." — pure-
   branding-beat half only, see modules/ident.js's header comment) ----
   Same ink-on-paper full-screen treatment as .join-overlay (opaque, covers
   everything) but non-interactive (pointer-events stays off — it's a
   transition beat, not a gate the viewer has to act on) and driven purely by
   an opacity fade rather than display:none, so IDENT_DISPLAY_MS reads as a
   real stinger transition instead of a hard cut. Carries its own
   .dither-overlay child so the card gets the exact same fine-grain dither
   grain #scene-container does (see dither.js) — the two never share one
   overlay element since the ident sits in front of the scene, not behind. */
.ident-overlay {
  position: fixed;
  inset: 0;
  background: var(--paper);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 15;
  opacity: 0;
  transition: opacity 0.35s ease;
}
.ident-overlay.visible { opacity: 1; }
/* Deliberately heavier grain than the world's 0.05 (docs/overlay-layering-design.md:
   L1 "Produced Insert" vs L0 "World" — a network ident is a different piece of tape
   spliced into the broadcast, not a window into the game, so it should read as
   visibly graded differently, the way a real network bumper looks like a distinct
   videotape generation from the live feed around it. Reasoned starting value, not
   verified against a rendered frame — no headless browser on this box; tune by eye
   if it reads wrong live. */
.ident-overlay .dither-overlay { opacity: 0.11; }
.ident-card {
  position: relative;
  text-align: center;
  padding: 20px 32px;
  /* No border here on purpose (punchlist 2026-07-16 marquee redux) — the
     drawn-on SVG corner brackets below ARE the frame now; a plain box
     border around them would read as a second, redundant, boring frame
     competing with the theatrical one. */
}
/* ---- marquee wordmark (punchlist 2026-07-16 "huge redux" — see
   modules/identMarquee.js's header comment for the full design: bold
   single-stroke corner brackets + letterforms + bracketing rules that fly
   in from the four screen corners and stroke-draw on). `overflow: visible`
   is load-bearing: every piece starts translated well outside the SVG's own
   viewBox (that's the "fly in" part) and the default SVG clip would hide it
   there until it crossed back inside — see identMarquee.js's cornerOffset.
   `vector-effect: non-scaling-stroke` keeps the line weight a constant
   ON-SCREEN pixel width regardless of how much the responsive viewBox ends
   up scaled up/down, the same "hairline stays a hairline" discipline as the
   rest of this project's line art. */
.ident-marquee {
  display: block;
  width: min(480px, 82vw);
  height: auto;
  overflow: visible;
  margin: 0 auto;
}
.ident-marquee-line,
.ident-marquee-glyph,
.ident-marquee-mark {
  fill: none;
  stroke: var(--ink);
  stroke-linecap: square;
  stroke-linejoin: miter;
  vector-effect: non-scaling-stroke;
}
.ident-marquee-line { stroke-width: 3; }
/* Letters read bolder than the frame/rule accents — the wordmark is the
   point, the frame is trim around it. Currently unused in index.html's
   markup (the center glyph is the samsara wheel below, not S/N letters —
   see identMarquee.js's 2026-07-20 header comment) but kept for any future
   piece that reuses modules/identMarquee.js's letterPathD. */
.ident-marquee-glyph { stroke-width: 7; }
/* The samsara wheel mark (2026-07-20, modules/samsaraMark.js) — several
   close concentric rings in a small box, so this reads as a hairline like
   the frame accents (.ident-marquee-line) rather than the bold letter
   weight above: a thick stroke here would fuse the rings together. */
.ident-marquee-mark { stroke-width: 1.6; }
/* Visually-hidden (not display:none — screen readers should still see the
   real, config-driven abbreviation) accessible fallback for the decorative
   marquee above; see index.html's comment just above this element. Standard
   clip-based hidden-but-announced pattern. */
.ident-network-abbr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.ident-network {
  margin-top: 6px;
  font-size: 13px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.ident-rule { margin: 16px auto; width: 64px; height: 0; border-top: 1px solid var(--ink); }
.ident-league {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* ---- MIND VIEW (punchlist #54, modules/mindPanel.js) ----
   Double-click a hovered on-field figure to open this panel over a dim
   backdrop (unlike .suggest-overlay's non-blocking chrome, this IS a real
   modal — a deliberate, on-demand deep-dive the viewer opened, not an
   always-present HUD element) showing that figure's imagined-thought
   history. Closed via the header's ✕, clicking the backdrop, or Escape.
   Highest z-index of the overlay family (30) since it's the most recently
   and deliberately opened thing on screen. */
.mind-overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--ink) 40%, transparent);
  pointer-events: auto;
}
.mind-overlay.hidden { display: none; }
.mind-panel {
  width: min(420px, 90vw);
  max-height: min(70vh, 560px);
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  font-size: 11px;
}
/* Header carries a small standing "MIND VIEW" eyebrow (pure ::before, no
   markup change — index.html is outside this feature's owned files) above
   the name/close row: flex-wrap + a full-width ::before forces it onto its
   own line, so the panel reads as a distinct broadcast surface ("this is an
   interiority readout") rather than just another name-and-a-close-button
   card (presentation pass, 2026-07-18). */
.mind-panel-header {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 2px 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: bold;
}
/* MOOD (PLAYER-INTERIORITY Wave 3, additive; 2-axis meter added in the
   2026-07-18 presentation pass): the selected figure's current affect
   (server/src/playerAffect.ts's descriptor over valence/arousal), surfaced
   as a standing line under the header rather than buried in a thought row —
   a viewer should read "how this figure feels right now" at a glance, not
   have to infer it from prose. Hidden via .hidden whenever mountMindPanel has
   no reading for the open figure (see renderMood). Flex row so the meter
   widget and the text label sit side by side; renderMood's plain-text
   fallback (unit tests / a container with no appendChild) still renders fine
   as a single flex item here. */
.mind-mood {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--ink);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.8;
}
.mind-mood.hidden { display: none; }
/* Meter + label stay a row (unchanged pairing) within the now-column
   .mind-mood standing line, so the legend below (next rule) can sit on its
   own line without widening the row itself. */
.mind-mood-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* AXIS LEGEND (additive readability pass, 2026-07-18): a one-line, plain-
   English key for what the dot's position on the meter actually means —
   without it, "the dot sits up and to the right" only reads as intentional
   to someone who already knows the valence/arousal convention. Faint/small
   so it reads as a caption, not a second label competing with "Mood: X". */
.mind-mood-legend {
  font-size: 8px;
  letter-spacing: 0.04em;
  text-transform: none;
  opacity: 0.5;
}
/* The valence(x)/arousal(y) meter itself: a small square with a faint
   crosshair (::before/::after) and a single solid dot positioned by
   mindPanel.js's buildMoodMeter (inline left/top %) — right = more pleasant,
   up = more activated. Ink-only (opacity, not hue) so it stays correct under
   any palette swap (modules/palette.js). */
.mind-mood-meter {
  position: relative;
  flex: none;
  width: 24px;
  height: 24px;
  border: 1px solid color-mix(in srgb, var(--ink) 45%, transparent);
}
.mind-mood-meter::before,
.mind-mood-meter::after {
  content: '';
  position: absolute;
  background: color-mix(in srgb, var(--ink) 18%, transparent);
}
.mind-mood-meter::before { left: 0; right: 0; top: 50%; height: 1px; }
.mind-mood-meter::after { top: 0; bottom: 0; left: 50%; width: 1px; }
.mind-mood-dot {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink);
  transform: translate(-50%, -50%);
}
.mind-mood-label { white-space: nowrap; }
/* RELATIONSHIPS (PLAYER-INTERIORITY Wave 4, additive; per-row list added in
   the 2026-07-18 presentation pass): the selected figure's most-notable
   relationships (server/src/playerRelationships.ts), under mood — same
   standing-line treatment as .mind-mood just above, its own rule only so
   mood and relationships can be independently hidden. The plain italic
   sentence styling here is renderRelationships' fallback (unit tests /
   pre-appendChild container); the rich path below renders one .mind-rel-row
   per relationship instead, each overriding font-style locally. */
.mind-relationships {
  padding: 6px 10px;
  border-bottom: 1px solid var(--ink);
  font-size: 10px;
  letter-spacing: 0.02em;
  opacity: 0.8;
  font-style: italic;
  line-height: 1.4;
}
.mind-relationships.hidden { display: none; }
.mind-rel-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 3px;
}
.mind-rel-row:last-child { margin-bottom: 0; }
.mind-rel-name { font-style: italic; }
.mind-rel-tag {
  flex: none;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 9px;
  opacity: 0.7;
  white-space: nowrap;
}
/* fond/friendly lean bold+opaque, wary/resentful lean faint — direction read
   entirely off weight/opacity (never hue), same "safe under any palette"
   discipline as the mood meter above. */
.mind-rel-tag--fond, .mind-rel-tag--friendly { font-weight: bold; opacity: 0.9; }
.mind-rel-tag--wary, .mind-rel-tag--resentful { opacity: 0.5; }
/* Warmth bar (readability pass, 2026-07-18): a short track with a visible
   zero-line at its CENTER, filling outward from that midpoint — right for
   pos, left for neg (::before fills to half of --pct, set inline by
   renderRelationships from |affinity|, since each half of the track
   represents the full 0-100% intensity range in its own direction). This
   replaces the old left-aligned fill that told pos/neg apart only by a
   subtle opacity difference (0.85 vs 0.4), which read as "shorter bar" more
   than "opposite direction" at a glance, especially for small |affinity|.
   Direction is now unambiguous from geometry alone, and the always-visible
   track + center tick give a short bar a baseline to read "a little"
   against instead of floating in blank space. */
.mind-rel-bar {
  position: relative;
  flex: none;
  display: inline-block;
  width: 26px;
  height: 4px;
  background: color-mix(in srgb, var(--ink) 12%, transparent);
}
.mind-rel-bar::after {
  content: '';
  position: absolute;
  left: 50%;
  top: -1px;
  bottom: -1px;
  width: 1px;
  background: color-mix(in srgb, var(--ink) 35%, transparent);
}
.mind-rel-bar::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: calc(var(--pct, 0%) / 2);
  background: var(--ink);
  opacity: 0.85;
}
.mind-rel-bar--pos::before { left: 50%; }
.mind-rel-bar--neg::before { right: 50%; }
/* INTERESTS/CONCERNS (reader vertical slice, additive, 2026-07-18):
   server/src/playerInterests.ts's topic-id sets, surfaced as a standing line
   under relationships — same "own rule so it can hide independently" pattern
   as .mind-mood/.mind-relationships. There is no static markup hook for this
   in index.html (out of this feature's owned files); modules/mindPanel.js
   creates the container itself and splices it in after .mind-relationships,
   so it only ever exists as a real DOM node — no plain-text fallback rule
   needed here the way .mind-mood/.mind-relationships carry for their own
   caller-supplied containers. */
.mind-interests {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--ink);
  font-size: 10px;
  opacity: 0.8;
}
.mind-interests.hidden { display: none; }
.mind-interests-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px;
}
.mind-interests-label {
  flex: none;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 9px;
  opacity: 0.6;
  white-space: nowrap;
}
.mind-tag {
  flex: none;
  border: 1px solid color-mix(in srgb, var(--ink) 35%, transparent);
  border-radius: 2px;
  padding: 1px 5px;
  font-size: 9px;
  line-height: 1.5;
  white-space: nowrap;
}
/* Interests read plain/opaque; concerns lean italic + a touch fainter — same
   ink-only, weight/opacity-driven direction cue the relationship tags use,
   never a new hue. */
.mind-tag--interest { opacity: 0.9; }
.mind-tag--concern { opacity: 0.65; font-style: italic; }
/* LIFE-LOG DENSITY (readability pass, 2026-07-18): this is already the
   panel's sole scrolling surface (.mind-panel itself is height-capped above,
   flex-column, so this is the only element that grows instead of it) — the
   inset shadow just makes that fact legible: a permanent, subtle top/bottom
   vignette that reads as "this area scrolls" whether or not the log is
   currently long enough to overflow, so a long history never needs to push
   the panel itself off-screen and older entries fade toward the edge rather
   than crowd flush against the header/close button. */
.mind-panel-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 12px;
  box-shadow:
    inset 0 8px 6px -6px color-mix(in srgb, var(--ink) 14%, transparent),
    inset 0 -8px 6px -6px color-mix(in srgb, var(--ink) 14%, transparent);
}
.mind-status {
  opacity: 0.6;
  font-style: italic;
  padding: 12px 0;
  text-align: center;
}
.mind-status.hidden { display: none; }
/* STATUS KINDS (readability pass, 2026-07-18): loading/empty/error used to
   share this one undifferentiated faint-italic look — a stalled fetch and
   "nothing to see here" read identically. --loading pulses gently (still
   in progress, not stuck); --error drops the italic/faint treatment
   entirely and gets a small marker so a real failure doesn't read as just
   another quiet ambient state. --empty is the plain base look above,
   unchanged (that's the closest fit to "nothing here yet" as-is). Ink-only
   throughout — no new color, matches the house palette. */
.mind-status--loading { animation: mind-status-pulse 1.4s ease-in-out infinite; }
@keyframes mind-status-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 0.75; }
}
.mind-status--error {
  font-style: normal;
  font-weight: bold;
  opacity: 0.85;
}
.mind-status--error::before { content: '⚠ '; opacity: 0.7; }
/* LIFE-LOG (PLAYER-INTERIORITY Wave 4): a chronological stream of every
   thought/social/interstitial entry on file for the open figure. FORMATTING
   HIERARCHY (the whole point of this wave): .mind-thought (thought + social
   — the meaningful moments) stays BOLD/prominent, at the base panel scale;
   .mind-interstitial (a cough, an adjusted cap — quiet texture between them)
   is small/faint and carries no border/meta row of its own, so the log reads
   as a life stream where the small human ticks recede rather than compete
   for attention. A tighter gap than before (8px, was 10px) since
   interstitial rows are meant to sit close to their neighbors.

   A left "spine" (border-left on .mind-list) plus a per-entry ::before dot
   (2026-07-18 presentation pass) turns the stream into a readable timeline:
   .mind-thought gets a solid dot, .mind-thought--social a hollow ring (the
   same bold tier, visually marked as an exchange rather than private prose),
   .mind-interstitial a tiny faint dot. Pure CSS — no new DOM per entry, so
   this costs nothing structurally and can't drift from renderList's classes. */
.mind-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-left: 4px;
  padding-left: 12px;
  border-left: 1px dotted color-mix(in srgb, var(--ink) 22%, transparent);
}
.mind-thought {
  position: relative;
  border-bottom: 1px dotted color-mix(in srgb, var(--ink) 25%, transparent);
  padding-bottom: 8px;
}
.mind-thought:last-child { border-bottom: none; padding-bottom: 0; }
.mind-thought::before {
  content: '';
  position: absolute;
  left: -15.5px;
  top: 3px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--ink);
}
.mind-thought--social::before {
  background: var(--paper);
  border: 1px solid var(--ink);
}
.mind-thought-meta {
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.5;
  margin-bottom: 3px;
}
.mind-thought-text { line-height: 1.4; font-style: italic; font-weight: 600; }
/* A social entry ("traded words with X") is a plain grounded fact, not
   interior prose — same bold/prominent tier as a thought, just not
   italicized (italics stay reserved for the figure's own imagined voice). */
.mind-thought--social .mind-thought-text { font-style: normal; }
/* The small/faint tier — deliberately NOT a peer of .mind-thought (no
   border, no uppercase meta row, no bold): a single quiet line so a run of
   several in a row reads as background texture, not a list of equals. Now a
   flex row (text + a trailing faint relative-time span) rather than one bare
   text node, so recency stays visible without a bold meta row. */
.mind-interstitial {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  padding: 0 0 0 2px;
  font-size: 9px;
  font-style: italic;
  line-height: 1.5;
  opacity: 0.42;
  letter-spacing: 0.01em;
}
.mind-interstitial::before {
  content: '';
  position: absolute;
  left: -14.5px;
  top: 6px;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--ink);
}
.mind-interstitial-text { flex: 1 1 auto; }
.mind-interstitial-time {
  flex: none;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* ---- side panels: compact + collapsible (UI cleanup 2026-07-14) ----
   Both .stats-panel and .feed-panel carry a click-to-collapse header
   (`[data-collapse-header]` — see ui.js wireCollapsible, state persisted per
   panel in localStorage) so the field view can be kept clean without losing
   either panel outright. Panels stay FULLY OPAQUE at all times — the earlier
   idle auto-hide/gray-out (faded panels to ~12% after a few idle seconds) was
   removed per user feedback; there is no longer any opacity fade.

   Collapsed = fully hidden except a small, clearly-clickable re-open TAB: the
   panel shrinks to just its header bar (body hidden, width/height shrink to fit
   the label, nav hidden), rather than leaving the full-size box behind. Click
   the tab to re-open. */
[data-collapse-header] { cursor: pointer; user-select: none; }
.panel-caret { display: inline-block; margin-right: 5px; transition: transform 0.15s ease; }
.collapsed .panel-caret { transform: rotate(-90deg); }

/* Collapsed: hide the body, shrink the panel to a compact header-only tab. */
.stats-panel.collapsed .stats-panel-body,
.feed-panel.collapsed .feed-list { display: none; }
.stats-panel.collapsed,
.feed-panel.collapsed { width: auto; }
/* The feed panel is normally stretched between a top and bottom anchor; when
   collapsed it must size to its header instead of spanning the full height. */
.feed-panel.collapsed { bottom: auto; }
/* Nothing but the label + caret in the tab (hide the stats prev/next nav). */
.stats-panel.collapsed .stats-nav { display: none; }
/* No dangling divider under the header once the body is gone. */
.stats-panel.collapsed .stats-panel-header,
.feed-panel.collapsed .feed-panel-header { border-bottom: none; }

/* ---- stats pane (STATS & BANTER track) ----
   Anchored top-right, independent of the .overlay flex flow (like
   .controls) so it never displaces the scoreboard (top-left) or the
   bottom-left play-by-play log (feed.js) another track owns.
   Interactive (roster click, prev/next, collapse), so pointer-events
   re-enabled locally. Compact by default (narrow, small type).

   BANNER-OVERLAP FIX (2026-07-14, remeasured 2026-07-18 for the White-Sox-
   style scorebug rebuild): the panel is right-anchored, so as the window
   NARROWS its left edge slides leftward across the top-right of the
   scoreboard — it used to cover part of the banner. It's anchored BELOW the
   banner's full height so it can never overlap it at any width above the
   480px phone breakpoint (where it's hidden entirely). The scorebug rebuild
   made the banner noticeably taller than the old single-row 5-box layout —
   it now stacks a main bar + a day/league strip + a batter/pitcher sub-panel
   (.matchup-block, held to a single line per side via the ellipsis rules on
   .matchup-row/.matchup-stat-line so this height stays a known quantity) —
   measured ~148px tall at its realistic worst case (5-figure score/inning,
   3-digit day). top:180px clears that with margin. NOTE: in the live app
   modules/ui.js's injected "compact controls" stylesheet OVERRIDES this
   top/max-height with !important (see its own matching comment) — this base
   rule is what a bare index.html without that script would still get, kept
   in sync by hand. The scoreboard also gets a higher z-index (below) as
   belt-and-suspenders. */
.stats-panel {
  position: absolute;
  top: calc(180px + env(safe-area-inset-top));
  right: calc(14px + env(safe-area-inset-right));
  z-index: 1;
  width: 200px;
  max-height: calc(100vh - 194px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ink);
  background: var(--paper);
  font-size: 10px;
  pointer-events: auto;
  overflow: hidden;
}
.stats-panel-body { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
.stats-panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 5px 7px;
  border-bottom: 1px solid var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 10px;
  opacity: 0.85;
  flex: 0 0 auto;
}
.stats-nav { display: flex; align-items: center; gap: 6px; }
.stats-nav span { min-width: 3.5ch; text-align: center; opacity: 0.7; }
button.stats-chip {
  padding: 1px 7px;
  font-size: 12px;
  line-height: 1.4;
  min-height: 0;
}
.stats-live { padding: 6px 8px; border-bottom: 1px dotted color-mix(in srgb, var(--ink) 35%, transparent); }
.stats-live-row {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 1px 0;
  opacity: 0.85;
}
.stats-live-row span:first-child { opacity: 0.55; letter-spacing: 0.06em; }
.stats-live-row span:last-child { text-align: right; word-break: break-word; }

.stats-card { padding: 6px 8px; border-bottom: 1px solid var(--ink); }
.stats-card-name { font-weight: bold; text-transform: uppercase; letter-spacing: 0.04em; }
.stats-card-meta { opacity: 0.55; font-size: 10px; margin-top: 1px; text-transform: uppercase; letter-spacing: 0.06em; }
.stats-card-line { margin-top: 4px; opacity: 0.9; line-height: 1.35; }

.stats-roster {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 2px 0;
}
.stats-roster-group {
  padding: 4px 8px 2px;
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.5;
}
.stats-roster-row {
  padding: 3px 8px;
  cursor: pointer;
  opacity: 0.8;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stats-roster-row:hover { background: color-mix(in srgb, var(--ink) 8%, var(--paper)); }
.stats-roster-row.active { background: var(--ink); color: var(--paper); opacity: 1; }
.stats-roster-row.inactive { opacity: 0.4; font-style: italic; }

/* ---- play-by-play feed (the single, consolidated game log) ----
   Bottom-left. This used to be additive to a separate one-line
   `.ticker[data-ticker]` (owned by scoreboard.js/playback.js) stacked below
   it — the two were redundant (per user feedback), so the ticker is gone and
   this panel is now the ONLY log surface, decomposing every event into
   several fine-grained lines, fed in sync with on-screen play (see
   playback.js onSettle). Anchored with both `top` and `bottom` (rather than
   a fixed height) so its height is whatever room is left between the
   scoreboard above and the captions row below. Compact by default (narrower
   than before, small type); pointer-events re-enabled locally so the line
   list can be scrolled back through by hand (like .stats-roster). */
.feed-panel {
  position: absolute;
  left: calc(14px + env(safe-area-inset-left));
  top: calc(200px + env(safe-area-inset-top));
  bottom: calc(64px + env(safe-area-inset-bottom));
  width: 260px;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ink);
  background: var(--paper);
  font-size: 10px;
  pointer-events: auto;
  overflow: hidden;
}
.feed-panel-header {
  padding: 5px 7px;
  border-bottom: 1px solid var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 10px;
  opacity: 0.85;
  flex: 0 0 auto;
}
.feed-list {
  flex: 1;
  overflow-y: auto;
  padding: 4px 8px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* Newest line is appended at the BOTTOM (documented ordering choice — reads
   like a scrolling console) and panel auto-scrolls there unless the reader
   has scrolled up (see feed.js mountFeedPanel). */
.feed-line {
  padding: 2px 0;
  border-bottom: 1px dotted color-mix(in srgb, var(--ink) 20%, transparent);
  line-height: 1.3;
  opacity: 0.75;
}
.feed-line:last-child { opacity: 1; border-bottom: none; }

/* FIGURES+UMPS+LIFE: imagined between-play conversations (see dialogue.js /
   feed.js logConversation). Set clearly apart from the mechanical play-by-play
   — italic, indented, with a small chat glyph — so the log reads as overheard
   dugout/field chatter interleaved with the calls. */
.feed-line.feed-chat {
  font-style: italic;
  padding-left: 16px;
  position: relative;
  opacity: 0.62;
  border-bottom: none;
}
.feed-line.feed-chat::before {
  content: "\201C"; /* left double quote as a small chat glyph */
  position: absolute;
  left: 3px;
  font-style: normal;
  opacity: 0.7;
}
.feed-line.feed-chat:last-child { opacity: 0.85; }

@media (max-width: 480px) {
  /* Same call as .stats-panel just above: no room on a compact phone layout
     for a secondary detail panel. */
  .feed-panel { display: none; }
}

/* ---- dev.html scrub bar ---- */
.scrub-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  background: var(--paper);
  border-top: 1px solid var(--ink);
  padding: 8px 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: auto;
  z-index: 15;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.scrub-bar input[type="range"] { flex: 1; accent-color: var(--ink); }
.scrub-bar .label { min-width: 90px; }

/* ---- touch tap targets (real pointing devices, any viewport size) ---- */
@media (pointer: coarse) {
  .chip {
    min-width: 40px;
    min-height: 32px;
    padding: 6px 10px;
  }
  .chip-icon {
    min-width: 40px;
    min-height: 32px;
  }
}

/* ---- portrait phone layout (360-430px-class widths) ----
   Full-bleed canvas is already achieved by #scene-container (position:fixed;
   inset:0). Here: shrink the scorebug bar to a compact single row of team
   abbrs/scores/inning/outs/count, drop the day/league strip and the
   batter/pitcher sub-panel (secondary info that doesn't fit), and keep
   captions readable. */
@media (max-width: 480px) {
  .overlay { padding: 10px; }
  .overlay {
    padding-top: calc(10px + env(safe-area-inset-top));
    padding-bottom: calc(10px + env(safe-area-inset-bottom));
    padding-left: calc(10px + env(safe-area-inset-left));
    padding-right: calc(10px + env(safe-area-inset-right));
  }

  .scoreboard {
    width: 100%;
    margin-top: 28px;
  }

  .scorebug-bar { flex-wrap: nowrap; }

  .scorebug-net { min-width: 0; padding: 3px 5px; }
  .scorebug-net-abbr { font-size: 10px; }
  .scorebug-net-league { display: none; }

  /* flex: 0 0 auto (NOT 1 1 auto) — a shrinkable neighbor next to fixed-width
     siblings is exactly how a big score got crushed down to a few unreadable
     pixels here before (2026-07-18 fix): the team abbr is always <=3 chars
     (see deriveAbbr) and the score has its own data-digits step-down above,
     so this column's content width is already bounded — it doesn't need to
     (and must never be allowed to) shrink below it. */
  .scorebug-teams { min-width: 0; flex: 0 0 auto; padding: 2px 6px; }
  .scorebug-team-row { gap: 6px; }
  .scorebug-team-abbr { font-size: 10px; }
  .scorebug-team-score { font-size: 13px; }
  .scorebug-team-score[data-digits="4"] { font-size: 12px; }
  .scorebug-team-score[data-digits="5"] { font-size: 11px; }
  .scorebug-team-score[data-digits="6"] { font-size: 9px; }

  .scorebug-inning { flex: 0 0 auto; padding: 2px 8px; min-width: 0; }
  .scorebug-inning-num { font-size: 22px; }
  .scorebug-inning-num[data-digits="4"] { font-size: 17px; }
  .scorebug-inning-num[data-digits="5"] { font-size: 13px; }
  .scorebug-inning-num[data-digits="6"] { font-size: 10px; }

  .scorebug-count { flex: 0 0 auto; min-width: 0; padding: 3px 6px; }
  .scorebug-count-bs { font-size: 10px; }
  .outs-dots { gap: 3px; }
  .dot { width: 6px; height: 6px; }

  .scorebug-bases { flex: 0 0 auto; padding: 3px 6px; }
  .diamond { width: 17px; height: 17px; }
  .base { width: 7px; height: 7px; }
  /* same corner-placement fix as the desktop rule above */
  .base-second { top: 0; left: 0; }
  .base-first { top: 0; right: 0; }
  .base-third { bottom: 0; left: 0; }

  /* Day/league strip and the At-bat/pitching sub-panel aren't in the
     required compact row and the latter is the biggest overflow risk
     (arbitrary-length player names) — drop both. */
  .scorebug-strip { display: none; }
  .matchup-block { display: none; }

  .bottom-block { gap: 6px; }

  .captions { font-size: 13px; min-height: 32px; }
  .caption-line { line-height: 1.3; }

  /* Stats pane is a secondary/detail view — no room on a compact phone
     layout (same call as matchup-block above); still reachable on wider
     viewports where it doesn't compete with the core scoreboard. */
  .stats-panel { display: none; }
}
