/* Seven-segment display face for the scoreboard (DSEG7 Classic, SIL OFL). */
@font-face {
  font-family: "DSEG7 Classic";
  src: url("fonts/DSEG7Classic-Bold.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
/* The hand the scoresheet is kept in (Architects Daughter, Kimberly Geswein,
   SIL OFL). IT WAS CAVEAT BOLD UNTIL s26 (Brian: "wire Architects Daughter").
   Caveat is a MARKER — thick, wet, and the same width whichever way the stroke
   travels; this is a drafting hand, even pressure and open counters, which is
   what actually reads as pencil. It is also narrower, and a scoresheet is mostly
   `6-3` and `4-6-3`, so the digits are the characters that matter here.
   SUBSET TO WHAT A CARD CAN WRITE — the digits, the two dozen letters the codes
   use, the hyphen and the parentheses — 22 KB of TTF down to 3.8 KB of woff2,
   half what Caveat's cut cost. THE COVERAGE WAS MATCHED TO THE OLD SUBSET
   CODEPOINT FOR CODEPOINT, not re-chosen, so nothing the card can print lost a
   glyph in the swap: 38 in, 38 out, including the drawn U+A4D8.
   `swap`, not `block`: the marks are legible in the fallback and a card that
   renders instantly in the app's own face and then turns into handwriting is
   better than a blank grid while a font loads. */
@font-face {
  font-family: "Architects Daughter";
  src: url("fonts/ArchitectsDaughter-subset.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

:root {
  --bg: #0e1a12;
  --panel: #16241b;
  --panel-2: #1d2f23;
  --line: #2c4433;
  --text: #e8f0e9;
  --muted: #9ab3a0;
  --accent: #f5c542;
  --accent-2: #6fd08c;
  /* The bases a runner reached — the same amber as a run, held back so a full
     diamond is the only mark at full strength, but not so far back that a man on
     second is hard to see (Brian: "the muted yellow is too muted, brighten up a
     bit"). 0.40 -> 0.62. */
  --onbase: rgba(228, 186, 76, 0.62);
  /* The corner a scorer rules in where the half-inning ended — a filled
     triangle, and dark enough to read as a rule rather than as a mark about the
     play (Brian: "make the color darker and make it filled in triangle"). */
  --inn-end: #2c6b42;
  /* A PLAY THAT HAS ALREADY BEEN READ — the third out of a half, sitting under
     the gold line that says which half is starting (Brian, s29). It is `--muted`
     held about 55% of the way back toward `--panel`, which is what makes it read
     as quieter than an ordinary out rather than as a different color. Written
     as a token and re-pointed in the print block below, because an unthemeable
     color is invisible until something changes the theme — the `--panel2` hole
     s27 found in the scoresheet header. */
  --spent: #5f7364;
  --danger: #e07a5f;

  /* THE LOGO'S SIZE AND PLACE, DEFINED ONCE (Brian, s14: same size and same spot
     on every screen). The front door draws it over the dugout wall and every
     other screen draws it in the page header — two elements, so the only way
     they stay identical is for both to read these three numbers rather than
     each carrying its own.
     They are in `vw` because the front-door box IS the viewport width, which is
     what lets the header match a position originally expressed as a percentage
     of the artwork: 3% of 100vw is 3vw, and 3% of the artwork's HEIGHT is
     3% x (100vw / 1.7814) = 1.684vw. */
  --logo-w: 9.9vw;
  --logo-x: 3vw;
  --logo-y: 1.684vw;
  --logo-min: 81px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }

/* A CONTROL MAY NOT BE WIDER THAN WHAT CONTAINS IT.
   A `<select>` takes its intrinsic width from its LONGEST OPTION and will not
   shrink below it, so one long ballpark name pushed the whole document sideways
   on a phone: 445 of scroll against a 375 viewport, with `#set-park` the widest
   thing on the page.
   EVERYTHING ELSE THAT LOOKED LIKE AN OFFENDER WAS DOWNSTREAM — both team
   panels measured 397, the search boxes 380 — because they are 100%/1fr of a
   containing block the overflow itself had widened. Measured: capping the
   selects alone takes the page to exactly 375 and puts the widest element back
   inside the viewport at 351. The `.teams` grid already stacked at 800px and
   was never the problem, which is the trap — the visible symptom was two
   panels running off the screen.
   The cap sits on the CONTROL rather than inside a breakpoint, because the rule
   holds at every width; `min-width: 0` is what lets a flex item honor it. */
select { max-width: 100%; min-width: 0; }
.era-toggle, .search-box, .search-box input { max-width: 100%; min-width: 0; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  min-height: 100vh;
  padding: 1.5rem;
}

/* LOGO UPPER LEFT (Brian, s12). The mark led a centered stack before, which
   reads as a title card; off to the left with the tagline beside it, it reads
   as a place you have arrived at. */
/* FULL BLEED, so the logo can sit where the front door puts it. The header used
   to be a centered 1100px block, which placed the mark ~90px from the viewport
   edge on a 1280 screen against the front door's 38px — the same logo in two
   different places depending on which screen you were on. */
.home-header {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  margin-top: -1.5rem;                /* cancel the body padding, as #front does */
  margin-bottom: 1.1rem;
  padding: var(--logo-y) 0 0 var(--logo-x);
  display: flex; align-items: flex-start; gap: 0.85rem;
}
.home-logo { width: var(--logo-w); min-width: var(--logo-min); height: auto;
  flex: 0 0 auto; display: block; }
/* The mark IS the button, so the button draws nothing of its own. */
.home-logo-btn {
  background: none; border: 0; padding: 0; cursor: pointer;
  flex: 0 0 auto; line-height: 0; border-radius: 10px;
  transition: filter 0.15s, transform 0.12s;
}
.home-logo-btn:hover { filter: brightness(1.12); transform: translateY(-1px); }
.home-logo-btn:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; }
#btn-home[hidden] { display: none; }

/* ---- The front door ----
   The whole scene — logo, Harry Wright, his welcome, the dugout — is ONE
   background image, so nothing here draws them and the page header is hidden
   while this screen is up. The four ways in sit on the empty stretch of wall to
   the right of him, which is what that empty stretch is for.

   `aspect-ratio` pins the box to the artwork so the cards stay on the wall as
   the window changes width; without it the image would crop and they would
   drift onto Wright's face. */
/* THE BANNER (Brian, s21) — 2560x320, 8:1, anchored LEFT because everything in
   the image lives in its left 40% and `cover` crops the sides on a narrow
   viewport. Full bleed to the window edges, which is what "runs along the top"
   means; the content column underneath keeps its own margins.
   The old dugout scene's rules are parked below this block. */
#front {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  margin-top: -1.5rem;
  margin-bottom: 1.4rem;
  /* THE STRIP IS A PHOTOGRAPH NOW (Brian, s26: "replace homepagebanner.png with
     homepagebanner2.png"). The whole 2560x320 is a ballpark with the same lockup
     composited into its left 16.8%, which reverses the two decisions under this
     banner and it is worth saying why rather than just what.
     `cover`, NOT `contain` — AND THE OLD NOTE HERE ARGUED THE OPPOSITE. Its
     reasoning was sound and its premise is now false: `contain` letterboxed the
     art against `--bg` and that was invisible ONLY because the flat green in the
     file was `--bg` exactly (measured: #0e1a12). A photograph shares no edge
     color with anything, so the same `contain` would draw a hard green band
     down the right of a picture. `cover` fills the strip at every width.
     THE FILE IS THE WHOLE CANVAS AGAIN, not the 405x249 lockup crop. That crop
     existed to stop `contain` from fitting 85% emptiness; under `cover` the
     emptiness is the picture and cropping to the mark would throw the ballpark
     away.
     WHAT DOES NOT COME BACK IS THE 8:1 `aspect-ratio`, which is the bug s25
     actually fixed — it tied the height to `width / 8` and gave a 375px phone a
     47px strip. The height is still sized directly, so BELOW 880px the floor
     holds the picture at 99px and `cover` crops it from the right, where there
     is only outfield; at 880px and up `11.25vw` IS `width / 8.889` and the file
     is shown whole, uncropped, to its native 288px.
     THE SHIPPED FILE IS 2560x288, A 10% CROP OF THE 2560x320 MASTER (Brian, s26:
     "narrow the height of the header by 10%") — 16 rows off the top, 16 off the
     bottom. THE CROP IS IN THE FILE ON PURPOSE. Taking the 10% off `--fd-h`
     instead looks identical, because `cover` would drop those same rows itself,
     but it breaks the tie below — and the tie is what makes every measurement on
     this banner one expression. `assets/homepagebanner.png` stays 2560x320: it is
     the master, not the deliverable.
     THE TIE: `cover` scales this art by exactly `--fd-h / 288` at EVERY width.
     Below 880px the height floor drives the scale (99/288); above it the width
     does (W/2560) — and `11.25vw` is what makes those the same number, so they
     cross at 880px with no step. One scale is why the lockup's box, the click
     target and the nav's clearance are each `--fd-h` times a constant and cannot
     drift from the picture.
     THE FLOOR IS 99px BECAUSE OF THE LOCKUP, not the picture. The mark measures
     383 x --fd-h / 288, so 99px holds it at 132px across on a phone — what the
     405x249 crop gave at 84px, and the whole point of s25's re-cut. The floor and
     the vw term were cut by 10% TOGETHER; cutting one alone would have moved the
     crossover off 880px and put a kink in the middle of the range. */
  /* NAMED, because `.fd-nav` has to reserve the lockup's width. It is the MARK's
     right edge, not the file's — the art is full-bleed now, so reserving the
     picture would reserve the entire strip. */
  --fd-h: clamp(99px, 11.25vw, 288px);
  --fd-art-w: calc(var(--fd-h) * 430 / 288);
  height: var(--fd-h);
  /* NO PADDING: THE FILE CARRIES ITS OWN INSET AGAIN. The 1.5rem here replaced
     the 47px of empty background the 2560-wide canvas used to hold to the left
     of the lockup, which the crop to 405x249 had taken away. The canvas is back,
     so the margin is back — 16px on a phone, about 26px at 1440 — and a padded
     `background-origin: padding-box` would now inset the PHOTOGRAPH, leaving a
     bare strip of `--bg` down the left edge. `.fd-logo-btn` loses the same
     1.5rem, or the click target and the picture part company. */
  background: url("img/banner.webp") left center / cover no-repeat var(--bg);
  position: relative;
  display: block;
}
/* The hotspot sits over the wordmark rather than spanning the strip — an
   invisible full-width button is a click target nobody aimed at. */
.fd-logo-btn {
  /* DEAD RULE, KEPT WITH ITS BLOCK. The parked-scene section further down
     re-declares `.fd-logo-btn` and wins on order, so the size that applies is
     the one there — see the note beside it. */
  position: absolute; left: 0; top: 0; width: 26%; height: 100%;
  background: none; border: 0; padding: 0; cursor: pointer; border-radius: 0;
}
.fd-logo-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
/* PARKED WITH THE SCENE — the 2000:940 box, the image-set, the container-query
   context and the width cap, plus the logo's three `cqw` variables. Restore this
   block and the markup in index.html together; they are one thing.
   width: min(100vw, 880px); aspect-ratio: 2000 / 940.8;
   background-image: image-set(url("img/homebg4.webp") 1x, url("img/homebg4@2x.webp") 2x);
   container-type: inline-size;
   .fd-logo-btn { --logo-w: 9.9cqw; --logo-x: 3cqw; --logo-y: 1.684cqw; } */
#front[hidden] { display: none; }
.fd-cards { position: absolute; inset: 0; }

/* THE THREE SIGNS. Every number here was MEASURED off the LOSSLESS SOURCE by
   scanning it for the boards' yellow, not eyeballed — the band runs y 12.18%
   with a height of 14.04%, and the three boards start at 20.88 / 43.07 / 65.23
   percent, each 16.54 wide, at a pitch of 22.18 / 22.16.
   The button draws nothing of its own: the board is in the photograph, so this
   is type on top of it and a hover that lifts the board slightly.

   RE-MEASURE ON THE LOSSLESS SOURCE, NOT ON THE SHIPPED WEBP — lossy encoding
   smears the dark bezel into the yellow and reads every board 0.46% narrower
   and further right, which is indistinguishable from the artwork having moved.

   AND VALIDATE THE INSTRUMENT ON A RECORDED ANSWER FIRST (s17). The scan that
   produced these numbers was run against the s15 source until it reproduced the
   s15 answer — 18.82 / 36.66 / 54.47 / 72.28 against a recorded 18.87 / 36.68 /
   54.52 / 72.33, within 0.12%, pitch exact. Two defects had to be fixed to get
   there and both read as real geometry: measuring the yellow PANEL rather than
   the board (the bezel is 0.6% of the width), and a specks filter throwing away
   each board's thin outer rim, which sits outside a dark pinstripe. A board's
   pinstripe gap is 0.0105 of its width and the tightest gap BETWEEN boards is
   0.0885, so the merge threshold is 0.04 — sized against a board splitting in
   three and against every board merging into one. */
.fd-sign {
  position: absolute;
  /* 12.18% and 14.04% of the uncropped scene — see the crop note on `#front`. */
  top: 4.98%;
  height: 16.71%;
  background: transparent;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  text-align: center;
  padding: 0 1.2%;
  /* SIZED OFF THE BOX, NOT THE VIEWPORT (Brian, s21). This was `1.5vw` on the
     stated reasoning that "`#front` IS the viewport width" — true until the
     scene was capped at 880px to make it a band, at which point the type went on
     scaling with a 1280px window while the boards it sits on had stopped. The
     middle sign's three lines promptly overflowed its board top and bottom.
     `cqw` is the same number against the right thing: `#front` is a size
     container (see its rule), so 1.5cqw is 1.5% of the SCENE however wide the
     scene happens to be — which is what the original comment meant. Clamped at
     both ends still: below the floor it is unreadable, above the ceiling it
     outgrows the sign. */
  font-size: clamp(0.6rem, 1.5cqw, 1.35rem);
  /* CLARENDON, the slab serif old ballpark signage was actually lettered in.
     It is a system font rather than a webfont — present on macOS, absent on most
     other machines — so the stack falls through other slabs before it lands on
     a plain serif. If it has to be guaranteed everywhere, that means bundling a
     woff2 the way the scoreboard's DSEG7 already is. */
  font-family: Clarendon, "Clarendon BT", "Bookman Old Style", Rockwell,
    "Roboto Slab", Georgia, serif;
  font-weight: 700;
  line-height: 1.1;
  flex-direction: column;
  /* Dark on the sign's yellow — the one place in the app that is not on panel. */
  color: #1c1a0e;
  transition: transform 0.12s, background 0.15s;
}
.fd-sign:nth-child(1) { left: 20.88%; width: 16.54%; }
.fd-sign:nth-child(2) { left: 43.07%; width: 16.54%; }
.fd-sign:nth-child(3) { left: 65.23%; width: 16.54%; }
.fd-sign:hover { background: rgba(255, 255, 255, 0.24); transform: scale(1.03); }
.fd-sign:focus-visible { outline: 2px solid #1c1a0e; outline-offset: -4px; }
/* WHICH SIGN YOU ARE UNDER. The signs stay hung once you pick one (s17), so
   without this all three read as unpressed on every screen. A ring on the board
   rather than a fill, because the board is a photograph and tinting it reads as
   the artwork being wrong rather than as a control being active. */
.fd-sign[aria-pressed="true"] {
  box-shadow: 0 0 0 3px #1c1a0e, 0 0 0 6px rgba(255, 255, 255, 0.5);
}

/* ---- THE THREE BENCH COACHES, ON THE BENCH (Brian, s17) ------------------
   Transparent hotspots over the three seated men, measured off the same source
   as the signs: Pops 35.5–47.0, Coach 50.0–61.2, Professor 64.5–77.5 percent of
   the width, all of them y 42–89. The gutters between them are ~3%, so a
   pointer between two men hits neither rather than the wrong one.
   WRIGHT IS NOT ONE OF THEM — the man on the left in the top hat is the host
   who says the welcome, and giving him a hotspot would offer a fourth coach
   who does not exist. */
.fd-coaches { position: absolute; inset: 0; pointer-events: none; }
/* 42% / 47% of the uncropped scene — see the crop note on `#front`. */
.fd-coach { position: absolute; top: 40.48%; height: 55.95%; pointer-events: auto; }
.fd-coach:nth-child(1) { left: 35.5%; width: 11.5%; }
.fd-coach:nth-child(2) { left: 50.0%; width: 11.2%; }
.fd-coach:nth-child(3) { left: 64.5%; width: 13.0%; }
.fd-coach-hit {
  position: absolute; inset: 0;
  background: transparent; border: 0; border-radius: 10px;
  cursor: pointer;
  transition: background 0.15s, box-shadow 0.15s;
}
/* NO BOX ON THE MAN (Brian, s17). The selected coach used to carry a bright
   ringed rectangle and a tint, which fenced a painted figure inside a piece of
   UI and looked like a crop mark rather than a choice. THE ROLLOVER IS THE
   AFFORDANCE now: hovering opens his panel, and the man you have picked is the
   one whose panel stays open and says "your bench coach".
   `:focus-visible` KEEPS its outline — that is the keyboard's only cue, and it
   appears solely when tabbing, so it never shows during ordinary pointer use. */
.fd-coach-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* HARRY WRIGHT — informational, never selectable. Measured the same way as the
   three coaches: he runs x 24.5-34.0% and y 43-90% of the scene, leaving a
   1.5% gutter before Pops at 35.5% so a pointer between them hits neither.
   MEASURED OFF THE SHIPPED WEBP, not the lossless source, which the standing
   rule warns against — but that rule is about strict color-keying of the SIGN
   EDGES, where the encoder erodes about 0.46%. This is a generous box over a
   painted man, where half a percent is nothing. (The source export has since
   left `assets/`, which is gitignored.)
   His panel is wider than a coach's because his blurb is longer, and centered on
   him by the same (100 - width) / 2 rule. */
/* 43% / 47% of the uncropped scene — see the crop note on `#front`. */
.fd-host { position: absolute; left: 24.5%; top: 41.67%; width: 9.5%; height: 55.95%; }
.fd-host .fd-coach-panel { left: -55%; width: 210%; }
.fd-host:hover .fd-coach-panel,
.fd-host:focus .fd-coach-panel,
.fd-host:focus-within .fd-coach-panel { opacity: 1; visibility: visible; }
.fd-host:hover, .fd-host:focus-within { z-index: 3; }
.fd-host:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 10px; }
.fd-host .sk-name { color: var(--accent); }
.fd-host .sk-name em { font-style: normal; color: var(--muted); font-weight: 400; font-size: 0.82em; }

/* THE PANEL OPENS ON ROLLOVER, and stays open for the man you picked — which is
   the only way to see who that is once the pointer has left. Hover is NEVER the
   only way in: `:focus-within` covers the keyboard, and the button carries the
   whole blurb as screen-reader text, so nothing here is reachable by pointer
   alone. It hangs off the BOTTOM of the figure so it covers his legs and the
   floor clutter rather than his face. */
.fd-coach-panel {
  position: absolute;
  /* NARROWER AND A TAD HIGHER (Brian, s17): 190% -> 150% of the figure and
     62% -> 54% down him, which lifts it off the floor clutter and onto his
     legs, and stops one man's panel reaching across two of his neighbors.
     `left` is derived from the width so the panel stays centered on the man it
     belongs to — (100 - 150) / 2 — rather than being a second number to keep
     in step by hand. */
  left: -25%; top: 54%; width: 150%;
  display: flex; flex-direction: column; gap: 0.18rem;
  padding: 0.5rem 0.6rem;
  background: rgba(11, 18, 12, 0.93);
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  opacity: 0; visibility: hidden;
  transition: opacity 0.14s;
  /* Above its neighbors: the panels are wider than the men, so they overlap. */
  z-index: 2;
}
/* HOVER AND FOCUS ONLY — the SELECTED man's panel no longer sits open (Brian,
   s17). With the ring gone too, nothing is drawn over the artwork until you
   reach for it, which is the point: the scene is a photograph, not a form.
   WHICH MAN IS PICKED THEREFORE SHOWS NOWHERE ON THE SCENE, so it is named in
   the "Who's your bench coach?" row below instead (see `renderCoaches`) — a
   choice with no visible answer anywhere would be worse than the box was. */
.fd-coach:hover .fd-coach-panel,
.fd-coach:focus-within .fd-coach-panel { opacity: 1; visibility: visible; }
/* The hovered one wins over the merely selected one, or reading about a coach
   you have not picked puts his panel behind the one you have. */
.fd-coach:hover, .fd-coach:focus-within { z-index: 3; }
.fd-coach-panel .sk-name { color: var(--accent); }
.fd-coach.on .fd-coach-panel .sk-name::after {
  content: " — your bench coach"; color: var(--muted); font-weight: 400;
  font-size: 0.7rem;
}

/* Top left of the screen, over the dugout wall, sized as a share of the scene
   so it holds its place as the window changes width. */
/* Same three numbers as the header's (see `--logo-*`), so the mark does not move
   or resize when you leave the front door. 9.9vw is 40% smaller than the original
   15%, then 10% back — both of those were Brian's, in that order. */
/* THE MARK IS A BUTTON NOW (s17) — it clears the bottom back to the landing
   state, the way the header logo does everywhere else. The positioning moved to
   the button so the image inside it stays an ordinary block; putting both in
   the same absolute box is what would let the two disagree about the size. */
.fd-logo-btn {
  position: absolute;
  top: var(--logo-y); left: var(--logo-x);
  width: var(--logo-w); min-width: var(--logo-min);
  padding: 0; border: 0; background: none; cursor: pointer;
  line-height: 0;
  border-radius: 8px;
}
/* THE SCENE'S COPY OF THE LOGO RIDES THE SCENE (Brian, s21), for the same
   reason the sign lettering does: the three shared variables are in `vw` on the
   stated reasoning that "the front-door box IS the viewport width", and the
   width cap made that false. The HEADER's copy keeps the `vw` values untouched —
   it is drawn on the page, where they are still the right measure — so the two
   still agree wherever the scene is full width, which is every viewport under
   880px. Declared on the element rather than on `#front`, because `cqw` in a
   declaration ON a container queries its ANCESTOR container, not itself. */
/* THE LOGO STANDS ALONE (s21). It was absolutely positioned as a percentage of
   the artwork; with the artwork parked there is nothing to position against, so
   it is a plain block at a fixed size. The `cqw` values are kept in the comment
   because they belong with the scene and come back with it.
   PARKED: --logo-w: 9.9cqw; --logo-x: 3cqw; --logo-y: 1.684cqw; */
/* THE RULE THAT ACTUALLY APPLIES — it re-declares the block above and wins on
   order. 18% of the strip, not a fixed 132px (s23): the banner is at 100% now
   and the mark scales with it, so a pixel width covered the badge and stopped
   short of the wordmark at any viewport wider than about 730px. The floor keeps
   it aimable on a phone. */
/* THE HOTSPOT IS THE ART (s25). It was 18% of the STRIP, which tracked the old
   2560x320 canvas — and once the banner is sized by height instead, the art's
   share of the width changes with the viewport (36% of a phone, 20% of a
   desktop), so a fixed percentage covered half the mark on a phone and
   overshot it on a wide screen. Given the art's own ratio and the box's full
   height it covers exactly the lockup at every size, because it is now
   describing the same rectangle the background paints. */
/* s26: THE INSET MOVED BACK INTO THE FILE, so it comes off here too — `#front`
   dropped its `padding-left` when the art went full-bleed, and a target still
   held 1.5rem right of the background would sit 24px off the mark. The offset
   the lockup is drawn at is 47/2560 of the strip, which the picture provides.
   AND `height: 100%` GOES WITH IT. That was right while the FILE was the lockup:
   `contain` fitted the crop by height, so the mark did fill the strip top to
   bottom and one ratio placed the whole target. Inside the photograph the mark
   occupies y 28..254 of 288 — 78% of the height — so a full-height target would
   now reach up into the empty sky above it. All four numbers come off the
   measured box (47..430 across, 28..254 down, in the SHIPPED 2560x288 crop — the
   master's 44..270 less the 16 rows that crop took off the top).
   AGAINST `--fd-h`, because `cover` scales this art by exactly `--fd-h / 288` at
   every width the banner is drawn at: below 880px the height floor drives the
   scale, above it `11.25vw` is `width / 8.889`, and the two agree. It drifts
   only past a 2560px viewport, where the art is upscaled and centered and the
   target sits low by half the overflow — beyond any screen this runs on. */
.fd-logo-btn {
  position: absolute;
  left: calc(var(--fd-h) * 47 / 288);
  top: calc(var(--fd-h) * 28 / 288);
  width: calc(var(--fd-h) * 383 / 288);
  height: calc(var(--fd-h) * 226 / 288);
  min-width: 0; margin: 0;
  /* A BUTTON ON index.html AND AN ANCHOR ON THE DOC PAGES (s33). Those load no
     main.js, so the hotspot cannot be a router button there and is a plain link
     to index.html — and an <a> is inline, which would collapse a box whose whole
     job is to be a measured rectangle. Stated here rather than in a second rule
     so the two elements cannot be sized apart. */
  display: block;
}
.fd-logo-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.fd-logo {
  width: 100%;
  height: auto;
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.55));
}

/* The front door carries its own logo, so the page header would be a second
   one — it stands down here and comes back for every other screen. */
body.on-front .home-header { display: none; }

/* THE NAV, ON THE BANNER (Brian, s25). Bottom right of `#front`, which is
   already `position: relative` and whose artwork sits hard LEFT — so this is
   the empty half of the strip rather than something laid over the art.
   ABSOLUTE ON PURPOSE: it adds no layout, so the banner's aspect ratio and
   every measured hotspot percentage in it are untouched. */
.fd-nav {
  position: absolute;
  left: 0; right: 0;
  /* LOWER ON THE STRIP (Brian, s33: "move the nav down about 5-10 pixels"). The
     links sat 9px off the bottom edge of the photograph; this puts them at 2,
     which is 7px down — the middle of what he asked for and as far as the type
     can go. The box is `align-items: center` over a 25px line box, so the ink
     stops well short of the edge even though the box nearly reaches it. */
  bottom: 0.1rem;
  /* RIGHT-ALIGNED WITH THE CONTENT COLUMN, not with the viewport (Brian, s25:
     "right align with 'the game' line"). The banner is full-bleed 100vw while
     every section under it — `.sec-rule`, `#play-row`, `#settings-bar` — is a
     1100px block centered inside the body's 1.5rem padding. Reproducing that box
     here and pushing the links to its end is what puts the nav's right edge on
     the same line as THE GAME's rule, at every width: 1100 while it fits, and
     the padded full width once it does not.
     `min(...)` rather than a media query because the column itself has none —
     it is one expression in `.sec-rule` and it is one expression here. */
  width: min(1100px, 100% - 3rem);
  margin-inline: auto;
  /* KEEP OFF THE LOCKUP. Right-alignment alone left this to chance: at 375px the
     first link cleared the tagline by two pixels and at 320px it sat on top of
     it, because the links are sized by their own text while the art is sized by
     the banner's height. Reserving `--fd-art-w` (plus the inset it is drawn at,
     plus a gap) makes the clearance structural — the nav wraps to a second line
     on a small phone instead of crossing the mark.
     THE 1.5rem CAME OFF WITH `#front`'s PADDING (s26). `--fd-art-w` is measured
     from the strip's own left edge now — it is the lockup's RIGHT edge inside
     the full-bleed photograph, 430/320 of the height — so the inset it used to
     be added to is already inside it. Leaving it would reserve 24px twice. */
  padding-left: calc(var(--fd-art-w) + 1rem);
  display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap;
  justify-content: flex-end;
  font-size: 0.86rem;
  /* Above the logo hotspot, which spans its own corner of the strip. */
  z-index: 2;
}
.fd-nav a {
  color: var(--accent-2); text-decoration: none; font-weight: 600;
  padding: 0.15rem 0; border-bottom: 1px solid transparent;
  /* THE ART CHANGED, AND THIS IS WHY THE SHADOW WAS ALREADY HERE. It read "a
     photograph at its left end and flat `--bg` at this one, but the art is free
     to change" — s26 changed it, and the links now sit over open ballpark for
     their whole width, so the shadow is doing real work rather than insuring
     against a future. No plate behind them: a filled chip on a photograph reads
     as a button, and these are links.

     RE-MEASURED EACH TIME THE BANNER IS REPLACED, because the figure is a claim
     about a photograph and two of them have shipped since it was written. Under
     the links exactly (x .731-.98, y .793-.946 of the strip, at 1440): s26's art
     was mean luminance 48; banner3 was 95, which is the worst this has been;
     banner4 is 66, contrast 5.3:1 against the link green at the mean and 1.6:1
     against its 95th percentile. The p95 is the number that matters and it is
     why the shadow stays whatever the art does. */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}
.fd-nav a:hover { border-bottom-color: var(--accent-2); }
/* THE PAGE YOU ARE ALREADY ON STATES ITSELF and does not offer to take you
   there — the rule the format pills and the old `.doc-nav` both followed, moved
   here with the nav when the banner went onto the doc pages (s33). Only those
   pages ever set it: index.html's nav lists the two documents and you are on
   neither. The border is `--line` rather than transparent so the current page
   still reads as a labeled item and not as a link that lost its underline. */
.fd-nav a.on { color: var(--text); border-bottom-color: var(--line); cursor: default; }
@media (max-width: 620px) {
  /* HIGHER HERE THAN ON THE DESKTOP, which is the opposite of how these two
     used to sit and is deliberate (s33). The desktop nav dropped to 0.1rem when
     Brian asked for it lower; down here the links WRAP to two lines against the
     lockup's reserved width, so the box is twice as tall and the same offset
     would push the second line into the bottom edge of the photograph. Measured
     at 375: two lines, 9px of clearance under the last one. */
  .fd-nav { font-size: 0.76rem; gap: 0.7rem; bottom: 0.35rem; }
}

/* Narrow: the scene is too small to hold readable cards on top of it, so it
   becomes a banner and the ways in move underneath. Cropped to the left half,
   which is where Wright and the logo are — the empty wall is what we were
   using the right side FOR, and it has nothing to look at. */
@media (max-width: 860px) {
  /* THE BANNER, NOT THE DUGOUT SCENE (Brian, s25: "iPad and iphone still have
     old header image of the coaches in the dugout").
     THE SCENE WAS PARKED IN ONE PLACE AND LIVE IN THE OTHER. s23 replaced it
     with `banner.webp` and the desktop rule was commented out with the markup —
     but this branch painted `homebg4.webp` through a `::before`, so every phone
     and every iPad went on showing the retired artwork while every desktop
     showed the new one. The same shape this file records twice over: a thing
     fixed in one builder and not in its sibling.
     NOTHING TO OVERRIDE NOW. `#front` keeps the desktop background, sizing and
     fit at every width, which is also the only way the two can no longer
     disagree — there is one declaration of the banner and this block does not
     mention it. That still holds after s26 swapped the art for a photograph and
     `contain` for `cover`: the change was one line up there and none down here,
     which is the whole return on there being a single declaration.
     THE ART WAS RE-CUT (Brian, s25: "re-export banner.webp"), which is what
     this note used to send the reader to PLAN-home §9 for. The strip was thin on
     a phone because `contain` was fitting a 2560x320 CANVAS that was 85% empty;
     s26 put the full canvas back, but the height stayed clamped rather than a
     ratio of the width — which is the half of that fix that mattered — so there
     is still nothing here to compensate for.
     AND SO THE `#front` OVERRIDE IS GONE. It set `flex-direction` on a
     `display: block` box (inert), re-stated the base `margin-top` (identical),
     and zeroed the padding for the parked scene's full-bleed cards — which by
     s25 meant a phone dropped the banner's left inset while `.fd-logo-btn` kept
     it, so the click target sat 24px right of the mark it is supposed to be. */
  /* A 16%-wide board on a phone is about fifty pixels across, so the signs stop
     being signs and go back to being buttons under the scene. The artwork keeps
     its blank boards, which is fine — they are part of the dugout. */
  .fd-cards { position: static; display: grid; gap: 0.5rem;
    width: calc(100% - 3rem); margin: 0 auto; }
  /* `:nth-child(n)` MATCHES ALL THREE AND IS HERE FOR ITS SPECIFICITY, not to
     select anything a bare `.fd-sign` would miss. The board positions above
     are `.fd-sign:nth-child(1..3)` (0,2,0) and this block is `.fd-sign` (0,1,0) —
     and A MEDIA QUERY ADDS NO SPECIFICITY, so the desktop `width: 16.54%` beat
     the `width: auto` below it no matter that it comes later in the file. The
     phone shipped s14 with 54px-wide buttons in a left-hand column: 16.42% of the
     327px card box, which is precisely the "about fifty pixels across" the comment
     above gives as the reason this block has to exist. It never took effect.
     GENERAL SHAPE: an override inside a media query has to out-specify the rule
     it is overriding, not merely follow it. */
  .fd-sign:nth-child(n) {
    position: static; left: auto; width: auto; height: auto;
    background: rgba(16, 22, 17, 0.93);
    border: 1px solid var(--line); border-radius: 12px;
    padding: 0.8rem 0.9rem;
    font-size: 1rem; color: var(--accent);
  }
  .fd-sign:hover { background: rgba(24, 34, 25, 0.96); transform: none;
    border-color: var(--accent); }
  /* PARKED WITH THE SCENE. This moved the mark out of the artwork and above it,
     which was the answer while the dugout painting was the banner. The banner is
     a lockup on a strip now and the hotspot tracks it by ratio at every width —
     see `.fd-logo-btn` above — so overriding it here would put the target back
     where the art is not.
     .fd-logo-btn { position: static; width: 34%; min-width: 132px;
                    display: block; margin: -0.2rem 0 0.55rem 1.5rem; } */
  /* THE COACHES COME OFF THE PICTURE TOO. A hotspot over a painted figure needs
     the figure to be big enough to aim at, and at 375px the three men are about
     forty pixels wide each — so they become three ordinary cards under the
     scene, with their blurbs always showing. Same reasoning as the signs one
     block up, and the same specificity trap: the desktop positions are
     `.fd-coach:nth-child(1..3)` at (0,2,0), so these overrides are written
     `:nth-child(n)` to tie on specificity and win on source order. A MEDIA
     QUERY ADDS NO SPECIFICITY. */
  .fd-coaches {
    position: static; inset: auto; pointer-events: auto;
    display: grid; gap: 0.5rem;
    width: calc(100% - 3rem); margin: 0.75rem auto 0;
  }
  .fd-coach:nth-child(n) {
    position: relative; left: auto; top: auto; width: auto; height: auto;
  }
  .fd-coach-hit {
    /* Stretched over the whole card, so the card IS the target — the desktop
       button has the figure to cover and here it has the panel. */
    inset: 0; border-radius: 12px;
  }
  .fd-coach-panel {
    position: static; left: auto; top: auto; width: auto;
    opacity: 1; visibility: visible;
    padding: 0.7rem 0.85rem;
  }
  .fd-coach.on .fd-coach-panel { border-color: var(--accent); }
  /* Wright joins the cards under the scene, panel open, and stops being a
     hotspot over a forty-pixel-wide figure. */
  .fd-host {
    position: static; left: auto; top: auto; width: calc(100% - 3rem); height: auto;
    margin: 0.5rem auto 0; display: block;
  }
  .fd-host .fd-coach-panel {
    position: static; left: auto; top: auto; width: auto;
    opacity: 1; visibility: visible; padding: 0.7rem 0.85rem;
  }
}


/* ---- Who's your bench coach? ----------------------------------------------
   THE CARDS LEFT THIS SECTION IN s17. The three men are hotspots on the dugout
   scene now (`.fd-coach` above), so what is left here is the question and the
   one answer that is not a man — you take both chairs. `.sk-card` / `.sk-shot`
   / `.sk-text` / `.sk-tag` went with them; `.sk-name` and `.sk-blurb` stayed,
   because the scene's panels render exactly those two.
   The three portrait jpgs in img/ are unreferenced as a result. They are left
   in place rather than deleted unasked — same posture as the retired
   homebg/homebg2 files — and `img` is still on the coach data for whatever the
   bench-coach work wants to do with a face. */
/* The gap ABOVE this row is `#park-bar`'s bottom margin — one rhythm for the
   three setup lines, see there. The gap BELOW is its own and is larger on
   purpose: the line above is the last of the setup and the panels below are the
   next thing, so this boundary is where the page changes subject. */
#skippers { max-width: 1100px; margin: 0 auto 1.3rem; padding: 0 0.2rem;
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
#skippers[hidden] { display: none; }
.sk-hint { color: var(--muted); font-size: 0.74rem; }

/* THE FOUR ANSWERS, WITH FACES (Brian, s21). Cards rather than pills, because
   three of them are CHARACTERS: the portraits and the bios both already existed
   (see `renderSkippers`), and the dugout scene that used to introduce these men
   is parked.
   THE MAN ON THE LEFT AND HIS PITCH ON THE RIGHT. Stacked, each card was as tall
   as a portrait plus four lines of bio; side by side the same content is half
   the height, and the row of four stops dominating the block it sits in. */
.sk-picks {
  flex-basis: 100%;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  margin-top: 0.4rem;
}
@media (min-width: 1180px) { .sk-picks { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .sk-picks { grid-template-columns: 1fr; } }
.sk-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.55rem;
  align-items: start;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 0.6rem 0.7rem;
  cursor: pointer;
  color: var(--text);
  transition: border-color 0.15s, background 0.15s;
}
.sk-card:hover { border-color: var(--accent-2); }
/* THE CHOSEN ONE IS RINGED, NOT FILLED — the opposite of `.fmt-btn.on` and the
   other pill rows, and deliberately: a filled card would tint a photograph,
   which s17 already recorded as reading like the artwork is wrong rather than
   like a control is active. */
.sk-card.on {
  border-color: var(--accent-2);
  box-shadow: inset 0 0 0 1px var(--accent-2);
  background: var(--panel-2);
}
.sk-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sk-card-who {
  display: flex; flex-direction: column; align-items: center;
  gap: 0.2rem; width: 54px;
}
/* The portraits are 307x420, so a square box needs `cover` and a top-weighted
   position — centering a tall portrait in a circle cuts the face off at the chin. */
.sk-face {
  width: 54px; height: 54px; border-radius: 50%; overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; flex: none;
}
/* LIFTED A LITTLE (Brian, s21). The three portraits are painted dark and moody
   and they sat heavy against a dark panel — a touch of brightness and contrast
   separates the faces from the card without repainting anything. */
.sk-face img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 16%;
  display: block; filter: brightness(1.16) contrast(1.04) saturate(1.05);
}
.sk-face-empty { color: var(--muted); display: block; width: 30px; height: 30px; }
.sk-face-empty svg { width: 100%; height: 100%; display: block; }
.sk-card.on .sk-face { border-color: var(--accent-2); }
.sk-card.on .sk-face-empty { color: var(--accent-2); }
.sk-card-name {
  font-weight: 700; font-size: 0.78rem; color: var(--accent);
  text-align: center; line-height: 1.15;
}
.sk-card-copy { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.sk-card-tag {
  font-size: 0.57rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent-2);
}
.sk-card-blurb { font-size: 0.7rem; line-height: 1.38; color: var(--muted); }
/* NOT YET APPLYING. The cards are up before you have taken a dugout so a
   newcomer meets the three men, but the choice does nothing until you do — so
   the block says so by standing back rather than by going away. */
.sk-inert .sk-picks { opacity: 0.72; }
.sk-inert .sk-card.on { box-shadow: none; }

/* Reachable by a screen reader, invisible to everyone else. The coach hotspot
   is a transparent box over a painted figure, so without this it is a button
   with no accessible name at all. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
.sk-name { font-weight: 700; font-size: 0.95rem; }
.sk-blurb { color: var(--muted); font-size: 0.74rem; line-height: 1.35; }

/* The help picker is GONE from the dugout (Brian, s19) and its rules went with
   it — an orphaned selector is the thing that gets reused by mistake, the same
   reason s10 deleted `PA_ENDING_ACTIONS` outright rather than leaving it
   dangling. `settings().info` and every `showPct` branch still work; only the
   control is gone. */
/* LEFT, AND TWO POINTS DOWN (Brian, s26: "left align the footer and make the
   text 2 points smaller"). It was centered, which set the credits apart as a
   colophon; ranged left they line up with every other block on the page and read
   as the last thing in the column rather than as a plaque under it. 0.8rem is
   12.8px, so two points off is 10.8px — written as 0.675rem because everything
   else on this page is sized in rem and one absolute number in the middle of
   them is the one that stops scaling when the rest does. */
/* AND IT LINES UP WITH THE BOXES (Brian, s26: "footer left align with the
   boxes"). Ranging the text left was only half of it: the footer itself spanned
   the full padded width of `#app` — 1232px at a 1280 screen, starting at 24 —
   while every block above it is a 1100px column centered in that padding and
   starting at 90. So the credits began 66px left of everything they sit under,
   which reads as a different page rather than the end of this one.
   THE COLUMN IS RESTATED, NOT INVENTED. `.sec-rule`, `#locker` and `.teams` are
   each `max-width: 1100px; margin-inline: auto`, and `.page-doc .page-footer`
   below has been doing exactly this on About and How to Play all along — the
   base rule was the odd one out, which is why the two pages already agreed with
   their own content and the home page did not. */
.page-footer {
  text-align: left; color: var(--muted); font-size: 0.675rem;
  max-width: 1100px;
  margin: 2.5rem auto 1.5rem; padding-top: 1rem; border-top: 1px solid var(--line);
}
/* A FLOOR ON THE PHONE, NOT A DIFFERENT SIZE (Brian, s26: "floor the footer at
   12px on mobile"). 0.675rem is 10.8px, which is the smallest type in the app
   and readable on a desktop at arm's length; on a phone it is the one thing a
   reader has to work at, and these are the credits two databases are owed.
   12px IS THE FLOOR AND THE DESKTOP KEEPS ITS 10.8 — the size Brian asked for
   holds everywhere it was working, and only the case that was not gets moved.
   `.page-footer` AND NOT ITS PARAGRAPHS: they inherit, so raising the block
   raises the credits with it, and the team-code glossary keeps its own sizes
   (0.78rem rows, 0.9rem summary) which are already above this. */
@media (max-width: 620px) {
  .page-footer { font-size: 0.75rem; }
}

/* ---- THE LOADING SCREEN ----
   Three baseballs bouncing in a wave over a figure that says what is being
   loaded. Sized in rem so it holds together from a phone to a desktop without
   a breakpoint; the balls are the only thing on the page that is deliberately
   off-palette (a real ball is cream and red, and tinting it green would make
   it a token rather than a baseball). */
#loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.1rem;
  min-height: 60vh;
  padding: 2rem 1rem;
  text-align: center;
  color: var(--muted);
}
.ld-balls { display: flex; gap: 0.85rem; }
.ld-ball {
  width: 2.4rem;
  height: 2.4rem;
  /* Two animations on one element: the arc it travels and the spin it carries
     through the arc, which is what stops it reading as a bouncing dot. */
  animation: ld-bounce 1.05s ease-in-out infinite, ld-spin 2.1s linear infinite;
  filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.45));
}
/* The wave. Thirds of the bounce period, so the three are evenly spaced around
   it and the group never lands together. */
.ld-ball:nth-child(2) { animation-delay: -0.35s, -0.7s; }
.ld-ball:nth-child(3) { animation-delay: -0.7s, -1.4s; }
@keyframes ld-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-0.85rem); }
}
@keyframes ld-spin {
  from { rotate: 0deg; }
  to   { rotate: 360deg; }
}
.ld-count {
  font-size: clamp(2.1rem, 9vw, 3.2rem);
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;   /* so the count-up doesn't jitter */
}
.ld-what { margin-top: 0.35rem; font-size: 0.95rem; color: var(--text); }
/* Wide enough that the three facts sit on ONE line on a desktop — at 34ch it
   broke after the second separator and left "1871–2025" orphaned on its own
   row. Still a max-width, so a phone wraps it properly rather than overflowing. */
.ld-sub { margin-top: 0.5rem; font-size: 0.78rem; line-height: 1.5; max-width: 52ch; }

/* Same posture as `.mg-ballgame` below: the motion is decoration, and the
   screen has to work without it. The balls hold still and breathe instead, and
   `paintStatCount` reads this same query to skip the count-up. */
@media (prefers-reduced-motion: reduce) {
  .ld-ball { animation: ld-fade 1.6s ease-in-out infinite; }
  .ld-ball:nth-child(2) { animation-delay: -0.53s; }
  .ld-ball:nth-child(3) { animation-delay: -1.06s; }
  @keyframes ld-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
}

/* THE SCREEN HEADER IS GONE (s21 — PLAN-home.md §6, step 5). It printed the
   screen's name over a page whose format strip states it two lines further
   down, which is the s14 argument that removed its kicker applied to the title.
   `#screen-head`, `.scr-line`, `.scr-kicker`, `.scr-title` and `.scr-sub` went
   with it. */

/* SETTINGS, AS A PLAIN SECTION (Brian, s21). It was a `<details>`; it is headed
   by a `.sec-rule` like the other two groups now and is not folded. It still
   APPEARS only once you have picked a source — see `settingsEarned` — which is
   what keeps it off a blank board without a disclosure to click. */
#settings-bar { max-width: 1100px; margin: 0 auto 1.1rem; }

#settings-bar[hidden] { display: none; }
/* Its own rule leads the section, so it needs no gap above it of its own. */
#settings-bar > .sec-rule { margin-top: 0; }
#settings-bar #options-bar, #settings-bar #park-bar { margin-left: 0; margin-right: 0; }

/* THE PAGE-WIDE MODE SELECTOR IS GONE (s21 — PLAN-home.md §2, step 2). It set
   one mode for both clubs; each panel carries its own `.fill-row` now. `.mt-label`
   survives because `#skippers` and `#format-bar` both use it for their question. */
.mt-label { color: var(--muted); font-size: 0.85rem; font-weight: 600; margin-right: 0.25rem; }

/* WHAT ARE WE PLAYING (s21 — PLAN-home.md §5, step 3). The format, stated where
   the decision is made: under the two clubs. It mirrors the painted signs and
   neither holds the answer — `curScreen` does. Same pill vocabulary as
   `.who-btn` and `.fill-btn.on`, because all three mean "exactly one of these is
   true" and the page should only have one way of saying that. */
#format-bar {
  max-width: 1100px;
  /* A CHANGE OF SUBJECT NEEDS A GAP (Brian, s21). The clubs end and a new
     question begins, and at zero the strip read as the last row of the rosters
     rather than as the next thing being asked. */
  margin: 1.6rem auto 1.1rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}
#format-bar[hidden] { display: none; }
.fmt-btn {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.4rem 1rem;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.fmt-btn:hover { border-color: var(--accent-2); }
.fmt-btn.on {
  background: var(--accent-2); color: #10231a; border-color: var(--accent-2);
  font-weight: 700;
}
.fmt-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* HOW (s21 — PLAN-home.md §5, step 4). One button and one question, on one row.
   `Play ball` leads it because it is the act; "who's managing" follows because
   it is a setting on that act. */
#how-bar {
  max-width: 1100px;
  margin: 0 auto 0.9rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
#how-bar[hidden] { display: none; }
/* ITS OWN ROW, GOLD, AND BIGGER (Brian, s21). It was pill-sized at the end of
   the "who is managing" line — right while it was one item in a row, wrong once
   it became the conclusion of the whole block.
   THE ONLY GOLD BUTTON ON THE PAGE, deliberately: every filled green control
   here means "this is what is selected", and this is not a selection. Gold is
   what happens; green is what is chosen. */
#play-row { max-width: 1100px; margin: 0.2rem auto 1.2rem; }
#play-row[hidden] { display: none; }
#btn-playball {
  background: var(--accent);
  color: #1a1a10;
  border: 1px solid var(--accent);
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  padding: 0.68rem 2.1rem;
  border-radius: 999px;
  cursor: pointer;
  transition: filter 0.15s, transform 0.12s;
}
#btn-playball:hover { filter: brightness(1.08); transform: translateY(-1px); }
#btn-playball:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; }
/* DARK UNTIL THERE ARE TWO CLUBS, FULL GOLD WHEN THERE ARE (Brian, s21). The
   color is the readiness: you can see across the page whether the app is
   waiting on you.
   STILL NOT `disabled`, and that is deliberate — pressing it is how you find out
   WHY, because it answers with the nudge in `playBall`. A `disabled` button
   cannot be clicked and so cannot explain itself, which is the same argument s18
   made when it made `#btn-manage-both` absent rather than dead. */
#btn-playball.not-yet {
  background: #4a4029; border-color: #4a4029; color: #9a8f75;
}
#btn-playball.not-yet:hover { filter: none; transform: none; background: #55492e; }
.who-picks { display: flex; flex-wrap: wrap; gap: 0.35rem; }
/* Same pill as `.fmt-btn` and `.fill-btn`: three rows on this page now mean
   "exactly one of these is true", and they should not each invent a look. */
.who-btn {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 0.32rem 0.85rem;
  cursor: pointer;
  max-width: 16rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.who-btn:hover { border-color: var(--accent-2); }
.who-btn.on {
  background: var(--accent-2); color: #10231a; border-color: var(--accent-2);
  font-weight: 700;
}
.who-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#btn-reroll {
  margin-left: auto;
  background: var(--panel-2);
  color: var(--accent);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.85rem;
  padding: 0.4rem 0.95rem;
  cursor: pointer;
}
#btn-reroll:hover { border-color: var(--accent); }

/* ---- Ready-made clubs. Sits directly under the build tabs because it is the
   same question ("where does this roster come from?") answered a fourth way,
   and it is pulled in a little so it reads as a subordinate row rather than a
   second set of tabs. ---- */
#special-bar {
  max-width: 1100px;
  margin: -0.6rem auto 1rem;
  padding-left: 0.15rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
#special-bar select {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 0.82rem;
  padding: 0.32rem 0.5rem;
  /* 24rem is 384px, which still overflows a 375 phone on its own — and an id
     selector beats the bare `select` cap above, so this had to say both. */
  max-width: min(24rem, 100%);
}
#special-bar select[hidden] { display: none; }
#btn-special {
  background: var(--panel-2);
  color: var(--accent-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 600;
  padding: 0.34rem 0.9rem;
  cursor: pointer;
}
#btn-special:hover { border-color: var(--accent-2); }
#special-name {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 0.8rem;
  padding: 0.32rem 0.55rem;
  width: 15rem;
}
#special-name:focus { outline: none; border-color: var(--accent-2); }
.mt-note { color: var(--muted); font-size: 0.78rem; font-style: italic; }

/* ---- Game options (matchup info / CPU manager / pitch detail) ---- */
#options-bar, #park-bar {
  max-width: 1100px;
  /* ONE RHYTHM FOR THE THREE SETUP LINES (Brian, s19): Game options, Ballpark
     options and "Who's your bench coach?" are three answers to the same
     question — how is this game set up — so the leading between them is equal.
     MEASURED before and after, because they were not: the first two sat 4px
     apart and the coach line 20px below that, which read as the coach belonging
     to something else and pushed him down the page. The gap came from
     `#park-bar` carrying BOTH a -0.25rem top and a 1.25rem bottom, so the two
     margins fought over every boundary. One bottom margin, and the negative
     top is kept only on the FIRST row, where it does the job it was added
     for. */
  margin: 0 auto 0.55rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.1rem;
}
#options-bar { margin-top: -0.25rem; }

/* THE BALLPARK ROW: the picker with its badge on the right, and the park's own
   info on the line below (Brian, s14). The note had been squeezed onto the same
   line and truncated with an ellipsis to keep the row from wrapping; a line of
   its own is what lets a park with six live factors actually be read. */
/* THE INFO LINE LINES UP WITH THE DROPDOWN (Brian, s14), not with the row's
   label — it describes what the menu selected, so it should read as belonging to
   it. Done with a GRID rather than a measured indent: the first attempt set a
   `padding-left` from the select's offset in JS, which is a number that has to
   be recomputed on every resize and was wrong the first time. Two columns and
   the note in the second is the same alignment with nothing to keep in step. */
#park-bar {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 0.25rem 0.6rem;
}
.pb-controls { display: flex; align-items: center; gap: 0.5rem; min-width: 0; flex-wrap: wrap; }
/* The note is the last thing on the row and takes whatever is left of it. */
.pb-controls .park-note { flex: 1 1 18rem; min-width: 0; }
/* `.era-toggle select` carries a 0.3rem left margin to sit off its own label
   text — and this label has no text any more, so that margin is leading
   whitespace that put the dropdown 5px right of the note under it. */
#park-bar .era-toggle select { margin-left: 0; }
#park-bar .park-note { margin: 0; }

/* "Manage this team" — inside each club panel */
.manage-this {
  background: var(--panel-2);
  color: var(--accent-2);
  border: 1px solid var(--accent-2);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.4rem 0.95rem;
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
}
.manage-this:hover:not(:disabled) { background: var(--accent-2); color: #10231a; }
.manage-this:disabled { opacity: 0.4; cursor: not-allowed; color: var(--muted); border-color: var(--line); }
/* "Manage both dugouts" — the third entry in the options bar. Same pill as the
   per-club button, pushed to the far end so it reads as its own choice rather
   than a fourth game option. */
.manage-both {
  background: var(--panel-2);
  color: var(--accent-2);
  border: 1px solid var(--accent-2);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  /* LEADING AND CLEARANCE (Brian, s14). It sat flush against the card above it
     with its own text crowding the pill; `line-height: 1` on a button leaves
     descenders touching the border, and `margin-left: auto` was pushing it right
     from a flex row it no longer lives in. */
  line-height: 1.35;
  padding: 0.45rem 1.1rem;
  margin-top: 0.7rem;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, opacity 0.15s;
}
.manage-both:hover:not(:disabled) { background: var(--accent-2); color: #10231a; }
.manage-both:disabled { opacity: 0.4; cursor: not-allowed; color: var(--muted); border-color: var(--line); }
/* Club header: name on the left, "Manage this team" right-aligned on the same line */
.team-head { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.6rem; }
/* WHO THE CLUB REALLY IS (Brian, s21) — sits under the editable name, so it
   states the season's identity no matter what the panel has been renamed to.
   Quiet by design: it is a caption on the input above it, not a second title. */
.team-real {
  margin: -0.35rem 0 0.6rem; font-size: 0.85rem; color: var(--muted);
  letter-spacing: 0.01em;
}
.team-head .team-name { flex: 1; min-width: 0; margin-bottom: 0; }
.team-head .manage-this { flex: none; white-space: nowrap; }

/* The ballpark crest. Sized here rather than in the SVG so the same mark can be
   dropped anywhere at any size — the module draws into a 100x100 viewBox and
   takes whatever box it is given. Real artwork replaces the <svg> with an <img>
   and inherits the same box, so a logo file cannot change the layout. */
.park-mark { flex: none; display: inline-flex; line-height: 0; }
.park-mark svg, .park-mark img {
  width: 3.1rem; height: 3.1rem; border-radius: 50%; display: block;
  object-fit: cover;
}

/* ---- Team-code glossary (s14, Brian) ----
   A reference, so it is collapsed by default and set in columns: 257 entries in
   one column is a page you scroll past rather than a list you scan. The count
   drops to one column on a phone, where two would be about fourteen characters
   wide. */
#team-codes {
  max-width: 1100px;
  /* Above the credits, inside the footer. */
  margin: 0 auto 1.4rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 0.7rem 1rem;
  /* LEFT, ALL OF IT (Brian, s26: "and left align team codes"). This used to read
     "the footer is `text-align: center` and a 257-row glossary is not", and the
     summary was centered to match the furniture around it while the list under it
     went back to left. THE FOOTER IS RANGED LEFT NOW, so the centering is the only
     thing on the block that is not, and the reason for it has expired with the
     rule it was matching. The `[open]` override went with it: it existed to undo
     a centering that no longer happens. */
  text-align: left;
}
#team-codes > summary {
  cursor: pointer;
  color: var(--accent-2);
  font-weight: 600;
  font-size: 0.9rem;
}
#team-codes[open] > summary { margin-bottom: 0.7rem; }
.tc-list { columns: 3; column-gap: 2rem; }
.tc-row { break-inside: avoid; display: flex; gap: 0.5rem; padding: 0.12rem 0;
  font-size: 0.78rem; line-height: 1.35; }
.tc-code { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums;
  flex: none; min-width: 3.1em; }
.tc-name { color: var(--text); min-width: 0; }
.tc-yrs { color: var(--muted); font-style: normal; }
/* The earlier names are the small print of the entry — present, and never
   competing with the answer somebody opened this to find. */
.tc-was { color: var(--muted); font-style: normal; font-size: 0.92em; }
@media (max-width: 900px) { .tc-list { columns: 2; } }
@media (max-width: 620px) { .tc-list { columns: 1; } }

/* RUN THE NUMBERS (Brian, s25). The line explains what the two buttons are for,
   so it reads FIRST and they sit under it — underneath them it was a caption on
   a decision you had already made. `.league-row` is a flex ROW, so stacking
   needs its own modifier rather than an override that has to out-specify it.
   LEFT-ALIGNED, with the shelves above it — the room has one edge and this
   block keeps to it. */
/* OUT-SPECIFIED, not merely later. `.league-row` sets `align-items: center` and
   is defined further down this file, so a single-class override loses on order
   and the block sat centered while the rule said flex-start. Two classes settle
   it wherever either one moves. Same trap as `.lk-shelf-head-bare` above, found
   the same way — by measuring the rendered edge instead of reading the rule. */
.league-row.league-row-stack { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
/* ONE LINE, NO BREAK (Brian, s25). The 62ch measure is right for a PARAGRAPH
   and wrong for a single sentence that is meant to read as one — it split
   "A win split tells you what's true" onto its own row and made three clauses
   look like two thoughts. No cap here: it fits the Office's width at this size,
   and on a phone it still wraps rather than overflowing, which `nowrap` would
   not. */
.league-row.league-row-stack > span { max-width: none; }
.league-row.league-row-stack .league-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }
/* THE SIMULATION'S OUTPUT TAKES THE ROW'S WIDTH, NOT ITS CONTENT'S (s26, found
   by playing a game on a 375px phone and measuring: the PAGE scrolled sideways,
   1127px against a 375px viewport).
   `#sim-out` had no rule at all. It is a flex item in a column-direction row set
   to `align-items: flex-start`, so it does NOT stretch — it shrink-to-fits, and
   shrink-to-fit cannot go below its content's min-content width. `resultsToOffice`
   moves the whole `#results` block in here after a simulation, and that block
   contains `.ss-grid`, which declares `min-width: 54rem`. So the box became 1082px
   inside a 285px column and took the document with it.
   AND `.ss-scroll` COULD NOT DO ITS JOB. It already carries `overflow-x: auto` and
   is built to scroll the card inside a phone-width window — but a scroll container
   only scrolls when something has decided how wide it is, and nothing had. The
   card was not too wide for the scroller; the scroller was as wide as the card.
   `width: 100%` is what decides it. `min-width: 0` goes with it so this stays
   fixed if the row is ever flipped back to a horizontal direction, where the auto
   minimum would reintroduce exactly this.
   THE SAME DEFECT, THE SAME PLACE TO FIX IT. Three times now a wide table has
   pushed the page sideways and the fix has belonged to the box the tables SHARE
   rather than to whichever renderer was on screen when it was noticed. `#results`
   in its home position is a block child of `#app` and was never affected, which is
   why this only ever showed up in the Office. */
#sim-out { width: 100%; min-width: 0; }
/* THE TWO THINGS YOU CAN DO WITH A FINISHED RESULT, side by side (s26). A row
   rather than two loose buttons so they wrap together on a phone, and it takes
   no height of its own when both are hidden — which is most of the time, since
   `#sim-save` only appears after a SINGLE game and `#sim-clear` only while a
   result is sitting in the Office. `:has()` keeps the gap from being drawn
   around nothing when only one of them is showing. */
.sim-acts { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.sim-acts:not(:has(button:not([hidden]))) { display: none; }
#sim-save[disabled] { opacity: 0.75; cursor: default; }
/* THE RULE ABOVE `Run the Numbers` IS BACK (Brian, s25: "add a line between
   seasons and run the numbers"), reversing his own earlier "remove a break line
   from above simulations section" in the same session. The reasoning that took
   it away — a rule there read as the start of another shelf — stopped applying
   when the league block below it was parked: this head no longer sits between
   two similar things, it is the last section on the page and the line now marks
   the end of the shelves rather than pretending to start a fifth one.
   THE CLASS IS KEPT rather than deleted from the markup, so putting it back is
   one rule and not a hunt through index.html. */
.lk-shelf-head.lk-shelf-head-bare { padding-top: 0.7rem; }
/* THE UNRULED GAP IS GONE (Brian, s26: "missing line between saved games and
   saved seasons"), reversing s25's "remove extra line between saved games and
   saved series". `.lk-shelf-head-joined` had exactly one user and it has been
   taken off it, so the rule goes rather than sitting here keyed to nothing —
   see the note in `index.html` for why the original reasoning expired. */

/* ---- The prose pages: about.html and how-to-play.html (s25, Brian) ----
   They are separate DOCUMENTS, not screens, so they carry their own shell and
   none of the app's chrome. `.ft-prose` is shared between them and is the only
   part that was ever in index.html. */
/* `.doc-header`, `.doc-home` and `.doc-logo` ARE GONE (s33, Brian: "put the
   header on the how to and about pages"). These pages carried a small vertical
   lockup and a text nav while the app carried the full-bleed photograph, so
   leaving the game for the rules looked like leaving the site. They use the
   app's own `#front` now — the same element and the same rules, not a lookalike
   — and the three rules that dressed the old shell had no other caller, so they
   came out rather than sitting here looking live. `.doc-nav` went with them; its
   one distinctive part, the current-page state, moved to `.fd-nav a.on`. */
.doc-main { max-width: 1100px; margin: 0 auto; }
.doc-main h1 {
  font-size: 1.5rem; margin: 0 0 1.1rem; color: var(--accent);
  /* The measure is the prose's, and the heading sits over it rather than over
     the full card, or the page reads as two different columns. Widened with it
     in s25 — the two have to stay equal or the heading stops matching the
     column it heads. */
  max-width: 93ch;
}
.page-doc { padding: 1.5rem 1rem 2.5rem; }
.page-doc .page-footer { max-width: 1100px; margin: 2rem auto 0; }

.ft-prose {
  color: var(--text); font-size: 0.92rem; line-height: 1.6;
  /* The measure, not the card — in `ch`, which IS the unit the measure is
     expressed in, so it tracks the font rather than restating it in pixels.
     MEASURED, NOT ASSUMED: `ch` is the width of a ZERO, which is wider than
     average prose, so the rendered line runs longer than the number says. 46rem
     measured 108 characters and 72ch measured 87; 62ch lands at about 75, the
     top of the range the eye reads without losing its place.
     WIDENED BY HALF (Brian, s25: "on the how to and about pages widen the copy
     column by 50%"). 62ch -> 93ch. That is past the measure the note above
     argues for, and it is his call on his own pages: these are reference
     documents people scan for a rule rather than read end to end, and the
     narrow column was making them very long. The number stays in `ch` so it
     still tracks the type rather than restating it in pixels. */
  max-width: 93ch;
}
.ft-prose p { margin: 0 0 0.8rem; }
.ft-prose ul { margin: 0 0 0.8rem; padding-left: 1.1rem; }
.ft-prose li { margin: 0 0 0.3rem; }
/* The section heads are the only thing that has to survive a long scroll, so
   they take the accent the rest of the block does not. */
.ft-prose h2 {
  margin: 1.6rem 0 0.6rem;
  font-size: 1rem;
  color: var(--accent);
  border-top: 1px solid var(--line);
  padding-top: 0.9rem;
}
.ft-prose h2:first-child { margin-top: 0; border-top: 0; padding-top: 0; }
.ft-prose b { color: var(--text); font-weight: 700; }
.ft-prose em { font-style: italic; }
/* CASE IS PRESENTATION. Writing TIRING in the markup would take the word out of
   the copy document, which reads all-caps as a code token (SP, RP, HR). */
.ft-prose .caps { text-transform: uppercase; letter-spacing: 0.04em; }
/* The way back to the game, at the end of the read.
   DELIBERATELY NOT GOLD. `Play Ball!` is the only gold control in the app and
   that is a stated rule — gold means "this is what HAPPENS", and a link off a
   prose page is not the act of starting a game. It takes the green that means
   "this is what is selected" everywhere else. */
.doc-cta { margin-top: 1.8rem; display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap; }
/* `:not(.doc-btn)` IS THE WHOLE FIX (Brian, s26: "not labeled on the how to and
   about pages"). The button had no visible label on either page — a bare green
   pill — because this rule painted it `--accent-2`, which is exactly the color
   `.doc-btn` fills its own background with. Green text on a green ground.
   AND SOURCE ORDER COULD NOT SAVE IT. `.doc-cta a` is a class plus an element,
   (0,1,1); `.doc-btn` is a class, (0,1,0). The more specific selector wins no
   matter that the other one comes after it — the same trap this file records at
   `.fd-sign:nth-child(n)` and at `.ss-code`'s fitted sizes, and the reason both
   of those are written the way they are.
   IT WAS ALWAYS FOR THE OTHER LINK. This rule exists to tint the plain "or read
   how to play" link that sat beside the button; it only ever hit the button by
   accident, and s26 removed that link, which would have left a rule whose entire
   remaining effect was the bug. Excluding the button says what was meant. */
.doc-cta a:not(.doc-btn) { color: var(--accent-2); }
.doc-btn {
  background: var(--accent-2); color: var(--bg); font-weight: 700;
  padding: 0.5rem 1.1rem; border-radius: 999px; text-decoration: none;
}
.doc-btn:hover { filter: brightness(1.08); }
@media (max-width: 620px) {
  .doc-header { gap: 0.6rem; }
  .doc-nav { font-size: 0.8rem; gap: 0.8rem; }
  /* THE SAME 44px FLOOR THE DUGOUT GOT (s26, measured on a 375px phone: the
     button came out 39.5 and the three nav links 24.9). `min-height: 2.75rem` is
     the number already used for `#mg-exit`, `#mg-stats`, `.mg-toggle` and every
     `#manage button` — one standard for a tap target rather than a second one
     invented over here.
     `inline-flex` IS WHAT MAKES IT APPLY. These are anchors, not buttons: an
     inline box ignores `min-height` outright, so setting it alone would have
     measured as changed and rendered as nothing. Centering the label keeps the
     text where it was and grows the target around it.
     THE NAV IS THE WORSE OF THE TWO and the easier to miss — a 25px link is
     under half a finger, and there are three of them side by side, so the cost
     of a near miss is landing on the wrong page rather than on nothing. */
  .doc-btn, .doc-nav a {
    min-height: 2.75rem;
    display: inline-flex; align-items: center; justify-content: center;
  }
}

/* First-load empty club: just the loader + a hint */
.empty-hint { color: var(--muted); font-size: 0.9rem; line-height: 1.5; margin: 0.75rem 0 0.25rem; }

/* ---- Roster depth chart + batting-order strip ---- */
.sub-head {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  font-weight: 700;
  margin: 0.9rem 0 0.4rem;
  padding-bottom: 0.2rem;
  border-bottom: 1px solid var(--line);
}
.sub-hint { color: var(--muted); font-weight: 400; text-transform: none; letter-spacing: 0; }

/* WHICH DUGOUT THIS SAVE WAS (Brian, s36). A chip rather than a sentence: the
   title already names both clubs, and s25 took the full "managing 1927 New York
   Yankees" off the line because it repeated one of them and ran to five lines on
   a phone. The nickname alone answers "which one was me" in the width of a word. */
/* The club you managed, marked INSIDE the title now rather than repeated in a
   pill beside it (s38) — so it is a highlight on the name that is already
   there, and it has to sit at the title's own size and baseline. */
.sg-mine {
  color: var(--accent-2);
  font-weight: 600;
}

/* A position the man standing in it cannot play — the window between hitting for
   the catcher and setting the defense. Amber rather than red: nothing is wrong
   yet, the manager simply has not answered. See `mgSlotUnset`. */
.mg-pos-unset {
  border-color: var(--accent) !important;
  color: var(--accent) !important;
  opacity: 0.85;
}

/* The reserves drawer. Its `<summary>` CARRIES `.sub-head`, so a shut drawer
   reads as exactly the heading that section always had and an open one is
   indistinguishable from the old markup — the only additions are a pointer and
   the disclosure triangle. `list-style` is set on both the element and the
   ::-webkit-details-marker so the triangle sits inside the rule's own
   padding-bottom rather than hanging outside the underline. */
.res-drawer > summary { cursor: pointer; list-style-position: inside; }
.res-drawer > summary::-webkit-details-marker { color: var(--accent); }
.res-drawer > summary::marker { color: var(--accent); }
.res-drawer[open] > summary { margin-bottom: 0.5rem; }
/* The count, so a shut drawer still says how many men are behind it. */
.res-count {
  background: rgba(111, 208, 140, 0.14);
  color: var(--accent-2);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
  font-size: 0.68rem;
  margin-left: 0.1rem;
}
/* No margin under a group any more: the group used to be "label, then row", so
   it needed a gap to keep the next label off this row. It is one row now, and
   `.dc-row`'s own border is the separator — the same one the bench and the
   staff have always used. (A group still wraps the row because the scoped
   search opens inside it.) */
.dc-group { margin-bottom: 0; }
/* `.dc-poslabel` is gone (s20) — the position moved into `.dc-bat`. Deleted
   rather than left dangling, on the standing rule about a stale hook getting
   reused by mistake. */
.dc-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* TIGHTER (Brian, s19). A depth chart is a LIST — nine of these read down the
     panel at once — and at 58px a row (76 when the stat line wraps) it read as
     nine cards rather than one card of nine men. The padding and the leading
     were both carrying air; see `.who` for the other half. */
  padding: 0.16rem 0.15rem;
  border-bottom: 1px solid var(--line);
}
.dc-row.cand { opacity: 0.82; }
.dc-row.cand .who small { color: var(--muted); }
.dc-row.open .who { color: var(--muted); font-size: 0.82rem; }
.dc-bat {
  flex-shrink: 0;
  width: 1.6rem;
  text-align: center;
  font-weight: 700;
  font-size: 0.8rem;
  color: var(--accent);
}
/* The lineup's spot, in the column the bullet used to decorate. Sized for the
   two-character labels (1B, SS, DH) rather than for "C", so the nine badges
   form a straight edge down the panel and the names all start at one x. */
.dc-bat.pos {
  width: 2.1rem;
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  color: var(--accent-2);
  background: rgba(111, 208, 140, 0.13);
  border-radius: 5px;
  padding: 2px 0;
  align-self: center;
}
.dc-row.cand .dc-bat { color: var(--muted); font-weight: 400; }
.dc-demote, .dc-drop, .dc-cut, .rm-arm, .dc-start {
  flex-shrink: 0;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--muted);
  font-size: 0.72rem;
  padding: 0.15rem 0.4rem;
  cursor: pointer;
}
.dc-start { color: var(--accent-2); font-weight: 600; }
.dc-start:hover { border-color: var(--accent-2); }
.dc-demote:hover, .dc-drop:hover, .dc-cut:hover, .rm-arm:hover { color: var(--danger); border-color: var(--danger); }
/* THE ROLL AND THE PICK ARE ONE CONTROL PAIR, so they take one style.
   `.dc-roll` had NO rule at all — the 🎲 buttons were rendering as the
   operating system's default gray while "+ Bench bat" beside them was the
   app's yellow-on-green pill, which read as two unrelated controls that
   happened to sit together (Brian, s14). */
.dc-add, .dc-roll {
  flex-shrink: 0;
  background: var(--panel-2);
  color: var(--accent);
  border: 1px dashed var(--line);
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.2rem 0.6rem;
  cursor: pointer;
}
.dc-add:hover, .dc-roll:hover { border-color: var(--accent); border-style: solid; }
.dh-picker {
  flex: 1;
  background: var(--panel-2);
  color: var(--accent-2);
  border: 1px dashed var(--line);
  border-radius: 6px;
  font-size: 0.78rem;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
}
.dh-picker:hover { border-color: var(--accent-2); }
/* Positions a hitter can actually play, tagged next to his name */
.pos-elig {
  display: inline-block;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--accent-2);
  background: rgba(111, 208, 140, 0.12);
  border-radius: 4px;
  padding: 0.02rem 0.32rem;
  margin-left: 0.15rem;
  vertical-align: middle;
}
/* Bench "Start…" menu — drop a bench bat into any lineup spot — and the
   reserves' "Replace…" menu, which sits in the same row and does the same kind
   of thing. ONE RULE FOR BOTH rather than a copy: they were two selects an inch
   apart wearing different clothes, because `.res-in` shipped with no rule at all
   and fell back to the OS control (Brian, s36 — the same note he made about the
   season pulldowns). Sharing the selector is what stops them drifting again. */
.bench-move, .res-in, .bx-pick {
  flex-shrink: 0;
  background: var(--panel-2);
  color: var(--accent-2);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.18rem 0.35rem;
  cursor: pointer;
}
.bench-move:hover, .res-in:hover, .bx-pick:hover { border-color: var(--accent-2); }
/* The box-score picker is that same control at reading size: it sits over a whole
   page rather than inside a roster row, so it takes the panel's type size and a
   minimum width that fits "Game 162 ★" — otherwise the control resizes under the
   cursor as you move through a season. */
.bx-pick { font-size: 0.82rem; padding: 0.3rem 0.5rem; min-width: 9rem; }
/* THE POSITION BESIDE THE NAME IN A STAT SHEET (Brian, s36: "in series stats
   page, list the position with the player's name"). A chip rather than a column,
   because the sheet is already 16 columns wide on a season and a 17th of two
   letters is the one that gets squeezed. `margin-left` is the whole reason s36's
   "2B/RF/LF/DHJeff McNeil" happened — a chip with no space before it reads as
   part of the name. */
/* The heading over each half of the leaderboard. Same type as a `.sub-head` —
   this IS one, in a screen that had none — without borrowing the class, because
   `.sub-head` carries a bottom rule that would draw a line across a grid of
   free-standing boxes rather than under a section of a list. */
.lead-group {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--accent); font-weight: 700;
  margin: 1.1rem 0 0.5rem;
}
.lead-group:first-child { margin-top: 0.2rem; }

.stat-pos {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.02rem 0.32rem;
  border-radius: 4px;
  background: rgba(111, 208, 140, 0.12);
  color: var(--accent-2);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  vertical-align: middle;
}
.szn-pick-lb { display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--accent); font-weight: 700; }

.dc-row.sp-current { background: rgba(245, 197, 66, 0.07); }
.sp-badge { color: var(--accent); }
.dc-row.sp-current .sp-badge { color: var(--accent); font-weight: 800; }
.sp-flag {
  flex-shrink: 0;
  color: var(--accent);
  font-size: 0.72rem;
  font-weight: 700;
  white-space: nowrap;
}
.sp-start {
  flex-shrink: 0;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--accent-2);
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.15rem 0.4rem;
  cursor: pointer;
}
.sp-start:hover { border-color: var(--accent-2); }

.lineup-strip { list-style: none; }
.ln-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.28rem 0.15rem;
  border-bottom: 1px solid var(--line);
  cursor: grab;
}
.ln-row:active { cursor: grabbing; }
.ln-row.dragging { opacity: 0.4; }
.ln-row.drag-over { border-top: 2px solid var(--accent); }
.ln-grip { flex-shrink: 0; color: var(--muted); font-size: 0.9rem; letter-spacing: -1px; }
.ln-ord {
  flex-shrink: 0;
  width: 1.4rem;
  text-align: center;
  font-weight: 700;
  color: var(--accent);
}
.ln-empty { color: var(--muted); font-size: 0.82rem; padding: 0.4rem 0.15rem; }

/* ---- Draft-by-position tray + inline slot search ---- */
.draft-tray {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.75rem;
}
.draft-tray-label { color: var(--muted); font-size: 0.78rem; font-weight: 600; margin-right: 0.15rem; }
.draft-slot {
  background: var(--panel-2);
  color: var(--accent);
  border: 1px dashed var(--line);
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0.3rem 0.7rem;
  cursor: pointer;
}
.draft-slot:hover { border-color: var(--accent); border-style: solid; }
.draft-search { margin-top: 0.75rem; }
.draft-search-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent);
  margin-bottom: 0.4rem;
}
.draft-cancel {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.72rem;
  padding: 0.15rem 0.55rem;
  cursor: pointer;
}
.draft-cancel:hover { color: var(--danger); border-color: var(--danger); }
.draft-search input {
  width: 100%;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  padding: 0.5rem 0.6rem;
  font-size: 0.85rem;
}
.draft-search input:focus { outline: none; border-color: var(--accent-2); }
/* Two classes so this beats `.search-results` (declared later) in the cascade. */
.draft-search .draft-results {
  position: static;       /* flow inline under the input, not as a floating dropdown */
  box-shadow: none;
  list-style: none;
  margin-top: 0.4rem;
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.draft-results li {
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  font-size: 0.85rem;
}
.draft-results li:last-child { border-bottom: none; }
.draft-results li:hover { background: var(--panel); }
.draft-results li em { color: var(--muted); font-style: normal; font-size: 0.78rem; }
.draft-results li small { display: block; color: var(--accent-2); font-size: 0.74rem; }
.draft-results li.none { color: var(--muted); cursor: default; }
.draft-results li .pos { display: inline-block; margin-right: 0.35rem; min-width: 1.8rem; }

.teams {
  display: grid;
  /* TWO COLUMNS, NOT THREE (s26). The middle one held the `VS` and is gone with
     it — see the note in `index.html` and `.team-role` below. */
  grid-template-columns: 1fr 1fr;
  /* THE GAP IS BETWEEN THE OUTLINES (Brian, s21). Two bordered panels an inch
     apart read as one wide box divided by a line. It was also what gave the VS
     room to read as "against" rather than as a label stuck to the left panel;
     that reason has expired but the first one has not, so the gap stays. */
  gap: 2rem;
  align-items: start;
  max-width: 1100px;
  margin: 0 auto;
}
/* THE FOOT OF THE ROSTERS (Brian, s21) — taking both dugouts, and the way
   across to the other format. Spans all three columns so it is a row under the
   two clubs rather than a third column beside them, and centered because both
   buttons are about the pair rather than about either one. */
.teams-foot {
  grid-column: 1 / -1;
  display: flex; justify-content: center; flex-wrap: wrap;
  gap: 0.5rem; margin-top: 0.35rem;
}
.teams-foot button[hidden] { display: none; }
/* THE `VS` IS GONE (Brian, s26: "take vs out and label both teams with away and
   home even after the team is chosen"), and s25's fix for it went with it — it
   used to drift down the page as the rosters filled, because `align-self:
   center` centers in a grid ROW as tall as the taller panel. Both the word and
   the rule that pinned it are deleted rather than hidden.
   WHAT REPLACES IT IS NOT A WORD IN THE MIDDLE. `VS` said "these two play each
   other", which two panels side by side already say; the thing it could never
   say is which club bats first, and that was carried only by the name field's
   default value — so it vanished the moment a real club was picked. The role is
   written on each panel now, permanently.
   AN EYEBROW, NOT A HEADING. It sits above the editable name and pairs with
   `.team-real` beneath it: role on top, identity under. Quiet enough that the
   club's name is still the title of the panel. */
.team-role {
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--dim, #9bb3a2);
  margin-bottom: 0.2rem;
}

.team-panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 1rem;
}

.team-name {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 2px solid var(--line);
  color: var(--accent);
  font-size: 1.2rem;
  font-weight: 700;
  padding: 0.25rem 0.1rem;
  margin-bottom: 0.6rem;
}
.team-name:focus { outline: none; border-bottom-color: var(--accent); }

/* WHERE THIS CLUB COMES FROM (s21 — PLAN-home.md §2, step 2). The row that used
   to sit under BOTH panels, now on each of them. Three modes as a radio group
   plus one roll, and the roll is styled apart because it is a different kind of
   thing: the modes are a state the panel is in, the roll happens once. */
.fill-row {
  display: flex; align-items: center; gap: 0.35rem;
  /* IT LEADS THE PANEL NOW, above whichever search box its mode calls for —
     you choose where the club comes from, then you search within that. It read
     as an afterthought under the box on one panel and as a heading over it on
     the other, which is what happens when a control's place depends on a mode
     rather than on what it means. */
  flex-wrap: wrap; margin: 0 0 0.7rem;
}
.fill-label { color: var(--muted); font-size: 0.74rem; }
/* A zero-height full-width item forces the wrap where the author wants it,
   rather than wherever the panel happens to run out of room. */
.fill-break { flex-basis: 100%; height: 0; }
.fill-btn {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.74rem;
  padding: 0.25rem 0.7rem;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.fill-btn:hover { border-color: var(--accent-2); color: var(--accent-2); }
/* FILLED, not outlined — an outline reads as hover in a row where every item is
   already outlined. Same rule as `.fmt-btn.on`, and deliberately the same look:
   they are both "exactly one of these is true". */
.fill-btn.on {
  background: var(--accent-2); color: #10231a; border-color: var(--accent-2);
  font-weight: 700;
}
.fill-btn.on:hover { color: #10231a; }
/* THE ROLL READS LIKE ITS NEIGHBORS (Brian, s21). It was dashed and muted to
   mark it as a one-shot rather than a mode — a true distinction that nobody
   asked about, and the cost was a button that looked disabled sitting beside
   three live ones. It is one of the four ways to fill a club and it says so. */
.fill-btn.roll { border-style: solid; color: var(--text); }
/* The one control in the row that DESTROYS work, so it is the one that does not
   look like the others. Quiet until you reach for it. */
.fill-btn.danger { margin-left: auto; color: var(--muted); border-style: dashed; }
.fill-btn.danger:hover { color: var(--danger); border-color: var(--danger); }
.fill-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.preset-row { display: flex; gap: 0.4rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.preset {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.75rem;
  padding: 0.25rem 0.7rem;
  cursor: pointer;
}
.preset:hover { border-color: var(--accent-2); }
.preset.clear { color: var(--danger); }

.lineup { list-style: none; }
.slot {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0.4rem;
  border-bottom: 1px solid var(--line);
  min-height: 2.6rem;
}
.slot.pitcher-slot { border-top: 2px solid var(--line); margin-top: 0.35rem; }
.ord {
  width: 1.5rem; height: 1.5rem;
  border-radius: 50%;
  background: var(--panel-2);
  color: var(--muted);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.75rem; font-weight: 700;
  flex-shrink: 0;
}
.pitcher-slot .ord { color: var(--accent); }
.pos {
  min-width: 2rem;
  text-align: center;
  background: var(--panel-2);
  color: var(--accent);
  border: 1px solid var(--line);
  border-radius: 5px;
  font-size: 0.68rem;
  font-weight: 700;
  padding: 0.15rem 0.25rem;
  flex-shrink: 0;
}
select.pos-select {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  font-family: inherit;
}
select.pos-select:hover { border-color: var(--accent); }

/* 1.25 across a name line, a glove phrase and a stat line that often wraps
   gave the roster its loose feel. The stat line takes its OWN, tighter figure:
   it is the one that wraps, so it is where the leading compounds. */
.who { flex: 1; line-height: 1.18; font-size: 0.9rem; }
.who em { color: var(--muted); font-style: normal; font-size: 0.78rem; margin-left: 0.3rem; }
.who small { display: block; color: var(--accent-2); font-size: 0.74rem; line-height: 1.32; }
.slot.empty .who { color: var(--muted); font-size: 0.8rem; }
.rm {
  background: none; border: none; color: var(--muted);
  cursor: pointer; font-size: 0.85rem; padding: 0.2rem;
}
.rm:hover { color: var(--danger); }

.search-box { position: relative; margin-top: 0.75rem; }
.search-box input {
  width: 100%;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  padding: 0.5rem 0.7rem;
  font-size: 0.9rem;
}
.search-box input:focus { outline: none; border-color: var(--accent-2); }
.search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0; right: 0;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  list-style: none;
  max-height: 280px;
  overflow-y: auto;
  z-index: 10;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.search-results li {
  padding: 0.45rem 0.7rem;
  cursor: pointer;
  font-size: 0.88rem;
  border-bottom: 1px solid var(--line);
}
.search-results li:last-child { border-bottom: none; }
.search-results li:hover { background: var(--panel); }
.search-results li em { color: var(--muted); font-style: normal; font-size: 0.78rem; }
.search-results li small { display: block; color: var(--accent-2); font-size: 0.74rem; }
.search-results li.none { color: var(--muted); cursor: default; }
.search-results li .pos { display: inline-block; margin-right: 0.35rem; }
.search-results li.blocked { opacity: 0.45; }
.search-results li.blocked .pos { color: var(--danger); }

.team-search { margin: 0 0 0.75rem; }
.team-search input {
  border-style: dashed;
  font-style: italic;
}

.missing {
  color: var(--muted);
  font-size: 0.78rem;
  margin-top: 0.4rem;
}

.notice {
  margin-top: 0.5rem;
  color: var(--danger);
  font-size: 0.8rem;
}

.bottom-row { margin-top: 0.75rem; }
.preset.dice { color: var(--accent); }

.controls {
  max-width: 1100px;
  margin: 1.25rem auto;
  display: flex;
  gap: 0.75rem;
  align-items: center;
  flex-wrap: wrap;
}
/* TWO ACTION ROWS CAN BE VISIBLE AT ONCE — the clubhouse shows the single game
   and the Monte Carlo — and two stacked `margin: 1.25rem auto` sections open a
   gap that reads as a missing element between them. `:not([hidden])` on BOTH
   sides is load-bearing: the router hides by attribute, and a plain
   `.controls + .controls` would tighten a visible row against a hidden one and
   pull it up into the panels above. */
.controls:not([hidden]) + .controls:not([hidden]) { margin-top: -0.75rem; }
.controls button {
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
  padding: 0.6rem 1.2rem;
  border-radius: 10px;
  cursor: pointer;
}
.controls button.primary { background: var(--accent); color: #1a1a10; border-color: var(--accent); }
.controls button:disabled { opacity: 0.4; cursor: not-allowed; }
#controls-hint { color: var(--muted); font-size: 0.85rem; }
.era-toggle {
  color: var(--muted);
  font-size: 0.85rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
  user-select: none;
}
.era-toggle input { accent-color: var(--accent); }

.series-sub { color: var(--muted); font-size: 0.85rem; margin-bottom: 0.75rem; }
/* THE SERIES STATE, AND WHAT IT TAKES TO WIN IT (s35). Two facts of unequal
   weight on one line: who is ahead reads at heading size, "first to 4" is the
   footnote that makes the number mean something. Baseline-aligned rather than
   stacked, because the second is a qualifier on the first and not a second
   line of its own. */
.series-state { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.6rem; }
.series-need { font-size: 0.72rem; font-weight: 500; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--muted); }
.series-table { border-collapse: collapse; font-variant-numeric: tabular-nums; }
.series-table td {
  padding: 0.35rem 0.9rem 0.35rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}
.series-table .score { font-weight: 700; color: var(--accent); }
.series-table .score em { color: var(--muted); font-style: normal; font-size: 0.8rem; }
/* What the CPU manager changed between games. It lives INSIDE `.score`, which is
   bold and accent-colored, so both are reset rather than assumed — this is a
   footnote to the result, not part of it. */
.series-table .ser-moves {
  font-weight: 400;
  color: var(--muted);
  font-size: 0.78rem;
  margin-top: 0.15rem;
  line-height: 1.35;
}

#locker {
  max-width: 1100px;
  /* THE SPACE GOES BETWEEN THE OUTLINES (Brian, s21), which is where he asked
     for it twice and where I put it neither time. The first attempt widened the
     gap above the heading and the second widened the gap between the two club
     panels — both INSIDE something, while the edge that was actually touching
     was this box's top against the roster boxes' bottom. This element had no top
     margin at all, so the two outlines met with nothing between them. */
  margin: 2.6rem auto 1.25rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 1rem 1.25rem;
}
/* THE OFFICE'S OWN HEADING IS A `.sec-rule` NOW (s21), like the game's and the
   rosters'. `.locker-summary` and `.locker-details` went with the `<details>`.
   `#locker` keeps its id: it is named in `PLAY_CHROME` and this file records
   three bugs from a chrome row being remembered in more than one place. */
/* ...and its heading is the first thing INSIDE the box, so it takes no top
   margin of its own — that was the space landing in the wrong place. */
#locker > .sec-rule { margin-top: 0; }
.locker-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.locker-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.locker-actions button, .league-row button {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 0.82rem;
  padding: 0.4rem 0.8rem;
  cursor: pointer;
}
.locker-actions button:disabled, .league-row button:disabled { opacity: 0.4; cursor: not-allowed; }
/* ---- The save shelf (Brian, s20) -----------------------------------------
   Deliberately the SAME row as the saved-team list above it: one room, one
   list idiom, so the two shelves read as two shelves rather than two features.
   It only adds the second line, because a game needs saying which game. */
.lk-shelf-head {
  margin-top: 1rem; padding-top: 0.7rem; border-top: 1px solid var(--line);
  font-size: 0.82rem; font-weight: 700;
}
/* The FIRST shelf head sits directly under the three save buttons, so it takes
   no rule above it — the block it would separate from is its own heading. */
.locker-head + .lk-shelf-head { margin-top: 0.9rem; padding-top: 0; border-top: 0; }
/* EVERY SHELF, NOT JUST THE GAMES ONE — the id-scoped version is exactly the
   "remembered in two places" failure the block below records, and a fourth
   shelf found it again. */
.lk-shelf .sg-line { font-size: 0.82rem; color: var(--muted); font-weight: 400; }
/* SPACED BY PADDING, NOT BY SPACES IN THE STRING — generated content collapses
   its trailing whitespace, so `" \00b7 "` renders as "· best of 3" with the dot
   welded to the next word. */
.lk-shelf .sg-line::before { content: "\00b7"; padding: 0 0.35em; }
/* TWO TIDY LINES RATHER THAN A RAGGED FOUR (Brian, s25). One line is right on a
   desktop and wrong on a phone: three long club names plus the format, the
   tally and the date wrapped wherever the box ran out — 72px of ragged text
   broken mid-clause. Below 620px the detail takes its own line, which is a
   DECISION about where the break goes rather than leaving it to the box, and
   the separator goes with it: a "·" between two things on one line is
   punctuation and at the head of a second line it is litter. */
@media (max-width: 620px) {
  .lk-shelf .sg-line { display: block; }
  .lk-shelf .sg-line::before { content: none; }
  /* THE NAME TAKES THE WHOLE ROW AND THE BUTTONS DROP UNDER IT. Splitting the
     detail onto its own line was not enough on its own: the actions column ate
     about a third of a 375px screen, so `1927 New York Yankees vs 1986 New York
     Mets` still broke over three lines beside them and the row came to four.
     Given the full width the title fits on one, which makes the row two lines
     of text and a button row instead of four lines of ragged text.
     MEASURED, not assumed — the title needs ~267px and the name column had
     ~230. */
  .lk-shelf li { flex-wrap: wrap; row-gap: 0.3rem; }
  .lk-shelf .saved-name { flex: 1 1 100%; min-width: 0; }
  .lk-shelf .saved-actions { margin-left: auto; }
}
/* THE SAVED ROWS SPEAK IN THE SAME QUIET VOICE AS AN EMPTY SHELF (Brian, s25:
   "anything saved ... should be the same color as 'No saved series yet'"). The
   room's rule already is that everything explanatory takes `--muted`; a saved
   row was the one line still shouting in full-strength text. */
.lk-shelf .saved-name { color: var(--muted); font-size: 0.82rem; }
/* ONE CLASS FOR EVERY SHELF (Brian, s21). These rules named `#saved-games` and
   `#saved-list` by id, so when `#saved-campaigns` arrived it inherited none of
   them — it kept its bullets, its rows had no rule, and its empty line was a
   different size from the other two. That is the "remembered in two places, so
   remembered in none" failure this file records three times over, and a third
   shelf is exactly what finds it.
   `.lk-shelf` is on all three now, so a fourth is correct by construction. */
.lk-shelf .fav { color: var(--accent); }
.lk-shelf li.kept { border-left: 2px solid var(--accent); padding-left: 0.5rem; }
/* The one sentence that stands in for three disabled controls' worth of hints
   until two clubs are saved (Brian, s25). Sized as body copy rather than as a
   hint, because while it is up it is the only thing describing the block. */
#league-locked {
  color: var(--muted); font-size: 0.88rem; max-width: 62ch; margin: 0.2rem 0 0.6rem;
}
/* STILL GOING (Brian, s25: "highlight 'left in progress' in the saved section of
   the office"). The shelf mixes finished games you might re-read with games
   waiting for you, and only the second kind is asking for anything.
   THE MARK IS ON THE DETAIL LINE, not the row: a favorite already owns the
   left border (`.kept`), and a second rail beside it would read as one thick
   stripe of no particular meaning. Coloring the phrase puts the signal on the
   words that carry it — "left in progress", "game 3 of 6" — and the two marks
   stack without fighting, which they have to, since a favorite can be
   unfinished. `--accent-2` is the app's "live" green; the gold is spoken for. */
.lk-shelf li.live .sg-line { color: var(--accent-2); font-weight: 600; }
.lk-shelf { list-style: none; margin-top: 0.6rem; }
.lk-shelf li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.9rem;
}
/* THE SIZE EVERY QUIET LINE IN THIS ROOM TAKES (Brian, s21: "all these lines
   should match the font size of 'No saved teams yet.'"). The empty-shelf lines
   set it; the league rows' explanations and the three hints under them now take
   the same, so the Office has one voice for "here is what this does" instead of
   four sizes of it. */
.lk-shelf li.none { color: var(--muted); border: none; font-size: 0.82rem; }
.league-row > span, #campaign-hint, #season-hint, #playoff-hint {
  color: var(--muted); font-size: 0.82rem;
}
.saved-actions { display: flex; gap: 0.35rem; }
.saved-actions button {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 0.72rem;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
}
.saved-actions button.del { color: var(--danger); }
.league-row {
  margin-top: 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.playoff-line { font-size: 0.95rem; padding: 0.2rem 0; }
.leaders-head { margin-top: 1.25rem; }
.injury-report { margin-top: 0.75rem; }

/* THE INJURY REPORT (Brian, s36: "announce injuries more prominently and the
   nature of the injury"). A bordered callout rather than a line of body copy —
   the news that a starter is unavailable should not be set in the same ink as
   his replacement's ERA. `--warn` when something happened tonight, the ordinary
   line when it is a man still working his way back. */
.inj-box {
  margin: 0.75rem 0;
  border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: 8px;
  background: var(--panel-2);
  padding: 0.5rem 0.7rem;
}
.inj-box-fresh { border-left-color: var(--accent); }
.inj-head {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted); margin-bottom: 0.35rem;
}
.inj-box-fresh .inj-head { color: var(--accent); }
.inj-list { list-style: none; margin: 0; padding: 0; }
.inj-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem;
  font-size: 0.86rem; padding: 0.18rem 0;
}
.inj-row + .inj-row { border-top: 1px solid rgba(255, 255, 255, 0.05); }
.inj-flag {
  font-size: 0.6rem; font-weight: 800; letter-spacing: 0.06em;
  background: var(--accent); color: #14210f;
  border-radius: 4px; padding: 0.05rem 0.28rem;
}
.inj-who { color: var(--text); }
/* The injury itself is the thing that was missing, so it is not muted. */
.inj-what { color: var(--accent-2); }
.inj-back { color: var(--muted); font-size: 0.8rem; margin-left: auto; }
@media (max-width: 560px) {
  .inj-back { margin-left: 0; flex-basis: 100%; }
}
/* **THIS 260px IS DEAD — THESE BOXES ARE 340px WIDE.** `.box-team` sets `flex`
   on the same element further down this file at the SAME specificity (0,1,0),
   so source order hands it the basis. Left alone because changing it would
   re-lay-out the whole board, but a reader sizing against 260 is wrong by 80px,
   and an override written at this specificity will lose the same way. */
.leader-box { flex: 1 1 260px; }
/* **A LEADER BOX LETS THE NAME WRAP, BECAUSE ITS NUMBERS ARE THE POINT** (Brian,
   s50: "the mobile leader boxes clipping their value column, fix it"). `.box
   .tname` is `nowrap`, which is right for the game box score it was written for
   — fifteen columns you scroll with the batting order frozen at the left. A
   leader box is two or three columns that should never scroll at all, and there
   the name ran 231-261px inside a 285px box and pushed the last column out.
   MEASURED BY SWEEPING THE CONTAINER: 24 of 26 boxes clipped at 280px, 6 at
   300px, none at 320px and up — and the ERA box hid its innings column
   completely at phone width.

   **THE THRESHOLD IS SET BY THE LONGEST NAME, NOT BY THE VIEWPORT**, which is
   why this is not a media query. "Carmen Hill 1927 Pittsburgh Pirates" is what
   decides it; a wider name in a wider box fails the same way, and a breakpoint
   would be fitted to today's data. Wrapping holds at every width down to 240px
   and costs nothing where the row already fits on one line — desktop measured 0
   wrapped rows, so nothing moved there.
 */
.leader-box .box .tname { white-space: normal; position: static; }
/* `position: static` is INERT AT EVERY WIDTH MEASURED — once the name wraps the
   table fits, so the sticky never engages, and dropping it changed nothing from
   240px up. It stays for the shape of the failure rather than its size: sticky
   turns an overflow into an INVISIBLE one, pinning the name over the columns it
   has pushed out, so a third figure added to one of these boxes later would go
   missing instead of merely going off the edge. */
/* …and the figures never wrap, which is the other half: given slack, a table
   will break "78.0" across two lines before it narrows the name beside it. */
.leader-box .box th:not(.tname), .leader-box .box td:not(.tname) { white-space: nowrap; }
/* The club board's division cell holds "NL West", and a table that is already
   past its container will wrap it to two lines before it gives the column the
   fifty pixels — which doubles the height of every row on the board. It is two
   words and it stays on one line; the table has its own scroller for the rest
   (s50, Brian: "sortable by division"). */
.stat-sheet .tdiv { white-space: nowrap; }
/* A substitute nests under the batting slot he took over, MLB-style, with the
   letter that keys him to the footnote below the table. */
.box .ph-row .tname { padding-left: 1.1rem; }
.box-sublet { color: var(--muted); font-variant-numeric: tabular-nums; }

/* EVERY STANDINGS TABLE SIZES TO ITS PANEL, not to its contents. There are three
   renderers — the round robin, the real season's divisions and the campaign's
   table — and only the season's ever got this, because only IT sits in a grid
   (`.szn-standings`) that set the width for it. The other two are eight columns
   wide in a bare div: 305px against a 285px panel, hanging 20px out over the
   edge on a phone. Not enough to scroll the page, which is exactly why it
   survived a session of sweeps that all asked whether the PAGE scrolled.
   The width belongs on the base class because all three want the same thing. */
.standings { border-collapse: collapse; font-variant-numeric: tabular-nums; width: 100%; }
.standings th, .standings td {
  padding: 0.35rem 0.8rem 0.35rem 0;
  text-align: right;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}
.standings th { color: var(--muted); font-weight: 600; }
.standings .tname { text-align: left; }
.standings td:first-child { color: var(--muted); }
/* AND `width: 100%` ALONE DOES NOT DO IT, which is the part worth remembering:
   it is a PREFERRED width, a table cannot go below its min-content, and
   min-content here IS 305 — eight columns at 0.8rem of right padding apiece is
   90px before a single figure is drawn. The phone gets the padding cut that
   actually buys the room, at the same breakpoint `.linescore` takes its own. A
   desktop keeps the roomier setting and uses the width to FILL its panel rather
   than huddle at the left. */
@media (max-width: 620px) {
  .standings th, .standings td { padding-right: 0.4rem; font-size: 0.86rem; }
}

#results {
  max-width: 1100px;
  margin: 0 auto 3rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 1.25rem;
}
#results h2 { color: var(--accent); font-size: 1.3rem; margin-bottom: 0.75rem; }

/* The line score has a WIDTH FLOOR it cannot shrink past — an extra-inning game
   is 10+ inning columns plus R/H/E — so on a phone it pushed the whole document
   sideways (375 wide, 469 of scroll). It scrolls inside its own box now, the
   same treatment `#mg-scoreboard` and `.stat-wrap` already give their wide
   tables. The wrapper holds the table alone, not the headline above it, so the
   result stays on screen while the innings scroll. */
.linescore-wrap { overflow-x: auto; }
.linescore { border-collapse: collapse; font-variant-numeric: tabular-nums; }
.linescore th, .linescore td {
  padding: 0.3rem 0.55rem;
  text-align: center;
  border: 1px solid var(--line);
  font-size: 0.9rem;
}
.linescore th { color: var(--muted); font-weight: 600; }
.linescore .tname { text-align: left; color: var(--text); }
.linescore .tot { font-weight: 800; color: var(--accent); }
/* THE CLUB NAME IS ONE LINE AND IT STAYS PUT (Brian, s25: "fix the club name
   column on mobile"). Scrolling the table inside its own box fixed the DOCUMENT
   pushing sideways and left the name column to fend for itself: with nothing
   stopping it wrapping, "1986 New York Mets" broke over FOUR lines in a 73px
   column and each row stood 91px tall — and then, because the column scrolled
   away with everything else, reaching R/H/E meant losing track of whose row you
   were reading. Measured at 375: rows 91px, and the totals off-screen.
   `nowrap` alone would only trade four lines for a wider column, so the column
   is STICKY as well — the name holds the left edge while the innings run under
   it, which is what a pinned column is for and what makes the horizontal scroll
   usable rather than merely possible. Rows are 31px now.
   EVERY ROW'S FIRST CELL, not just `.tname`. The header's corner cell is a bare
   `th` with no class; leaving it out pinned the two body rows and let the header
   slide, so the inning numbers stopped standing over their own columns — the
   table said 3 runs in the 8th when the 8th was somewhere else entirely.
   AN OPAQUE BACKGROUND IS NOT DECORATION. A sticky cell is painted over the
   cells passing beneath it, so without `--panel` behind it the digits scroll
   THROUGH the club name. It is `--panel` because that is what the panel around
   this table is; if the line score ever moves onto another surface, this moves
   with it.
   `border-collapse: collapse` IS LEFT ALONE. Sticky and collapsed borders are a
   known quarrel, so this was checked rather than assumed: the pinned cell keeps
   its 1px right border and holds position to the end of the travel. */
.linescore tr > *:first-child {
  white-space: nowrap;
  position: sticky; left: 0; z-index: 1;
  background: var(--panel);
}
/* On a phone the name is still 180px of a 285px box at the table's own 0.9rem,
   which leaves three innings visible. Smaller type and tighter cells put it at
   156 and show six. NARROW SCREENS ONLY — a desktop has room and keeps the size
   it has always had. */
@media (max-width: 620px) {
  .linescore th, .linescore td { padding: 0.3rem 0.4rem; }
  .linescore tr > *:first-child { font-size: 0.8rem; }
}

/* ---- the newspaper recap ------------------------------------------------
   Set like a clipping: a serif face (the only one in the app, which is the
   point — it reads as a different kind of object from the tables around it),
   a rule under the headline, and the lede's dateline in small caps. The body
   is set in COLUMNS on a wide screen so it looks like a column of type, and
   collapses to one on a narrow one, where two 20-character columns would be
   unreadable. */
.recap {
  margin: 1.4rem 0 0.4rem;
  padding: 1.1rem 1.3rem 1rem;
  border: 1px solid var(--line);
  border-top: 3px solid var(--accent);
  border-radius: 4px;
  background: var(--panel);
}
.recap-head {
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.5rem;
  line-height: 1.15;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}
.recap-dek {
  margin-top: 0.3rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--line);
  color: var(--accent-2);
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}
.recap-body {
  margin-top: 0.8rem;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 0.94rem;
  line-height: 1.55;
  color: var(--text);
  columns: 2;
  column-gap: 2rem;
}
.recap-body p { margin-bottom: 0.7rem; break-inside: avoid; }
/* The lede was given `break-after: column` at first, on the theory that a lede
   should not straddle the gutter. Measured, that was wrong for this content:
   with four short paragraphs it left the first column holding one line and the
   second holding three. Balanced flow is both better-looking and what a column
   of type actually does. */
.recap-lede::first-letter {
  font-size: 1.35em;
  font-weight: 700;
  color: var(--accent);
}
.recap-dateline {
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.08em;
  font-size: 0.82rem;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
@media (max-width: 720px) {
  .recap-body { columns: 1; }
  .recap-lede { break-after: auto; }
  .recap-head { font-size: 1.25rem; }
}

/* ===================================================================
   THE HOT SEAT CHRONICLE — phase 1, the paper (Brian, s27, off his mockup)
   ===================================================================
   CHROME OVER THE EXISTING ARTICLE, NOT A SECOND RENDERER. `recap.js` writes
   every word and is DOM-free with 63 assertions and 10 mutants behind it, and
   the copy round trip reads it — so the newspaper is a class on the article the
   app already builds. Nothing about the writing moved to get this look, and if
   these rules ever fail the article underneath is still readable.

   INK ON PAPER, STATED RATHER THAN INHERITED. Every other panel takes its
   colors from `:root`; this one cannot, because a newspaper is white whatever
   the app is doing around it. That is also why it needs no print rules of its
   own — it is already the color it prints. */
/* THE PAPER IS BRIAN'S OWN SHEET (s27, "use the PNG I gave you as the
   background"). `newsprint.webp` is a 120px patch lifted from a clean corner of
   his Chronicle mockup and MIRRORED into a 240px tile, so the four seams meet
   their own reflection and the grain repeats without a visible grid. Half a
   kilobyte.
   THE FLAT COLOR STAYS UNDER IT and is sampled from the same patch (rgb
   213/209/206), so the sheet is the right tone before the tile loads, if it
   fails to, and in the PDF — where a browser drops background images unless the
   reader asks for them. */
.chron {
  --paper: #d5d1ce;
  --ink: #14140f;
  --ink-soft: #4a4a41;
  --rule: #1e1e18;
  background: var(--paper) url("assets/newsprint.webp") repeat top left;
  background-size: 240px 240px;
  color: var(--ink);
  border: 0;
  border-radius: 2px;
  /* LESS AIR OVER AND UNDER THE FLAG (Brian, s27), so the top padding is the
     one that is trimmed rather than all four: the sides still hold the type off
     the sheet's edge and the foot still carries the article clear of it. */
  padding: 0.7rem 1.5rem 1.6rem;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
}
/* THE FLAG IS THE ARTWORK, NOT A REBUILD OF IT (Brian: "the black and white logo
   and Chronicle"). The first cut set the word in Georgia beside the app's color
   logo, which was a good likeness and the wrong thing: his lockup is an
   ENGRAVING, and the weight, the rules inside the badge and the fit of the word
   to the mark are all drawn. It is cropped straight out of the mockup with the
   paper knocked out to alpha, so it sits on the sheet with no seam.
   THE PAPER'S GRAIN WAS LANDING IN THAT ALPHA at a few percent everywhere — a
   faint rectangular haze over the sheet, and the reason the file would not
   compress. Clamping the bottom tenth to zero fixed both: 429 KB to 76. */
.chron-flag {
  display: block;
  padding-bottom: 0.2rem;
  /* ONE RULE, NOT A DOUBLE (Brian, s27). */
  border-bottom: 1px solid var(--rule);
}
.chron-lockup {
  display: block;
  width: 100%;
  max-width: 34rem;      /* its own drawn size; larger only softens it */
  height: auto;
  margin-inline: auto;
}
/* The date strip: its own band under the flag, the way the mockup rules it off
   above and below. Absent for a game played before the stamp shipped, and the
   band goes with it rather than ruling off nothing.
   IT SITS CENTERED BETWEEN THE TWO RULES (Brian, s27), which means no margin
   above and EQUAL padding either side of the line of type — the 0.35rem margin
   it used to carry pushed it down against the lower rule, so the band read as
   bottom-weighted even though the text was centered across it. */
.chron-date {
  margin: 0;
  padding: 0.42rem 0;
  border-bottom: 1px solid var(--rule);
  text-align: center;
  text-transform: uppercase;      /* case is presentation — see `playedOnLabel` */
  letter-spacing: 0.06em;
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1;                 /* or the type's own leading re-centers it */
  color: var(--ink);
  font-family: Georgia, "Times New Roman", serif;
}
/* THE ID IS IN THIS SELECTOR BECAUSE IT HAS TO BE. `#results h2` sets the
   headline gold at (1,0,1), and no number of classes beats an id — `.chron
   .recap-head` is (0,2,0) and lost silently, which is this file's own "an
   override must OUT-SPECIFY the rule it overrides" arriving from a new
   direction. The article is drawn into exactly two containers (`renderRecap`'s
   default `#recap` and the series box's `#sb-recap`), so both are named; the
   bare class stays last for any third one, where no id rule would apply. */
#recap .chron .recap-head,
#sb-recap .chron .recap-head,
.chron .recap-head {
  margin: 0.7rem 0 0;
  color: var(--ink);
  font-family: "Playfair Display", Georgia, "Times New Roman", serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.015em;
  line-height: 1.02;
  /* SIZED IN TWO PASSES (Brian, s27). It was set to dominate the sheet the way
     the mockup's placeholder does, and against a REAL headline — a sentence,
     not three words — that was shouting; the first cut took it to
     clamp(1.2rem, 3.6vw, 2rem) and he asked for half of that back. Each of the
     three values is the midpoint of the two, so the curve keeps its shape
     rather than one end being nudged and the others left where they were. */
  font-size: clamp(1.35rem, 4.4vw, 2.45rem);
  /* IT WRAPS RATHER THAN FITTING. A generated headline is any length — the
     mockup's own placeholder is cut off mid-word — and fitted type would be
     unreadable on a phone. Balanced so two lines break evenly. */
  text-wrap: balance;
}
.chron .recap-dek {
  margin-top: 0.4rem;
  padding-bottom: 0;
  border-bottom: 0;
  color: var(--ink);
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(0.8rem, 1.8vw, 0.95rem);
  line-height: 1.3;
  letter-spacing: 0;
  text-wrap: balance;
}
.chron-by {
  margin: 0.6rem 0 0;
  display: inline-block;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.1rem;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink);
}
/* THE ART AND THE TEXT ARE A ROW, NOT A FLOAT (phase 2). The mockup runs the
   columns down the left and the picture down the right, and a float would let
   the body wrap under the image once it ran past it — which is a magazine, not
   a front page. Grid keeps the picture a column of its own for its whole height.
   THE PICTURE IS ALLOWED TO BE ABSENT: `chronArtHtml` returns nothing for a
   neutral site, and one grid child simply takes the width. */
.chron-cols {
  margin-top: 0.9rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--rule);
  display: grid;
  /* A NARROWER PICTURE COLUMN CLOSES THE HOLE FROM BOTH ENDS (Brian, s27: "no
     empty space in the columns"). At a third of the article the crest stood 67px
     taller than the story beside it, and the article was as tall as the PICTURE
     — so the white was under the text, not in it. Giving the width back to the
     type makes the story taller and the art shorter at the same time. */
  grid-template-columns: 1fr minmax(0, 26%);
  gap: 1.4rem;
  align-items: start;
}
.chron-cols:not(:has(.chron-art)) { grid-template-columns: 1fr; }
.chron-art { margin: 0; }
/* THE PICTURE IS CAPPED, NOT JUST PROPORTIONED. A third of a wide article is
   340px, and the crest is a CIRCLE — filled to that it stood taller than the
   four paragraphs beside it and read as the subject of the page rather than as
   its illustration. The cap is on the mark, so the column keeps its third and
   the art simply centers in it. */
/* AND THE PICTURE PRINTS IN BLACK AND WHITE, because the page does. The flag is
   an engraving and the type is ink; a tan-and-brown crest beside them read as a
   sticker on a broadsheet. The contrast bump is because grayscale alone leaves a
   mid-tone roundel that goes muddy against newsprint.
   IT IS A FILTER, NOT A SECOND ASSET, so Brian's own ballpark art gets the same
   treatment the moment he drops a file in — and one line brings the color back
   if he would rather have it. */
.chron-art .park-mark {
  display: block; max-width: 10rem; margin-inline: auto;
  /* Treated like the photograph it stands in for — they occupy the same slot and
     only one of them is ever on screen, so a reader who sees both across two
     articles should not see two different treatments. (The sepia both of them
     briefly carried in s28 came off both of them together.) */
  filter: grayscale(1) contrast(1.12);
}
.chron-art .park-mark svg,
.chron-art .park-mark img {
  display: block; width: 100%; height: auto;
  /* Brian's own ballpark art is a photograph and the crest is a roundel; both
     have to sit in the same box without one of them being stretched. */
  object-fit: contain;
}
.chron-cap {
  margin-top: 0.4rem;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;   /* case is presentation — the caption is written plain */
  color: var(--ink);
  /* CENTERED UNDER THE PICTURE (Brian, s27). The mark is capped at 14rem and
     centers in a column a third of the article wide, so a right-aligned credit
     hung off the column's edge rather than off the art it belongs to. */
  text-align: center;
}
/* THE PLAYER OF THE GAME'S PHOTOGRAPH (Brian, s28), in the slot the crest was
   holding. Same width and the SAME GRAYSCALE TREATMENT as the crest, because
   the point is that it looks printed rather than pasted — most of these are
   already black-and-white (a Goudey card, a Conlon portrait) and the ones that
   are not have to join them.
   NO BORDER (Brian, s28: "no border around the image"). It carried a hairline
   rule to read as a halftone's edge; the ribbon above already frames the
   picture and a second frame was one too many.
   The photographs come in at every aspect ratio Commons has, so the box crops
   rather than letterboxes. */
.chron-photo {
  display: block; width: 100%; max-width: 10rem; margin-inline: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover; object-position: 50% 22%;   /* faces sit high in a portrait */
  /* PLAIN GRAY, NOT TONED. s28 tried a sepia pass over these and Brian took it
     straight back off — the paper is already warm, so a toned photograph on top
     of it read as an effect rather than as printing. Gray against a warm sheet
     is what a newspaper actually looks like. */
  filter: grayscale(1) contrast(1.08);
}
/* THE "PLAYER OF THE GAME" RIBBON (Brian, s28), ABOVE the photograph — so the
   column reads title, picture, man, credit. It announces him and the caption
   underneath names him, which is how a paper sets a labeled portrait; with the
   ribbon below the picture the two were doing the same job twice.
   UNFILTERED, unlike the photograph above it and the crest it replaced. Those
   are made to look printed because they arrive in color from somewhere else;
   this is Brian's own engraving and already is the ink — the same posture
   `.chron-lockup` takes with the masthead, which is the same hand.
   It is WIDER THAN THE PHOTOGRAPH on purpose: the ribbon's tails run past the
   frame the way a real one would be pasted over the plate. */
.chron-potg {
  display: block;
  /* MEASURED AGAINST THE PHOTOGRAPH, NOT THE COLUMN. `width: 118%` resolved
     against `.chron-art`, which is wider than the 10rem picture inside it — the
     ribbon came out 308px against a 160px portrait and sat out under the story.
     Capped in the same unit the photograph is capped in, so the overhang stays
     proportional to the thing it is labeling however the column is sized. */
  width: 100%;
  max-width: 11.6rem;          /* the photo's 10rem, plus a tail either side */
  height: auto;
  margin: 0 auto 0.35rem;      /* it sits ON the picture, so the gap is below */
}

/* The credit sits under the man's name, smaller and lighter — a photo credit is
   not part of the caption, it is the line beneath it. Its own line so a long
   photographer's name cannot push the subject's name onto two. */
.chron-credit {
  display: block;
  margin-top: 0.15rem;
  font-weight: 400;
  font-size: 0.52rem;
  letter-spacing: 0.03em;
  text-transform: none;
  opacity: 0.72;
}
.chron-where { white-space: nowrap; }
.chron-nick { font-weight: 400; font-style: italic; text-transform: none; letter-spacing: 0; }
.chron .recap-body {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  color: var(--ink);
  font-family: Georgia, "Times New Roman", serif;
  column-gap: 1.8rem;
  column-rule: 1px solid rgba(30, 30, 24, 0.22);
}
.chron .recap-lede::first-letter { color: var(--ink); }
/* THE CITY'S KERNING (Brian, s27: "fix the kerning of the name of the city").
   `.recap-dateline` is tracked 0.08em and set in the UI's sans, inherited from
   the dark-theme version where it is a label. In a newspaper it is the DATELINE
   — the first words of the story — and it was sitting loose and pale-looking
   against the serif that follows it, with the tracking opening gaps a reader
   reads as broken spacing rather than as emphasis.
   Small caps in the body's own serif does the job the tracking was reaching
   for: it is bigger than lowercase, quieter than full caps, and it kerns
   properly because the face draws the forms. `font-variant-caps` rather than
   `text-transform`, so the text stays "New York" for the copy document and it
   is the FACE that sets it. */
.chron .recap-dateline {
  color: var(--ink);
  font-family: Georgia, "Times New Roman", serif;
  font-variant-caps: all-small-caps;
  font-weight: 700;
  font-size: 1em;
  letter-spacing: 0.02em;
}
/* NO EMPTY SPACE IN THE COLUMNS (Brian, s27). `.recap-body p` carries
   `break-inside: avoid` from the dark-theme write-up, where four short
   paragraphs read better whole than split. In two newspaper columns that is
   exactly what leaves a hole: a paragraph that will not fit the remainder of
   column one is pushed WHOLE into column two, and column one ends early.
   A NEWSPAPER BREAKS PARAGRAPHS ACROSS COLUMNS — that is what a column is — so
   inside the Chronicle they may split, and the text fills both. */
.chron .recap-body p { color: var(--ink); break-inside: auto; }
@media (max-width: 620px) {
  /* The flag is the one row that must not wrap: a logo on one line and the
     wordmark on the next stops reading as a masthead. It shrinks instead. */
  .chron { padding: 1rem 0.9rem 1.2rem; }
  /* A 34% picture beside a one-column body is a stamp beside a paragraph. The
     art goes ABOVE the text on a phone, where it reads as a front-page photo
     rather than as a margin ornament, and the caption comes back under it. */
  .chron-cols { grid-template-columns: 1fr; gap: 0.9rem; }
  /* 62% predated the 10rem desktop cap and left the phone's crest LARGER
     than the desktop's — 220px against 160. Sized to sit under the flag
     without competing with it. */
  .chron-art { order: -1; max-width: 44%; margin: 0 auto; }
  .chron-cap { text-align: center; }
}

.box-wrap {
  display: flex;
  gap: 2rem;
  flex-wrap: wrap;
  margin-top: 1.25rem;
}
.potg {
  margin: 1rem 0 1.4rem;
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: rgba(245, 197, 66, 0.10);
  font-size: 0.9rem;
}
.potg strong { color: var(--accent); margin-right: 0.4rem; }
/* Same floor, same fix: the pitching box is nine columns and measures 341 at
   its narrowest, against a 285-wide column on a phone. It was overflowing here
   too — masked by the line score, which was wider still. The scroll box is the
   TEAM column rather than each table, so one rule covers every renderer that
   builds a `.box` (the game box, the series book, the leader boards, the stat
   sheets, and the live box in the dugout). */
.box-team { flex: 1 1 340px; min-width: 0; overflow-x: auto; }
.box-team h3 { color: var(--accent-2); font-size: 0.95rem; margin-bottom: 0.4rem; }
.box {
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  width: 100%;
  margin-bottom: 0.6rem;
}
/* TIGHT ENOUGH NOT TO SCROLL (Brian, s36: "dont want the pitcher box score to
   scroll, tighten the columns"). The pitching line is fifteen columns wide and
   the horizontal padding was doing most of the damage — 0.5rem a side on every
   cell is 15rem of air before a single digit is drawn, which pushed HR and HB
   off the right edge and put the whole table in a scroller.
   The numbers are `tabular-nums` already, so they need no room to breathe. */
/* A SPLIT IS ONE TOKEN, not a number — "41-18" must not be broken across a line
   or line up its hyphen with the column beside it. */
.standings .std-split { white-space: nowrap; font-variant-numeric: tabular-nums; }

.box th, .box td {
  padding: 0.22rem 0.3rem;
  text-align: right;
  border-bottom: 1px solid var(--line);
  font-size: 0.85rem;
}
.box th { color: var(--muted); font-weight: 600; }
/* The same pin, on the same reasoning, for the box tables themselves: the
   pitching box is 576px against a 262px window on a phone, and scrolling to the
   strikeouts left every row anonymous. `.ss-grid` keeps its own rule because
   its name column is the SECOND cell, not the first. */
/* THE NAME COLUMN DOES NOT CLIP, AND THAT IS THE WHOLE RULE (Brian, s36: "during
   season play, the W and L was not recorded" — with a box score showing "Adam
   Morgan RP …").
   IT WAS RECORDED. `.box-dec` — the W, the L, the save, the hold — is INSIDE the
   name cell, so the `text-overflow: ellipsis` added an hour earlier to stop the
   table scrolling was eating the decision itself. A pitcher's win is not
   decoration that can be trimmed to fit; it is the one thing on his line a
   reader is looking for.
   The width came from the padding, which is where the fix stayed: 0.3rem a side
   across fifteen columns. `nowrap` still keeps a row on one line. */
.box .tname {
  text-align: left; position: sticky; left: 0; z-index: 1; background: var(--panel);
  white-space: nowrap;
}
.box .tname em { color: var(--muted); font-style: normal; font-size: 0.72rem; margin-left: 0.2rem; }
.box .totals td { font-weight: 700; color: var(--accent); border-top: 2px solid var(--line); }
/* THE PITCHING TABLE SIZES TO ITS CONTENTS (s23, Brian: "narrower columns for
   pitcher box score"). `.box` is `width: 100%`, which was right when a club sat
   in a 340px column beside the other one; with the card in the block the team
   column is the full page, and nine numbers were being spread across all of it.
   `width: auto` lets the columns be as wide as the figures in them.
   THE FLOOR IS A QUARTER WIDER THAN THE FIRST CUT (Brian: "wide the pitching
   box score by 25%") — 22rem measured 395px against the card above it, which
   read as cramped rather than compact. */
.box-pitch { width: auto; min-width: 36rem; max-width: 100%; }
.box-pitch th, .box-pitch td { padding: 0.22rem 0.42rem; }
/* THE STAT COLUMNS ARE EVEN AND A QUARTER WIDER (Brian: "pitcher box score,
   widen the stats columns by 25%"). They were sized by their contents, so `R`
   and `K` came out at 29px against `P` at 51 — a ragged right half of the table.
   One floor, 25% up on the old average, and the name column still takes what it
   needs. */
.box-pitch th:not(.tname), .box-pitch td:not(.tname) { min-width: 3.1rem; }
.box-pitch .tname { padding-right: 1.1rem; }
/* A section label above a table, set like the notes headings under it — the
   page has one kind of section header, not two. */
.box-sec { margin-bottom: 0.15rem; }
.box-notes { color: var(--muted); font-size: 0.78rem; margin: 0.15rem 0 0.55rem; line-height: 1.5; }
.box-dec {
  display: inline-block;
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  padding: 0.02rem 0.28rem;
  border-radius: 4px;
  vertical-align: middle;
  /* The badge carries a RECORD now (s21) — "W, 4-1", "H, 5" — so it must not
     break across lines mid-decision, and the digits line up with every other
     number in the box. */
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.box-dec.win { background: var(--accent-2); color: #10231a; }
.box-dec.loss { background: var(--panel-2); color: var(--muted); border: 1px solid var(--line); }
.box-dec.sv { background: var(--accent); color: #1a1a10; }
.box-dec.hld { background: var(--panel-2); color: var(--accent-2); border: 1px solid var(--accent-2); }
.box-dec.bs { background: var(--panel-2); color: var(--danger); border: 1px solid var(--danger); }
.box-dec.cg { background: var(--panel-2); color: var(--accent); border: 1px solid var(--accent); }

/* Glove grade stars (roster rows + sub pickers) */
/* THE SCOUTING LINE. Baseball's own 20-80 scale, so the chip reads without a
   legend: 50 is average, 80 is the best of his generation. The color is a
   RAMP rather than three buckets, because the whole point of replacing the
   star was that 65 and 75 are different answers. */
.glove { font-size: 0.62rem; letter-spacing: 0; margin-left: 0.25rem;
         vertical-align: middle; white-space: nowrap; }
.gr-lbl { color: var(--muted); font-size: 0.52rem; letter-spacing: 0.06em;
          margin: 0 0.12rem 0 0.28rem; }
.gr-lbl:first-child { margin-left: 0; }
.gr { display: inline-block; min-width: 1.55em; text-align: center;
      padding: 0.06rem 0.22rem; border-radius: 3px; font-weight: 700;
      font-size: 0.66rem; font-variant-numeric: tabular-nums; }
.gr80, .gr75 { background: #2f6b3f; color: #c8f0d2; }
.gr70 { background: #2b5c39; color: #bfe8ca; }
.gr65, .gr60 { background: #294c33; color: #a9d6b6; }
.gr55, .gr50, .gr45 { background: #2a3630; color: #9db3a3; }
.gr40, .gr35 { background: #4a3526; color: #e0bb9a; }
.gr30, .gr25, .gr20 { background: #5e2f2b; color: #f0bcb6; }
/* NO RATED SEASON AT THIS SPOT (s17). Same shape as a real grade so the column
   still lines up, and deliberately dim: it has to read as ABSENT rather than as
   a low grade, which is the one way an unrated man could be misread. The star
   it replaces was a second METHOD masquerading as a second opinion. */
.glove-none { opacity: 0.55; }
.gr-none { background: none !important; color: var(--muted) !important;
  border: 1px dashed var(--line); }
/* The reason behind the stars, spelled out in the roomier pulldown rows */
/* A rating whose inputs are weak still shows, but it stops shouting: the era
   or the sample is named in the tooltip and the stars go quiet. Withholding it
   outright was the other option and it is worse — a blank where every other
   player has a grade reads as broken rather than as honest. */
.glove-soft { opacity: 0.62; }
.glove-why { font-size: 0.62rem; color: var(--muted); margin-left: 0.25rem; white-space: nowrap; }
/* The engine handing the pitch call back mid-plate-appearance, because the
   other dugout just pinch-hit or ran. Says why, so it doesn't read as a repeat. */
.mg-reask { margin-bottom: 0.6rem; padding: 0.4rem 0.7rem; border-radius: 6px;
  background: rgba(255, 200, 60, 0.12); border-left: 3px solid var(--accent-2);
  font-size: 0.85rem; color: var(--accent-2); }
/* The coach's read on a re-ask, under the sentence saying what happened. Its
   OWN LINE rather than trailing the fact, because the fact is the headline —
   you need to know a ball got away before you need anyone's view about it.
   Same italic-and-attributed treatment as `.tt-coach`, so his voice looks the
   same wherever it turns up; a different color here only because this banner
   is already accent-2 and gold on gold would vanish. */
.mg-reask-coach { display: block; margin-top: 0.2rem;
  font-style: italic; color: var(--muted); font-size: 0.95em; }
/* Steal threat on a base runner: one bolt for a man who picks his spots, three
   for a burner. Nothing at all for a station-to-station runner, so the common
   case stays quiet and the guy you have to worry about jumps out. */
/* Degree is carried by the NUMBER of glyphs, so it must not also be carried by
   opacity: a lone bolt at 55% read as muted yellow rather than as "one bolt".
   Every tier renders at full strength; the burner still stands out by his glow
   and his count, and the quiet case is ZERO glyphs, not faint ones. */
.mg-bolt { font-size: 0.72em; letter-spacing: -0.12em; cursor: help; vertical-align: 0.06em; }
.mg-bolt.t3 { filter: drop-shadow(0 0 3px rgba(255, 200, 60, 0.8)); }
/* Pull-tendency arrows: same read-at-a-glance grammar as the steal bolts, but
   they point at the field the hitter actually uses. Gold = pull it (shift bait),
   blue = opposite-field (shifting him backfires). Same no-opacity-ramp rule as
   the bolts above. */
.mg-pull { font-size: 0.78em; letter-spacing: -0.08em; cursor: help; vertical-align: 0.08em; }
.mg-pull.pull { color: var(--accent); }
.mg-pull.oppo { color: #7fb2ff; }
.mg-pull.t3.pull { filter: drop-shadow(0 0 3px rgba(245, 197, 66, 0.7)); }
/* A true half star: a filled star clipped to its left half over an outline star */
.box-notes strong { color: var(--text); }
/* THE SECTION HEADERS ARE LIGHT GREEN (s23, Brian: "in box score pitching,
   fielding and batting, header should be light green") — and they were MEANT to
   be all along. `.box-notes-h` has carried `--accent-2` since it was written and
   never once rendered it: `.box-notes strong` sets `--text` at (0,1,1) against
   this rule's (0,1,0), so the more specific selector won and every one of these
   headers came out near-white. An override has to OUT-SPECIFY what it overrides;
   the class alone does not. */
.box-notes strong.box-notes-h { color: var(--accent-2); }
.box-notes-h { color: var(--accent-2); text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.72rem; }

#pbp-wrap { margin-top: 1rem; }
#pbp-wrap summary { cursor: pointer; color: var(--muted); }
.pbp-line { padding: 0.15rem 0; font-size: 0.88rem; }
.pbp-line .half { color: var(--muted); font-size: 0.75rem; margin-right: 0.4rem; }
.pbp-line.home .half { color: var(--accent-2); }

.winbar {
  height: 18px;
  border-radius: 9px;
  background: var(--accent-2);
  overflow: hidden;
  margin: 0.5rem 0;
  max-width: 480px;
}
.winbar .away-fill { height: 100%; background: var(--accent); }

/* ---- Season campaign ---- */
#campaign {
  max-width: 1100px;
  margin: 0 auto 1.25rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 1.25rem;
}
#campaign h3 { color: var(--accent-2); font-size: 1.05rem; margin: 0.5rem 0; }
#campaign-exit {
  background: var(--panel-2);
  color: var(--danger);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.35rem 0.8rem;
  cursor: pointer;
  font-size: 0.85rem;
}
.camp-pick { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.75rem 0; }
.camp-radio { cursor: pointer; font-size: 0.95rem; }
.camp-radio input { accent-color: var(--accent); margin-right: 0.4rem; }
.camp-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; margin: 0.9rem 0; }
.camp-standings { margin-top: 0.75rem; }
/* WHITE, NOT GOLD (Brian, s36: "user's team should be in white in the season
   standings"). Gold is this app's ink for a RUN — the scoreboard, the run clause,
   the save badge — so the club you manage was reading as a result rather than as
   a marker of who you are. `--text` is the page's own white and the bold alone
   carries "this is you", which is all the row has to say. One rule rather than a
   standings-only override, because `camp-me` marks the same thing on the wild
   card table, the leaderboard and the schedule. */
.camp-me .tname { color: var(--text); font-weight: 700; }
/* A LEADERBOARD ROW IS A CONTROL NOW (David, s39) — it opens the man's season.
   The affordance is on the NAME, not the whole row: the s38 rule from the bench
   list, where washing the full width moved far more ink than the pointer was
   asking about. `stat-picked` is the open one. */
.leader-box tr[data-player] { cursor: pointer; }
.leader-box tr[data-player]:hover .tname { color: var(--accent); }
.leader-box tr[data-player]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.leader-box tr.stat-picked .tname { color: var(--accent); font-weight: 700; }
.era-toggle select {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.2rem 0.35rem;
  font-size: 0.8rem;
  margin-left: 0.3rem;
}

/* ---- Manager mode ---- */
#manage {
  /* WIDENED WITH THE ROSTER COLUMNS (Brian, s17), by exactly what they took.
     The columns went 268 -> 320 and this went 1240 -> 1344, so the CENTER
     COLUMN IS UNCHANGED at ~675px — the field diagram, the matchup cards and
     the play-by-play all keep the width they were laid out against, and only
     the rosters got the room. Widening the columns alone would have paid for
     legible names with a smaller ballpark. */
  max-width: 1344px;
  margin: -0.5rem auto 3rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  /* The scale below reads this; it is 1 until a step sets it. See `zoom`. */
  --mg-zoom: 1;
}

/* THE DUGOUT SCALES TO THE WINDOW (Brian, s32, off his own mockup: "everything
   needs a little more room, but not too much that it falls under the fold").

   MEASURED, BOTH SIDES. On a 1865px window the rule above left 521px of empty
   margin — the banner, both roster columns and the center panel all stopped
   short of the edges. His mockup runs edge to edge, and it is NOT a re-layout:
   the column shares are the ones that already ship (left 23.1% against 23.8%,
   center 50.4% against 50.2%) and the roster row pitch is 41.4px against 29.5,
   a ratio of 1.40 that the dugout width repeats at 1865/1344 = 1.39. The
   mockup is THIS dugout at 1.4x, which is what filling his window comes to.

   WHY `zoom` AND NOT 340 EDITED VALUES. A uniform scale is exactly what was
   asked for, and the block below this one holds 164 rem and 176 px values,
   most of them carrying a comment that states the number and what it was
   measured against. Multiplying all of them would make every one of those
   comments a lie and would be 340 chances to get one wrong; `zoom` scales
   layout AND type together, keeps hit-testing honest (unlike `transform`),
   and every documented figure below stays true in the dugout's own units.

   IT IS STEPPED, NOT FLUID, because the step has to clear `max-width` above:
   at 1.4x the 1344px cap wants 1930px of window, and below that the cap simply
   stops binding and the CENTER column absorbs the shortfall (at 1865 it lays
   out 633 logical px against 675, and renders 886 physical against today's
   675 — the rosters gain 448 against 320). Nothing overflows at any step;
   verified at 1440, 1500, 1700, 1865 and 2560.

   AND IT STOPS AT THE FINAL SCREEN. `.mg-final` is on `#manage` itself, so
   `:not()` leaves the verdict card, the Chronicle, the box score and the
   SCORESHEET at the size they were laid out against — the scoresheet's column
   work and its print stylesheet are the last things that want a scale factor
   underneath them. `screen` for the same reason: a printed page is ~816px and
   would not match these anyway, but saying so is cheaper than finding out.

   THE FOLD IS THE CEILING, and 1.4 is where it sits. At 1865x1035 everything
   you play with — situation strip, matchup cards, odds, approach, the action
   buttons, alignment and auto-manage — ends at 890px with 145px to spare, and
   the field diagram starts at 901 and runs under the fold. That is where his
   mockup has it too (the ballpark crest peeking in at the bottom edge). */
@media screen and (min-width: 1500px) {
  #manage:not(.mg-final) { zoom: 1.15; --mg-zoom: 1.15; }
}
@media screen and (min-width: 1700px) {
  #manage:not(.mg-final) { zoom: 1.28; --mg-zoom: 1.28; }
}
@media screen and (min-width: 1850px) {
  #manage:not(.mg-final) { zoom: 1.4; --mg-zoom: 1.4; }
}

/* Top bar: a small "Your Team" label over the user's lineup, the linescore in
   the middle, and a compact exit pinned to the corner. */
.mg-topbar { position: relative; margin-bottom: 0; }
/* Exit sits inside the banner, vertically centered against its right edge. */
.mg-head { position: absolute; top: 50%; right: 1.4rem; transform: translateY(-50%); z-index: 5; }
#mg-scoreboard {
  /* Wide banner: the line score (name column + 9 innings + R/H/E) is centered
     in the box, with the Hot Seat badge pinned at the left. Fills the manage
     column; `safe center` keeps the first digit from clipping while it scrolls
     on a narrow screen. */
  position: relative;
  width: 100%; margin-inline: auto;
  /* NAMED so the pinned club-name cell can be painted the same green — see
     `.mg-line tr > *:first-child`. A sticky cell needs an opaque background and
     the board is the only right answer for it; two literals would be two
     answers, and one of them would eventually be wrong. */
  --board: #4c7340;
  background: var(--board); border: 3px solid #33502b; border-radius: 10px;
  padding: 0.3rem 1.4rem;
  overflow-x: auto; display: flex; align-items: center;
  justify-content: safe center;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
/* THE PADDING RESERVES THE TWO OUT-OF-FLOW BLOCKS — the badge pinned left and
   `.mg-head` pinned right. Both are absolutely positioned, so the centering
   cannot see them, and the line score was free to slide underneath: measured at
   1280, `.mg-head` already sat 14px on top of it before the Stats button
   existed, and the button took that to 72.
   MEASURED, NOT GUESSED, and re-measured after the Help dropdown came out —
   which is the point. The first cut reserved 18rem on the right for a 259px
   head; removing Help took the head to 130px and that same 18rem then shoved
   the line score 24px UNDER THE BADGE, trading one collision for its mirror.
   At 10rem each side the badge clears by ~109px and the head by ~111px, with
   925px left for a 705px table.
   ONLY ABOVE 900px. Below it the board scrolls anyway (`overflow-x` plus
   `safe center`), and 320px of reserved padding on a phone would just mean
   scrolling past empty green to reach the score. Same specificity as the rule
   above and later in source order, so it wins on order — deliberately not
   relying on a media query to add any, which it does not. */
@media (min-width: 900px) {
  #mg-scoreboard { padding-inline: 10rem; }
}
/* AND THE PHONE GETS THE COLLISION THE RULE ABOVE EXISTS TO PREVENT (Brian, s25:
   "play a game and check the office on mobile"). "Below it the board scrolls
   anyway" is true of the SCORE and false of the two overlays: they are pinned to
   `.mg-topbar`, so they do not scroll with the board — they sit still while the
   table slides under them. Measured at 375: a 690px table in a 321px box, its
   first column starting at x=49 and the badge painted over 49..179, with Exit
   over 243..329. The club names and the first three innings were unreadable.
   THE BADGE COMES OFF: it is decoration, the mark is already on the page the
   manager came from, and 130px of it is 40% of a 321px board.
   EXIT COMES OUT OF THE OVERLAY INSTEAD OF BEING RESERVED FOR. Reserving its
   width works — but only at the END of the scroll travel, and at rest the name
   column is 196px against 194px of clear runway, so "1927 New York Yankees" lost
   its last letter under the pill. A reserve cannot fix that, because the thing
   it is reserving against is painted OVER the box rather than laid out in it.
   Taking it back into flow above the board removes the overlap at every scroll
   position rather than at one, and gives the score the full width. It stays
   above the fold and one tap away, which is the whole reason it was pinned. */
@media (max-width: 899px) {
  .mg-logo { display: none; }
  .mg-head {
    position: static; transform: none;
    display: flex; justify-content: flex-end;
    margin-bottom: 0.4rem;
  }
  /* AND IT COMES BACK ONCE THE GAME IS OVER (Brian, s27: "unhide the logo on the
     final screen for mobile"), which is also the screen where it is a way home.
     IT IS NOT SIMPLY UN-HIDDEN. The reason s25 took it off a phone was not that
     it was unwanted — it was that a 130px badge PAINTED OVER the line score,
     absolutely positioned at the left edge while a 690px table slid underneath
     it, and the club names and first three innings were unreadable. Un-hiding it
     as it stands would put that back.
     SO IT GOES INTO FLOW — BUT THE BOARD HAS TO STACK FOR THAT TO MEAN
     ANYTHING. `#mg-scoreboard` is `display: flex`, so `position: static` alone
     does not put the badge ABOVE the table, it makes it a flex ITEM BESIDE the
     table — and against a 690px line score it was squeezed to a few pixels and
     rendered as two horizontal streaks. Being the first child is only "above"
     in a block or a column. So the board turns to a column on this screen and
     the badge stacks over the score, centered with it.
     Smaller than the desktop's 66px, because a phone's board is 327px wide and
     the mark is a label on the game, not the subject of it.
     AND THE CENTERING HAS TO STAY `safe`. Turning the board into a column moves
     `justify-content: safe center` onto the VERTICAL axis, so the horizontal
     centering falls to `align-items` — and a plain `center` on an item wider than
     its scroll box pushes the item's start edge outside the scrollable area.
     Measured: the table began at x=-157 against a board at x=24 with
     `scrollLeft` already 0, so 181px of line score — the club names and the
     first innings — could not be reached by scrolling at all. `safe` is what the
     row had and what the column needs; it falls back to start alignment the
     moment centering would cause that loss. */
  .mg-final #mg-scoreboard { flex-direction: column; align-items: safe center; }
  .mg-final .mg-logo {
    display: block;
    position: static; transform: none;
    height: 40px; margin: 0 auto 0.4rem;
    flex: 0 0 auto;          /* a column item must not be stretched or shrunk */
  }
}
/* The Hot Seat badge, pinned to the left edge (out of flow) so the line score
   stays centered in the full box. */
.mg-logo {
  position: absolute; left: 1.4rem; top: 50%; transform: translateY(-50%);
  height: 66px; width: auto; filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.45));
}
/* THE TOPBAR PAIR SPEAK THE PAGE'S OWN VOCABULARY (Brian, s21: "match style of
   the other buttons"). They were 8px corners at 0.72rem in `--panel` while every
   other button in the app is a pill — they read as browser chrome sitting on the
   scoreboard rather than as controls of this app.
   PILLS, at the scale of the row they sit in: the topbar is dense, so they take
   the size the dugout's own small controls take rather than `.mg-btn`'s, and get
   its shape and its hover. Exit keeps the danger ink — it is still the one that
   ends the game — and the book keeps the app's ordinary text, because opening it
   is not destructive and should not read as though it were. */
#mg-exit, #mg-stats, #mg-bug {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
#mg-exit { color: var(--danger); }
#mg-stats { color: var(--text); margin-right: 0.4rem; }
#mg-exit:hover { border-color: var(--danger); }
#mg-stats:hover { border-color: var(--accent-2); color: var(--accent-2); }
/* An anchor dressed as the pair's pill: it opens a tab, so it is a link. Muted,
   because it is the one control in the row that is not about the game.
   ITS OWN LINE, UNDER THE PAIR, where the head floats over the board. The head
   is out of flow and the line score slides under it — measured at 950 in the
   row it took Exit's existing 61px overlap of R/H/E to 173. Stacked, the head is
   no wider than the pair, and the 92px board has the height for two lines.
   On the narrow layout the head is a flex row in flow, so `order` puts it back
   beside the others, first. */
#mg-bug {
  display: block; width: max-content; margin: 0.3rem 0 0 auto;
  color: var(--muted); text-decoration: none; font-family: inherit;
  /* A size down from the pair, so the stack is no wider than Exit alone. */
  font-size: 0.68rem; padding: 0.2rem 0.65rem;
}
/* The narrow layout's head is a flex row in flow (see the 899px block above),
   so the pill goes back in the row, first, at the pair's size. After the base
   rule on purpose: same specificity, so it wins on order. */
@media (max-width: 899px) {
  #mg-bug {
    order: -1; margin: 0 0.4rem 0 0; display: inline-flex; align-items: center;
    font-size: 0.75rem; padding: 0.3rem 0.85rem;
  }
}
#mg-bug:hover { border-color: var(--accent-2); color: var(--accent-2); }
#mg-exit:focus-visible, #mg-stats:focus-visible, #mg-bug:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* THE CLUB'S RECORD ON THE SCOREBOARD. Deliberately NOT in the seven-segment
   face the run columns use — that font has no useful hyphen and this is a
   caption about the club, not a number on the board. Quiet enough that the
   line score still reads first. */
.mg-rec {
  font-family: inherit; font-size: 0.68rem; font-weight: 600;
  color: rgba(255, 255, 255, 0.62); letter-spacing: 0.02em; white-space: nowrap;
}
.mg-line { border-collapse: separate; border-spacing: 5px 2px; }
.mg-line th {
  color: #fff; font-weight: 800; font-size: 0.82rem; letter-spacing: 0.05em;
  padding: 0 2px 2px; text-transform: uppercase; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
}
.mg-line th.now { color: var(--accent); }
.mg-line td {
  background: #17130d; color: #ee4a39;
  font-family: "DSEG7 Classic", "Courier New", ui-monospace, monospace;
  font-weight: 700; font-size: 1.1rem; text-align: right; min-width: 1.9em;
  padding: 3px 8px 2px; border-radius: 4px;
  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.85);
  text-shadow: 0 0 7px rgba(238, 74, 57, 0.5);
}
/* THE CURRENT INNING'S RING IS DRAWN INSIDE THE CELL (Brian, s25: "chase the
   gold tick"). It was an OUTER shadow, and an outer shadow is painted outside
   the cell's border box — which put it outside what the pinned club-name column
   covers. Scrolled to the far end, the ring on a cell that had passed under the
   pin showed as two gold ticks at the board's left edge, the only thing on the
   whole strip that the pin did not hide.
   RAISING THE PIN'S `z-index` DOES NOT FIX IT and was tried: the ring is not
   losing a stacking contest, it is being drawn somewhere the pin's own box never
   reaches. Nor does covering the strip — an absolutely positioned overlay inside
   a scroll container SCROLLS WITH THE CONTENT, so it slid away with everything
   else and was never over the ticks at all.
   `inset` puts the ring within the cell's own bounds, so it is occluded by the
   pinned column exactly as the digits are. Identified by elimination: switching
   this one declaration off made both ticks vanish, and nothing else did. The
   ring reads the same at rest — verified on the live current inning. */
.mg-line td.now { color: #ff6a4d; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.85), inset 0 0 0 1.5px var(--accent); }
/* THE LIVE HALF-INNING'S ZERO IS UNLIT UNTIL SOMEBODY SCORES (Brian, s35).
   Same red, a third of the way down toward the cell's own background, with the
   glow pulled back to match — so it still reads as the same lamp rather than as
   a different color, which is what a dimmer on a seven-segment board looks
   like. `.pend` must out-specify `.now` and does, by one class.
   THE BRIGHTEN IS A KEYFRAME AND NOT A TRANSITION, because the board is redrawn
   from `innerHTML` and a transition needs an element that survives the change.
   An animation on a freshly inserted node plays on insert, which is exactly the
   moment wanted; `renderMgScoreboard` sets `.lit` only when the previous render
   held a SMALLER number for the same half. It overshoots into amber before
   settling, the way a filament does. */
.mg-line td.now.pend { color: #8f3327; text-shadow: 0 0 4px rgba(238, 74, 57, 0.22); }
@keyframes mg-run-lit {
  0%   { color: #8f3327; text-shadow: 0 0 4px rgba(238, 74, 57, 0.22); }
  30%  { color: #ffd9a0; text-shadow: 0 0 15px rgba(255, 170, 90, 0.95); }
  100% { color: #ff6a4d; text-shadow: 0 0 7px rgba(238, 74, 57, 0.5); }
}
.mg-line td.now.lit { animation: mg-run-lit 0.75s ease-out 1; }
@media (prefers-reduced-motion: reduce) { .mg-line td.now.lit { animation: none; } }
/* "X" for an unplayed half-inning — a plain glyph, not the seven-segment face */
.mg-line td.np { font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif; font-weight: 700; color: #9a6f3e; text-shadow: none; }
.mg-line .tname {
  background: transparent; box-shadow: none; text-shadow: none; min-width: 0;
  font-family: inherit; color: #fff; font-weight: 800; font-size: 0.92rem;
  letter-spacing: 0.02em; text-align: left; padding: 0 0.6rem 0 0; white-space: nowrap;
}
/* WHICH CLUB IS YOURS IS SAID ONCE, IN WORDS (Brian, s28). It arrived as "an
   indicator ... a color change to gold?", shipped as gold on the board plus a
   ▸, lost the ▸ ("just color change works"), and then lost the color too:
   "no gold in the color in the scoreboard and not gold line above the lineup
   but make the 'Your Team' 2 points larger and space the lettering out."
   THE LABEL WAS ALREADY THERE — `.mg-you` sits above the panel and says it in
   plain words. Three markers for one fact is two too many, and the board is a
   seven-segment display where a second color reads as a state (the lit inning,
   the run total) rather than as ownership. The `.mine` class is still set in JS
   and still drives the panel; nothing on the BOARD hangs off it now. */
.mg-line .tot { color: #ff5f45; }
/* AND THE SCOREBOARD'S CLUB NAME STAYS PUT TOO (Brian, s25). The same complaint
   as the box score's line score, showing up as its other half: this one already
   had `white-space: nowrap`, so it never wrapped — it just scrolled away. At 375
   the board is a 321px window onto 735px of table, so reading the late innings
   meant reading rows of unlabeled digits and counting which was which.
   FIXED IN ONE PLACE AND NOT ITS SIBLING is the shape this file keeps recording,
   so both are pinned in the same session rather than one of them being found
   again later. `--board` rather than a second `#4c7340`. */
.mg-line tr > *:first-child {
  position: sticky; left: 0; z-index: 1;
  background: var(--board);
  /* THE BOARD HAS PADDING AND THE PINNED CELL DOES NOT REACH IT. `left: 0` pins
     to the scrollport, but `#mg-scoreboard` carries 1.4rem of its own padding,
     so the digit cells kept sliding through that strip to the LEFT of the club
     name — seven-segment slivers on the green, beside a name that was supposed
     to be the leftmost thing on the board. A shadow the same color, offset far
     enough left to cover any padding this board is given, and no offset right so
     it cannot touch the digits it is scrolling over. Painting rather than layout,
     so nothing about the table's widths moves. */
  box-shadow: -100px 0 0 var(--board);
}
/* Parts the nine innings from R/H/E. An empty cell rather than padding, because
   the digit cells carry their own background and radius — padding would widen
   the box instead of opening a gap between the two groups. */
.mg-line .lsgap { background: transparent; box-shadow: none; text-shadow: none;
  min-width: 12px; width: 12px; padding: 0; }
.mg-line .rhe { color: #d3e2c8; }

/* Latest-play announcement, between the situation strip and the field. Colored
   and popped by outcome so a big hit lands differently than a routine out. */
.mg-announce {
  display: flex; align-items: center; justify-content: center; gap: 0.6rem;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; padding: 0.5rem 1rem; text-align: center; transform-origin: center;
}
.mg-announce[hidden] { display: none; }
.mg-ann-text { font-size: 0.92rem; font-weight: 600; color: var(--text); }
/* A SENTENCE WRAPS AS A UNIT (Brian, s33: "don't break sentences in the
   situation box. Hard line break if the sentence is not gonna fit"). An
   inline-block is the whole mechanism and it is exactly the behavior asked
   for — the sentences sit side by side while they fit, and one that does not
   fit moves down whole instead of splitting across the break.

   `inline-block`, NOT a flex or grid row: the sentences must still be TEXT to
   each other, sharing a baseline and a normal word space, and a line with one
   sentence has to be indistinguishable from what it was before. `wrapSentences`
   emits no wrapper at all in that case, so this rule never applies to it.

   IT INHERITS THE MARKS RATHER THAN CARRYING THEM. `.mg-ann-k` and friends sit
   INSIDE these spans (every mark is sentence-internal or sentence-final, which
   is what makes the split safe at all), so the color rules above are untouched
   and none of them needed a descendant selector adding. */
.mg-ann-s { display: inline-block; }
/* THE STRIKEOUT HALF OF A DROPPED THIRD STRIKE (Brian, s29). The batter struck
   out and then reached first, and the two facts are one sentence each — so the
   out reads as an out and what follows it stays white. No specificity contest:
   a color set ON the span beats the color its parent merely passes down. */
.mg-ann-k { color: var(--muted); }
/* THE RUNNER'S HALF OF AN OUT (Brian, s30). "Frank Duncan strikes out, but
   Francisco Correa steals second" is one line and two events: the batter is out
   and `.mg-announce.out` mutes the line, but the runner is standing on second
   and that clause should read like it. Set ON the span, so the line's own kind
   is inherited-over rather than out-specified. */
.mg-ann-safe { color: var(--text); }
/* ...EXCEPT UNDER THE HALF-OVER LINE, where the whole play is already held back
   and a second, brighter gray inside it would undo that. `inherit` takes the
   spent color rather than restating it, so the two cannot drift apart. The
   same applies to the runner's clause: a white steal inside a held-back play
   would be the loudest thing on a line that is deliberately quiet. */
.mg-ann-text.mg-ann-spent .mg-ann-k,
.mg-ann-text.mg-ann-spent .mg-ann-safe { color: inherit; }
/* THE HALF ENDING, ON ITS OWN LINE (Brian, s25). THE BANNER IS A FLEX ROW, so
   `display: block` on the child does nothing — a flex item is laid out by the
   container, not by its own display. `flex-basis: 100%` with the container
   allowed to wrap is what actually breaks the line, and it is why this is not
   the one-liner it looks like it should be. A `<br>` in the string would work
   too and would then have to be carried through the copy document, which is
   worse. SENTENCE CASE, NOT ALLCAPS (Brian, s25: "On to the bottom of the 5th.
   not all caps"). It shouted for one commit; naming the half you are going to
   is a piece of information, not an announcement, and the gold and the weight
   already carry it.
   It does NOT take the banner's per-play color — an out banner is muted gray,
   and the one line that says where the game has moved to should not be the
   quietest thing in the box. */
.mg-announce:has(.mg-ann-over) { flex-wrap: wrap; }
.mg-ann-over {
  flex: 0 0 100%; margin-top: 0.15rem;
  font-size: 0.88rem; font-weight: 700; color: var(--accent);
}
/* THE PLAY THAT ENDED THE HALF, HELD BACK (Brian, s29: "gray it out more than
   an usual out"). It sits under the gold line naming the half you are going to,
   so it has to read as the play you have already watched rather than compete
   with the fact that is new.
   IT MUST OUT-SPECIFY EVERY PER-KIND RULE BELOW, because a half can end on any
   of them — an out, a double play, a caught stealing — and each of those sets
   `.mg-ann-text`'s color at (0,3,0). Four classes here, so this wins on
   specificity rather than on being further down the file.
   The s28 `:only-child` rule that used to sit here is GONE rather than kept:
   the play span is always rendered now, so `.mg-ann-over` can never be an only
   child and the rule could only ever have read as live. */
.mg-announce:has(.mg-ann-over) .mg-ann-text.mg-ann-spent { color: var(--spent); font-weight: 500; }
.mg-announce.hit, .mg-announce.steal { border-color: rgba(111, 208, 140, 0.5); background: rgba(111, 208, 140, 0.1); }
.mg-announce.hit .mg-ann-text, .mg-announce.steal .mg-ann-text { color: var(--accent-2); }
.mg-announce.hr { border-color: var(--accent); background: rgba(245, 197, 66, 0.16); }
.mg-announce.hr .mg-ann-text { color: var(--accent); font-weight: 800; font-size: 1.06rem; letter-spacing: 0.01em; }
.mg-announce.dp { border-color: var(--danger); background: rgba(224, 90, 74, 0.13); }
.mg-announce.dp .mg-ann-text { color: var(--danger); }
.mg-announce.out { border-color: rgba(120, 140, 125, 0.35); }
.mg-announce.out .mg-ann-text { color: var(--muted); }
.mg-announce.err { border-color: rgba(245, 197, 66, 0.45); background: rgba(245, 197, 66, 0.07); }
.mg-announce.walk { border-color: rgba(120, 160, 210, 0.45); }
/* Pitching change — a prominent, distinct blue banner so a bullpen move reads clearly */
.mg-announce.change { border-color: #6aa2d8; background: rgba(106, 162, 216, 0.16); }
.mg-announce.change .mg-ann-text { color: #9fc4ee; font-weight: 800; font-size: 1.06rem; letter-spacing: 0.01em; }
.mg-announce.change .mg-ann-text::before { content: "↻ "; }
.mg-announce.pop { animation: mg-ann-pop 0.42s cubic-bezier(0.18, 0.9, 0.32, 1.3); }
/* Broadcaster HR call — its own line, kept whole (no mid-phrase break) */
.hr-call { display: block; margin-bottom: 2px; font-style: italic; font-weight: 700; color: var(--accent); text-wrap: balance; }
.mg-announce.hr .hr-call { color: var(--accent); }
.pbp-play.hr .hr-call { color: var(--accent); }
@keyframes mg-ann-pop { 0% { transform: scale(0.9); opacity: 0.25; } 100% { transform: scale(1); opacity: 1; } }

/* Situation strip */
.mg-situation {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  /* AIR BETWEEN THE THREE GROUPS (Brian, s33: "I would like the three elements
     of the strip to breathe more"). 1.4rem -> 3.8rem, and it is a gap only the
     new base diamond can hold: three groups means TWO gaps, so every rem costs
     32px of width, and the old runner LIST needed 729px of a ~675px center
     column at its widest state before any of this. The diamond needs 507px and
     is still clear at 3.8rem.
     COLUMN ONLY. `gap` would set the ROW gap too, and this row wraps — on a
     phone that would put 50px of dead space between the stacked groups.
     3.8rem -> 3.04rem, "20% less space between elements" (Brian), once the
     groups were pinned to fixed positions and the air could be judged against a
     strip that stops moving. */
  column-gap: 1.5rem; row-gap: 0.6rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 0.5rem 1rem; font-size: 0.85rem; color: var(--muted);
}
/* THE HALF-INNING LEADS THE STRIP (Brian, s25: "make the half inning indicator
   brighter, bolder"). It was 0.95rem/700 in the same gold as everything else
   accented on the screen, so the one fact that orients you — whose half it is
   and how deep — carried no more weight than a chip. Bigger, heavier, and a
   LIGHTER gold: `--accent` at 62% luminance against a near-black panel is not
   actually bright, and lifting the tint reads as brightness where adding size
   alone would just read as bulk. The triangle comes with it. */
.mg-sit-inning {
  font-size: 1.15rem; font-weight: 800; color: #ffd968;
  letter-spacing: 0.01em;
}
/* THE OUTS READ A SIZE UP (Brian, s33: "make outs and the dots 10% larger").
   The word rides the strip's 0.85rem and the pips were 11px; both go up by a
   tenth. Scoped OFF `.mg-sit-park`, which shares this class and is an absolutely
   positioned crest that wants nothing to do with the outs' type scale. */
.mg-sit-grp { display: inline-flex; align-items: center; gap: 0.5rem; }
.mg-sit-grp:not(.mg-sit-park) { font-size: 0.935rem; font-weight: 700; }
.mg-pip { width: 12.1px; height: 12.1px; border-radius: 50%; border: 1.5px solid var(--line); display: inline-block; }
/* FILLING CHANGES THE INSIDE, NOT THE EDGE (Brian, s33). Both of these used to
   repaint the border `--danger` as well, so a pip lost the green rim the empty
   ones carry and the two states stopped being the same object in two conditions.
   Keeping `--line` means the ring is constant and only the fill reads as news. */
.mg-pip.on { background: var(--danger); }
/* THE BASE DIAMOND, WITH THE RUNNERS ON IT (Brian's sketch, s33). This replaced
   `.mg-basemini`, a 34px unlabeled diamond that sat beside a text list naming
   the same three men. Same geometry, scaled up enough to hang names off. */
.mg-basefield {
  position: relative; display: inline-grid; flex: none;
  /* SYMMETRIC SIDE COLUMNS, or the diamond is not where it says it is. With
     `auto` columns the third-base and first-base labels size themselves, so an
     uneven pair — "Gruber" against "Fernandez ⚡⚡⚡" — pushed the bag itself off
     center inside its own box. Measured across 135 renders of a real game: the
     diamond's center took TWELVE different x positions. `1fr` on both sides is
     what pins it, and it is what "fixed positions regardless of information"
     actually requires. */
  /* `minmax(0, 1fr)`, NOT `1fr`. A bare `1fr` is `minmax(auto, 1fr)` and that
     `auto` floor is min-CONTENT, so a long name grew its own column and shoved
     the bag sideways — the identical trap as `min-width: auto` on a flex item,
     one level down. Measured with plain `1fr`: the diamond's center still took
     six different positions (224.6 down to 192.3) across one game. */
  grid-template-columns: minmax(0, 1fr) 44px minmax(0, 1fr); grid-template-rows: auto 30px;
  align-items: center; justify-items: center;
  /* A ROTATED SQUARE OVERFLOWS ITS OWN BOX: `rotate(45deg)` on 13px gives an
     18.38px bounding box, so every glyph sticks out (18.38-13)/2 = 2.69px past
     each edge of its grid cell. With no row-gap the second-base label did not
     merely touch the bag, it OVERLAPPED it by those 2.69px. The row needs more
     than the column because the top base points straight up at its label where
     the side bases present a flat edge — 4.5px of real clearance against 3.7. */
  column-gap: 0.4rem; row-gap: 0.45rem;
}
.mg-bf-diamond { grid-column: 2; grid-row: 2; position: relative; width: 44px; height: 30px; }
.mg-bf-diamond i { position: absolute; width: 13px; height: 13px; border: 1.5px solid var(--line); transform: rotate(45deg); }
.mg-bf-diamond i.on { background: var(--danger); }
.mg-bf-diamond .b2 { left: 15.5px; top: 0; }
.mg-bf-diamond .b3 { left: 0; top: 15px; }
.mg-bf-diamond .b1 { right: 0; top: 15px; }
/* THE LABEL ROW IS PINNED TO ITS LINE BOX. A bolt is a nested span with its own
   `font-size` and `vertical-align`, so a runner who can run made the line box
   ~4px taller than one who cannot — which grew the base field, which grew the
   strip, which moved the whole row by 4px depending on WHO was on second.
   Height pinned to exactly the line, so a bolt changes the picture and nothing
   about the layout. Same number the symmetric padding below is derived from. */
.mg-bf-lbl { font-size: 0.72rem; font-weight: 700; color: var(--text); white-space: nowrap;
  line-height: 1.2; height: calc(0.72rem * 1.2); }
/* An empty base RESERVES its label and draws nothing — see the note in main.js:
   collapsing the row makes the strip change height as runners move, and the
   buttons below it jump on a walk. */
.mg-bf-lbl.empty { visibility: hidden; }
.mg-bf-2 { grid-column: 2; grid-row: 1; }
.mg-bf-3 { grid-column: 1; grid-row: 2; justify-self: end; }
.mg-bf-1 { grid-column: 3; grid-row: 2; justify-self: start; }
/* THE DIAMOND LINES UP WITH THE WORDS, AND THE WHOLE STRIP SITS CENTERED IN ITS
   BOX (Brian, s33: "the diamond should always be middle aligned with inning and
   outs", then "equal space above and below them within the box").

   `align-items: center` centers the base field's BOX, and that box carries a
   reserved label row on TOP and nothing underneath — so the diamond sat ~10px
   below the line the inning and the outs sit on. Measured before: tops
   138 / 142 / 125.

   THE FIRST FIX WAS TO DROP THE OTHER TWO GROUPS to meet the diamond, which
   aligned them for free and was WRONG for the reason Brian names: it left the
   content hard against the bottom of the box with a band of air above it. The
   honest fix is to make the base field SYMMETRIC — reserve the same space below
   the diamond that the label takes above it — and then every default does the
   right thing with no transform anywhere. It costs 21px of height and it is the
   thing being asked for. Derived from the label's own metrics, so it stays true
   if the type size or the row gap moves. */
.mg-basefield { padding-bottom: calc(0.72rem * 1.2 + 0.45rem); }
/* FIXED POSITIONS, WHATEVER THE INFORMATION SAYS (Brian: "all three elements
   should be fixed positions all the time regardless of information"). With
   `justify-content: center` the three groups are centered AS A SET, so the inning
   and the outs slid sideways every time a runner reached, left, or had his name
   replaced by a longer one — the strip re-centered on every play. Equal shares
   with each group centered in its own share pins them: the content still grows
   and shrinks, but it does so around a fixed point instead of moving everything
   else. `flex: 1 1 0` rather than a 3-column grid because the per-pitch beat
   adds a FOURTH element (the count chip) and a fixed column count would wrap it. */
/* EQUAL GAPS, GROUP CENTERED, POSITIONS FIXED — all three at once, which is only
   possible if the elements have FIXED WIDTHS (Brian, s33: "equal gap between
   elements, and all three elements as a group should be centered in the strip",
   after "all three elements should be fixed positions all the time").
   Equal SHARES of the strip got the positions pinned but made the visible gaps
   unequal, because each group centered inside a share it did not fill. Fixed
   widths plus `justify-content: center` and one `column-gap` gives a group that
   is centered as a whole, separated by identical gaps, and anchored — the content
   grows and shrinks INSIDE its own box and nothing else hears about it.
   AND THE THREE BOXES ARE THE SAME WIDTH, which is what makes it LOOK even
   (Brian: "when nobody is on base it looks like it's uneven"). Equal gaps
   between boxes of UNEQUAL width do not read as even spacing, because what the
   eye measures is center to center: sized to their own worst content the boxes
   ran 9 / 6 / 18rem, and the centers came out 168.6px apart and then 240.6px
   apart. With the bases empty that put a 44px diamond adrift in the middle of an
   18rem box with a visible void beside it. Equal widths make the center spacing
   a constant, whatever the boxes happen to be holding.

   11rem IS THE LARGEST THAT FITS, and the arithmetic has to include the strip's
   OWN box: a 675px center column is 641px of content once its 1px borders and
   1rem padding come off. Three 11rem boxes plus two 3.04rem gaps is 625px, which
   clears 641 by 16. 11.5rem measures 649 and looks like it fits against 675 —
   it wraps, which is how this was found. It clears the two elements whose
   content is bounded — "▼ Bottom 25th" measures 141.4px and the outs group is
   93.1px of fixed content, the word and three pips being all it ever holds.
   The bag is the one that can exceed its box, and it does so HARMLESSLY: at
   11rem each side column is 59.6px, so anything longer than about thirteen
   characters spills into the 49px gap rather than pushing its neighbours, which
   is only safe because these widths are fixed. The longest surname in the file
   is Encarnacion-Strand and "X. Encarnacion-Strand ⚡⚡⚡" measures 152.4px — two
   of those plus the bag is 362px, so the true worst case fits in NO layout at
   this column width. Spilling beats clipping: a scoreboard that hides half a
   name is lying. */
/* `min-width: 0` IS WHAT MAKES A FIXED WIDTH ACTUALLY FIXED. A flex item
   defaults to `min-width: auto`, which is min-CONTENT — so the basis is only a
   suggestion and a long enough name overrides it. Measured with the pathological
   name in all three slots and this line missing: the bag grew 73.5px, the
   diamond slid 168.6px, and the strip wrapped to 125px. Everything above about
   fixed positions is false without it. */
.mg-situation > * { flex: 0 0 auto; min-width: 0; }
.mg-situation > :not(.mg-basefield) { display: flex; justify-content: center; align-items: center; }
.mg-sit-inning,
.mg-situation > .mg-sit-grp:not(.mg-sit-park),
.mg-sit-count,
.mg-basefield { flex-basis: 10rem; }
.mg-basefield { justify-content: center; }
/* AND THE BAG SITS 20px LEFT OF ITS BOX CENTER (Brian, s33). Equal boxes give
   equal CENTER spacing, which is what stopped the strip reading as lopsided —
   but the three boxes hold very different amounts of ink, so the visible gap
   still runs wider on the diamond's side: "Outs ●●●" fills 93px of its 176 and
   the bag only 44, leaving 66px of air each side of it against 41.5px each side
   of the outs. This closes that by eye.

   A TRANSFORM RATHER THAN A MARGIN, deliberately: it moves the bag AND its three
   labels together, so nothing shifts relative to the bases they name, and it
   changes no layout at all — the boxes stay equal, the gaps stay equal and the
   positions stay fixed, which is everything the rules above spent their effort
   on. A margin would have re-centered the whole group and moved the other two. */
.mg-basefield { transform: translateX(-20px); }
@media (max-width: 620px) {
  /* 3.8rem of air is a desktop measurement; wrapped on a phone it only shows up
     as a hole between whichever two groups still share a row. And equal SHARES
     stop meaning anything once each group is alone on its own row, so they go
     back to sizing themselves. */
  .mg-situation { column-gap: 1.4rem; }
  .mg-situation > * { flex: 0 1 auto; }
  /* The bag takes the whole row on a phone, where it is alone on one anyway.
     Left to size itself it collapses to about 63px — the diamond and nothing
     else — because `minmax(0, 1fr)` lets the side columns go to zero, and the
     runner labels then have nowhere to be. Full width gives them ~119px a side. */
  .mg-basefield { flex-basis: 100%; transform: none; }
}

/* Three-column layout */
/* 320, not 268. MEASURED: at 268 seven of the 52 name-and-slash lines were
   ellipsized, the worst by 39px ("Andy Seminick R.252/.365/.427"), and three
   separate comments in this file describe working around the columns being
   "width-bound" — the hand chip kept borderless, the at-bat row's stat line
   shrunk a further 0.04rem. 320 clears the worst overflow with room over, so
   those workarounds are no longer load-bearing (left in place: they cost
   nothing and the widest name in history is not in this fixture). */
.mg-grid { display: grid; grid-template-columns: 320px minmax(0, 1fr) 320px; gap: 0.9rem; align-items: start; }
/* The center column stretches to the tallest roster; the play-by-play panel
   fills whatever space is left below the actions and scrolls, so the center
   never runs longer than the roster boxes beside it. */
.mg-center { display: flex; flex-direction: column; gap: 0.5rem; align-self: stretch; min-height: 0; }
.mg-pbp-panel {
  flex: 1 1 0; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 0.5rem 0.8rem 0.6rem;
}
.mg-pbp-panel .mg-sub-head { padding: 0 0 0.3rem; flex: none; }
/* Each side stacks a "Your Team" eyebrow above its roster box. */
.mg-side { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
/* Both eyebrows are LEFT aligned and sit directly over their club's name. They
   used to mirror each other (away flush left, home flush right), which read as
   two different labels rather than one label moving between dugouts. The left
   pad matches .mg-col-head's 0.7rem plus .mg-col's 1px border, so the label's
   first letter lines up with the first letter of the team name under it — change
   both together or the alignment drifts. `.right` is kept for anything that
   wants the old side. */
/* AND IT IS THE ONLY INDICATOR NOW, so it is sized to be one (Brian, s28: "make
   the 'Your Team' 2 points larger and space the lettering out a bit to cover
   more width"). 0.62rem -> 0.79rem is a true two POINTS at a 16px root
   (9.92px -> 12.59px), not two pixels; the tracking goes 0.09em -> 0.18em, which
   widens the label enough to read as a heading over the column rather than as a
   caption tucked above it. */
.mg-you { font-size: 0.79rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent); padding: 0 calc(0.7rem + 1px); }
.mg-you.left { text-align: left; }
.mg-you.right { text-align: right; }

/* GAME OVER. The rosters are cleared, which is what collapsed the play-by-play:
   `.mg-pbp-panel` is `flex: 1 1 0`, so its height came entirely from the space
   the roster columns created in the grid row. With them gone there was nothing
   to fill and 17 rendered lines had zero height. Content-size it here and cap
   the scroll instead. The field, the situation strip and the dugout eyebrows are
   empty boxes still drawing borders, so they go.
   AND TWO MORE OF THEM, found playing on a phone (Brian, s25). The list above
   named the field, the situation strip and the eyebrows; it missed the ACTION
   BAR and the two ROSTER COLUMNS, which empty at the final for the same reason
   and draw the same chrome around nothing. Measured at 375: `#mg-actions` an
   empty 327x21 panel under the result card, `#mg-away` and `#mg-home` collapsed
   to 2px each — a rounded pill and two hairlines that read as something that
   failed to load. `.mg-side` rather than `.mg-col`, so the grid does not keep
   the gap where the column used to be. */
.mg-final #mg-situation, .mg-final #mg-fieldcard, .mg-final .mg-you,
.mg-final #mg-actions, .mg-final .mg-side { display: none; }
/* ...AND THE GRID HAS TO STOP RESERVING THEIR TRACKS (Brian, s25: "make the box
   wider and centered"). This is fallout from the line above it, which is worth
   saying plainly: `display: none` takes the two roster columns out of grid FLOW,
   so `.mg-center` auto-placed into the FIRST 320px track and the whole final
   screen — verdict card and box score both — rendered 320 wide against the left
   edge of a 1440 window. Hiding a grid item is not the same as removing its
   column. One track now, so the center is the row. */
.mg-final .mg-grid { grid-template-columns: minmax(0, 1fr); }
.mg-final .mg-pbp-panel { flex: 0 1 auto; }
.mg-final #mg-log { max-height: 55vh; }

/* Roster columns */
.mg-col { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.mg-col-head {
  padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--line);
  display: flex; justify-content: space-between; align-items: center; gap: 0.4rem;
}
/* THE NAME IS GOLD, THE RULE UNDER IT IS NOT (Brian, s28: "not gold line above
   the lineup", then "keep the club name gold in the panel head").
   Those are two different marks and only one of them was ever about the club:
   the RULE was a 1px line at the bottom of a box, a long way from the thing it
   meant, and it predates this session. The NAME is the club, so it takes the
   ink. With the board back to plain white, the panel is where "this dugout is
   yours" is said — `.mg-you` in words above it, the club in gold inside it. */
.mg-col-head.mine .mg-club { color: var(--accent); }
/* THE NAME YIELDS, THE TAG DOES NOT. `nowrap` with no `min-width` leaves a flex
   item at its MIN-CONTENT floor, so "2016 Los Angeles Angels of Anaheim" simply
   refused to shrink and shoved "IN THE FIELD" 14px past the panel edge, where
   it was clipped. `min-width: 0` is what turns that floor off — the same
   property, for the same reason, as the `.teams` grid in s14.
   The NAME is the right thing to ellipsize: it is repeated on the scoreboard
   two inches above, whereas the tag says which dugout is batting and appears
   nowhere else. Pre-existing, and worse at the old 268px column. */
.mg-club { font-size: 0.86rem; font-weight: 600; white-space: nowrap;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.mg-tag { flex-shrink: 0; font-size: 0.56rem; letter-spacing: 0.06em; text-transform: uppercase; font-weight: 700; padding: 2px 7px; border-radius: 20px; white-space: nowrap; }
.mg-tag.bat { background: rgba(245, 197, 66, 0.16); color: var(--accent); }
.mg-tag.field { background: rgba(111, 208, 140, 0.16); color: var(--accent-2); }

.mg-lineup { display: flex; flex-direction: column; }
/* THE CLICKABLE ROWS ARE REAL BUTTONS (Brian, s26: "make them real buttons").
   A substitution used to be a `<div>` with `cursor: pointer` — no keyboard, no
   role, nothing announced. The markup emits a `<button>` wherever the row
   actually does something and a `<div>` where it does not, so the tab order
   holds only the rows that act.
   `:where()` IS THE WHOLE TRICK, and the reason this reset is one rule instead
   of a fight. It has ZERO specificity, so it beats the browser's own button
   styling — author styles always do — while losing to every `.mg-row` and
   `.mg-bench-row` declaration in this file. That matters most for `border`:
   `.mg-row` draws the row separator with `border-bottom`, and an ordinary
   `button.mg-row { border: 0 }` reset would out-specify it at (0,1,1) and quietly
   delete the line between every player. Written this way the separator, the
   padding, the grid and the hover fill all keep working untouched.
   `font: inherit` FOR THE SAME REASON: a button does not inherit the page's
   typeface, and `.mg-row`'s own `font-size` still wins over the shorthand. */
:where(button.mg-row, button.mg-bench-row) {
  font: inherit; color: inherit; background: none; border: 0;
  text-align: left; width: 100%; appearance: none; cursor: pointer;
}
/* This one DOES need to win — a keyboard user has nothing else to go on. */
button.mg-row:focus-visible, button.mg-bench-row:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.mg-row {
  display: grid; grid-template-columns: 8px 26px 1fr auto; gap: 0.35rem; align-items: center;
  padding: 0.32rem 0.65rem 0.32rem 0.45rem; border-bottom: 1px solid #1b2a20; font-size: 0.8rem;
}
.mg-row:last-child { border-bottom: none; }
.mg-marker { text-align: center; color: var(--accent); font-size: 0.75rem; }
.mg-pos {
  font-size: 0.64rem; font-weight: 700; text-align: center; font-variant-numeric: tabular-nums;
  border: 1px solid var(--line); border-radius: 5px; padding: 1px 0; color: var(--muted);
}
.mg-pos.field { background: rgba(111, 208, 140, 0.14); border-color: transparent; color: var(--accent-2); }
.mg-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mg-nm { color: var(--text); }
/* THE NAME IS THE SCOUTING REPORT'S HANDLE, AND IT HAD TWO CURSORS (Brian, s30:
   "on the yankees rollover the mouse was a cursor, and on the giants it's an
   arrow"). His club's lineup rows are real `<button>`s so they inherited
   `pointer`; the opponent's are `<div>`s, so the name text fell through to
   `auto` and rendered as a TEXT I-BEAM — which says "select this", the one
   thing it does not do. Doing the same act on the two panels felt different.

   `help` is the app's own existing answer for a hover-only rollover target —
   `.mg-ask` and `.mg-sit-grp` have used it for sessions — so this is applying a
   convention rather than inventing one, and the sweep takes the at-bat cards
   with it because they are the same kind of thing.

   IDENTICAL ON BOTH SIDES, AND THAT IS BRIAN'S CALL, NOT A JUDGMENT (s30:
   "make them identical"). It first shipped for one commit with `button .mg-nm`
   keeping `pointer`, on the reasoning that clicking your own man really does
   start a substitution — s26's "a real button when it is really a button". He
   asked for one cursor, and the click affordance is not lost by it: the row is
   still a `<button>`, and the badge, the stat line, the glove chip and the ⇄
   cue all still show the pointer. Only the NAME says "hover me", which is
   exactly what s30 made the name for. Do not re-add the override. */
/* AND THE SAME CURSOR WHEREVER THE ROLLOVER MOVED (s30, Brian: "wherever we
   currently have a scouting report rollover for a player ... the rollover should
   only be on the name like in game play"). The builder's roster rows and the
   at-bat cards hang their report off the NAME now, so the affordance has to
   follow it there or those names would go back to a text I-beam — the exact
   thing this rule was written to kill. `.mg-card` keeps it for the panel's own
   sake; the tip inside it is on the name. */
.mg-nm, .mg-card, .who > strong[data-tip] { cursor: help; }
.mg-ln { color: #5f7566; font-size: 0.68rem; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
/* Batting side (throwing hand on an arm) as a chip, so a bare "L" ahead of a
   slash line doesn't read as a stat. S = switch hitter. Kept borderless and
   tight: the roster columns are width-bound, and a bordered chip pushed five
   more stat lines into an ellipsis. */
.mg-hand { display: inline-block; margin-right: 0.28em; padding: 0 0.2em;
  border-radius: 3px; background: rgba(120, 150, 128, 0.16);
  font-size: 0.82em; color: #859c8b; }
.mg-rt { font-size: 0.62rem; color: var(--muted); text-align: right; }
.mg-row.atbat { background: rgba(245, 197, 66, 0.09); }
.mg-row.atbat .mg-nm { color: var(--accent); font-weight: 600; }
/* The semibold gold name runs a few px wider — trim its stat line to match
   so the at-bat row never ellipsizes the slash numbers. */
.mg-row.atbat .mg-ln { font-size: 0.6rem; }
.mg-base-dot { color: var(--danger); font-size: 0.62rem; white-space: nowrap; }

.mg-sub-head { padding: 0.3rem 0.85rem 0.1rem; font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; color: #5f7566; }
.mg-chips { display: flex; flex-wrap: wrap; gap: 0.28rem; padding: 0.1rem 0.7rem 0.55rem; }
.mg-chip { font-size: 0.66rem; color: var(--muted); background: #0f1a13; border: 1px solid var(--line); border-radius: 20px; padding: 2px 8px; white-space: nowrap; }
.mg-chip.arm { color: var(--accent-2); }
.mg-chip.closer { color: var(--danger); }
.mg-chip.now { color: var(--accent); border-color: var(--accent); }

/* Bench as a full, optionally-pickable list (name · position · slash line) */
.mg-trail { display: flex; align-items: center; justify-content: flex-end; gap: 0.25rem; }
/* ---- Dugout column density (Brian, s20: "cleaner and tighter") ------------
   The column ran 1082px against a 373px field card, so the roster was three
   times the height of the thing it surrounds and the bullpen fell off the
   bottom. NOTHING WAS REMOVED to fix it — every name, line and grade is still
   here; what changed is the air between them. Measured before and after rather
   than eyeballed: 1082 -> 946 at the same 320px column width.

   The arm rows stay TWO lines, and that was measured too, not assumed: the
   W-L/ERA/WHIP/IP line wants 339px and `.mg-who` has 235, so inlining it would
   only trade height for a wrap. Bench bats need 192 and fit. */
.mg-bench { display: flex; flex-direction: column; padding: 0.05rem 0.35rem 0.32rem; gap: 1px; }
.mg-bench-row {
  display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; gap: 0.35rem; align-items: center;
  padding: 0.26rem 0.5rem 0.26rem 0.4rem; border-radius: 7px; font-size: 0.79rem;
}
/* Badge listing every position a bench bat can play (e.g. "3B/SS") */
.mg-pos.multi { font-size: 0.55rem; padding: 1px 2px; white-space: nowrap; letter-spacing: -0.02em; }
.mg-bench-empty { padding: 0.3rem 0.7rem 0.55rem; font-size: 0.7rem; color: #5f7566; }
/* A ROW YOU CAN PICK KEEPS A FINGER-SIZED TARGET; a row that only shows you
   something takes the full tightening. The density pass above put bench bats at
   25px, which is fine to read and too small to tap — and these rows are half of
   a two-click substitution. So the floor is applied to the INTERACTIVE rows
   only: the unavailable list and the opposing dugout are never clicked and stay
   tight. A blanket value would have spent the height on rows nobody touches. */
.mg-bench-row.pick, .mg-row.subbable { min-height: 30px; }
.mg-bench-row.pick { cursor: pointer; }
.mg-bench-row.pick:hover { background: rgba(255, 255, 255, 0.05); }
/* Bullpen arms — the full W-L/ERA/WHIP/IP line won't fit on one line in a
   narrow column, so stack it under the name. */
.mg-pos.arm { background: rgba(111, 208, 140, 0.14); border-color: transparent; color: var(--accent-2); }
/* The men with a job: the closer you're saving for the 9th, the setup man who
   bridges to him. Read off real usage (saves, games finished). */
.mg-pos.arm.cl { background: rgba(255, 178, 60, 0.2); color: #ffc65a; font-weight: 700; }
.mg-pos.arm.su { background: rgba(120, 180, 255, 0.16); color: #8fc0ff; }
.mg-bench-row.arm { align-items: center; }
.mg-bench-row.arm .mg-who { white-space: normal; overflow: visible; }
.mg-bench-row.arm .mg-ln { display: block; margin-top: 0; line-height: 1.3; }
.mg-bench-row.arm.now { background: rgba(245, 197, 66, 0.1); }
.mg-bench-row.arm.now .mg-nm { color: var(--accent); font-weight: 700; }
/* The "on the mound" status sits under the stat line and wraps, so a long
   note (a reliever's "must face 3") never squeezes the name/stat column. */
.mg-arm-now { display: block; margin-top: 2px; font-size: 0.6rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.04em; white-space: normal; }
/* Unavailable (left the game / can't pitch today) */
.mg-bench.unavail { opacity: 0.6; }
.mg-bench-row.gone .mg-nm { color: var(--muted); }
.mg-pos.gone { border-color: var(--line); color: #5f7566; }
.mg-gone-note { font-size: 0.6rem; color: #5f7566; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }

/* Substitution: clickable lineup rows + selection highlights (shared) */
.mg-row.subbable { cursor: pointer; }
.mg-row.subbable:hover { background: rgba(111, 208, 140, 0.08); }
.mg-subcue { color: var(--accent-2); font-size: 0.78rem; opacity: 0.5; }
.mg-row.subbable:hover .mg-subcue { opacity: 1; }
.sub-target { box-shadow: inset 0 0 0 1.5px var(--accent-2); }
.sub-picked { box-shadow: inset 0 0 0 1.5px var(--accent); }
.sub-picked, .mg-row.sub-picked { background: rgba(245, 197, 66, 0.14); }

.mg-sub-hint {
  margin: 0.45rem 0.35rem 0.15rem; padding: 0.32rem 0.5rem; font-size: 0.64rem;
  background: rgba(245, 197, 66, 0.1); border: 1px solid rgba(245, 197, 66, 0.35);
  border-radius: 8px; color: var(--text); display: flex; align-items: center; gap: 0.4rem; justify-content: space-between;
}
.mg-sub-hint-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mg-sub-cancel { flex-shrink: 0; background: transparent; border: none; color: var(--muted); cursor: pointer; font-size: 0.62rem; text-decoration: underline; }
.mg-act-hint { font-size: 0.66rem; color: #5f7566; padding: 0 0.1rem; line-height: 1.3; }
/* The "?" that carries a rule which used to be two lines of always-on gray. */
.mg-about { display: inline-flex; align-items: center; justify-content: center;
  width: 13px; height: 13px; margin-left: 0.2rem; border-radius: 50%;
  border: 1px solid #5f7566; color: #5f7566; font-size: 0.56rem; font-weight: 700;
  cursor: help; vertical-align: middle; }
.mg-about:hover { border-color: var(--accent-2); color: var(--accent-2); }
/* The ⓘ that carries a control's explanation (Brian, s11). The whole button
   used to be the hover target; now only this is, so the button reads as a
   button. It is a SIBLING of the control, never a child — inside a <label> a
   click would toggle the checkbox, inside a <button> it would fire it — and the
   wrapper holds the pair together against the flex gap of the row. */
.mg-withinfo { display: inline-flex; align-items: center; gap: 0.35rem; }
/* THE MARK IS BIGGER, BOLDER AND SITS UP (Brian, s33: "the circled i needs to be
   bumped up, bigger, and bolder"). At 0.62rem in #5f7566 it was the faintest
   thing on a screen full of controls, and it is the one that says a control has
   an explanation behind it. Lifted off the baseline because it rides beside
   UPPERCASE section heads, where an x-height-aligned glyph reads as having
   dropped. */
.mg-info { color: var(--muted); font-size: 0.8rem; font-weight: 700; line-height: 1;
  cursor: help; user-select: none; vertical-align: 0.12em; }
.mg-info:hover { color: var(--accent-2); }
.mg-tactics-head .mg-info { margin-left: 0.25rem; }

/* Forced defensive realignment panel */
.mg-align { display: flex; flex-direction: column; gap: 0.35rem; background: var(--panel); border: 1px solid rgba(245, 197, 66, 0.4); border-radius: 12px; padding: 0.7rem 0.8rem; }
.mg-align-head { font-size: 0.78rem; font-weight: 600; color: var(--accent); line-height: 1.35; margin-bottom: 0.2rem; }
.mg-align-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 0.45rem; align-items: center; padding: 0.15rem 0.1rem; }
.mg-align-who, .mg-align-sel { background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px; font-size: 0.78rem; max-width: 100%; }
.mg-align-who { min-width: 0; }
.mg-align-at { font-size: 0.68rem; color: var(--muted); }
.mg-align-row.oop .mg-align-who, .mg-align-row.oop .mg-align-sel { border-color: var(--danger); }
.mg-align-flag { font-size: 0.62rem; color: var(--danger); text-transform: uppercase; letter-spacing: 0.04em; min-width: 0; white-space: nowrap; }
.mg-align-note { font-size: 0.66rem; color: var(--muted); line-height: 1.4; padding-top: 0.1rem; }
.mg-align #mg-align-ok { margin-top: 0.35rem; }

/* Center: the field diamond */
/* A SIZE CONTAINER, so the park crest can be a share of this box rather than a
   fixed 84px on a card that is 500px wide on a desktop and 314 on a phone — see
   `.mg-sit-park` for the sizing and the arithmetic. `inline-size` only, so the
   card's HEIGHT still comes from the diamond inside it. It is already
   `position: relative`, so the containment does not change what the absolutely
   positioned crest and fielder chips are positioned against. */
.mg-fieldcard { position: relative; container-type: inline-size; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 0.35rem; }
.mg-field-head { display: flex; justify-content: space-between; align-items: center; padding: 0.1rem 0.2rem 0.5rem; }
/* 340, not the original 380 — a tad smaller, which is all the vertical room the
   stacked layout can give up without the field feeling cramped (card 321 -> 290).
   Worth knowing if you shrink it further: the limit is NOT text legibility. The
   fielder chips are HTML overlaid at a FIXED size (.mg-fp 0.56rem, .mg-fn
   0.7rem), so shrinking the field never shrinks a name. The limit is the chips
   COLLIDING — measured by sweeping the width and testing for overlapping boxes,
   the floor is 280 with the longest realistic surname in all nine spots. */
/* 500, up from 340. The cap was the binding constraint once the park outline
   was drawn from real dimensions: a swept search over the layout could not get
   below ~10 overlapping chips at 340 with full names, and hits ZERO at 500
   with the stretched infield. Verified by sweeping all 71 drawable parks and
   testing every chip pair for an overlapping box.
   The old note below is about the GENERIC field and still holds for it. */
.mg-diamond { position: relative; width: 100%; max-width: 500px; margin: 0 auto; aspect-ratio: 1 / 0.72; }
.mg-field-svg { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 8px; }
/* THE NAMES WIN THE CORNER (Brian, s25, found playing a game on a phone). The
   park crest is `z-index: 2` on the same card and the chips had none, so the
   crest painted over right field: measured at 375, the crest covers 256..340 and
   RF's chip runs 223..288, which put half of "Strawberry" behind Shea Stadium.
   IT IS A PHONE PROBLEM BECAUSE THE CREST IS FIXED AND THE FIELD IS FLUID — 84px
   of mark is 17% of the 500px diamond and 27% of a 314px one — but the fix is
   not a phone rule: a crest may sit on a name at any width, and which of the two
   you must be able to read is not a question about screen size. You have to know
   who is in right field. The ballpark is a label on a game you already chose.
   OPAQUE, NOT JUST ON TOP. At 0.5 alpha the chip let the crest's lettering
   through and the two sets of words competed; over grass alone the darker plate
   is no worse than the old one. */
.mg-fielder { position: absolute; transform: translate(-50%, -50%); text-align: center; white-space: nowrap;
  background: rgba(6, 14, 9, 0.86); border-radius: 6px; padding: 1px 5px; z-index: 3; }
/* THE TYPE SCALES WITH THE FIELD TOO (Brian, s25, found playing on a phone).
   The note above this block says it plainly — "the chips are HTML overlaid at a
   FIXED size, so shrinking the field never shrinks a name. The limit is the
   chips COLLIDING" — and records a swept search that hits ZERO collisions at
   500. THAT SWEEP WAS RUN AT 500. A phone's diamond is 314, which is below the
   regime anyone measured, and the chips collide there again.
   MEASURED at 375 by sweeping surname length in all nine spots: the boxes start
   biting at 7 characters, and 2B/SS — the tightest pair on the field — overlap
   by 26.8px at 14, which is the full height of a chip. They are not two chips at
   that point, they are one smear. 324 of the 11,318 surnames in the file are 11
   characters or longer, and they are not obscure men: Yastrzemski, Goldschmidt,
   Garciaparra, Schoendienst, Castellanos. Garciaparra at short and Schoendienst
   at second read as "GarciaparraSchoendienst".
   SAME EXPRESSION AS THE CREST, for the same reason and against the same
   container: 2.24cqw is 0.7rem at a 500px card and 1.792cqw is 0.56rem, so the
   desktop keeps the size it has always had and only a narrower field shrinks.
   THE FLOOR IS THE POINT, THOUGH. Pure `cqw` would put the name at 7px on a
   phone — smaller than the position label and not readable — so it clamps at
   0.58rem, which measures 9.28px.
   WHICH MEANS THE FLOOR BINDS ON EVERY PHONE, not just tiny ones: `2.24cqw`
   falls under 0.58rem once the card is narrower than about 414px, so anything
   below a ~460px viewport gets the floor rather than the ratio. Measured with
   six long surnames placed (Garciaparra, Schoendienst, Yastrzemski, Goldschmidt,
   Castellanos, Encarnacion): ZERO collisions at 375, 768 and 1440, and at 320 a
   worst bite of 6.3px — down from 26.8, a graze rather than a smear, and the
   chips win the z-order so the name on top still reads. A fixed minimum on a
   shrinking field cannot do better than that; what it must not do is let the
   type get too small to read, which is the trade this clamp makes. */
.mg-fp { font-size: clamp(0.47rem, 1.792cqw, 0.56rem); color: var(--accent-2); font-weight: 700; }
.mg-fn { font-size: clamp(0.58rem, 2.24cqw, 0.7rem); color: #fff; line-height: 1.1; }
.mg-fielder.hot .mg-fp, .mg-fielder.hot .mg-fn { color: var(--accent); }
/* The first baseman holding the runner on — he is standing on the bag, so mark
   him instead of letting him look like a mis-drawn fielder. */
.mg-fielder.holding { outline: 1.5px solid var(--accent); outline-offset: 1px; }
.mg-fielder.holding .mg-fp { color: var(--accent); }
/* Shifted or drawn in. Quieter than `holding` on purpose: holding a runner is a
   call about ONE man standing somewhere specific, while a shift moves three at
   once and three gold outlines would read as an alarm. */
.mg-fielder.moved { outline: 1px dashed var(--accent-2); outline-offset: 1px; }
.mg-fielder.moved .mg-fp { color: var(--accent-2); }
.mg-fbase { position: absolute; width: 13px; height: 13px; transform: translate(-50%, -50%) rotate(45deg); border: 1.5px solid #fff; background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.35); }
.mg-fbase.on { background: var(--danger); border-color: var(--danger); }
/* LIVE AGAIN (Brian, s33). Briefly unused earlier the same session, and kept
   annotated for exactly that reason — the diagram is where a manager looks at
   the bases, and a lit bag with no name sends him back to the strip. */
.mg-frunner { position: absolute; transform: translate(-50%, -50%); font-size: 0.62rem; color: #fff; font-weight: 700; white-space: nowrap;
  background: var(--danger); border-radius: 5px; padding: 0 5px; }
.mg-plate { position: absolute; width: 12px; height: 12px; left: 50%; top: 88%; transform: translate(-50%, -50%) rotate(45deg); background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.35); }

/* Matchup + odds */
.mg-cards { display: grid; grid-template-columns: 1fr auto 1fr; gap: 0.6rem; align-items: stretch; }
.mg-card { background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px; padding: 0.6rem 0.75rem; display: flex; flex-direction: column; gap: 0.12rem; }
.mg-card.mine { border-color: var(--accent); }
.mg-card small { color: var(--muted); font-size: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase; }
/* THE ROLE IS THE LOUD HALF OF THE EYEBROW (Brian, s31). "AT BAT" and "ON THE
   MOUND" are what the two cards ARE, and they were the same muted gray as the
   club name beside them — so the panel had two labels of equal weight and
   neither said which card you were looking at. The role takes the bright green
   and the weight; the club stays quiet, because you already know who is playing. */
.mg-card-role { color: var(--accent-2); font-weight: 700; }
/* THE NAME GETS ITS OWN LINE, ALWAYS (Brian, s31). This was a baseline flex row
   that wrapped only when it ran out of room, so the same card read as one line
   or two depending on how long the man's name was and how much he had done
   today — "Waite Hoyt 0.0 IP, 1 BB · throws R · 5 pitches" on one line early,
   and broken in two by the fourth inning once the stat line grew. A card whose
   shape depends on the numbers in it is a card the eye has to re-find every
   time. Column, so the break is a decision rather than an accident. */
.mg-card-line { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; }
.mg-card-sub { color: var(--muted); font-size: 0.74rem; }
.mg-card strong { font-size: 1rem; }
.mg-card em { color: var(--muted); font-style: normal; font-size: 0.74rem; }
.mg-card .slash { color: var(--accent-2); font-size: 0.76rem; font-variant-numeric: tabular-nums; }
.mg-vs { align-self: center; color: #5f7566; font-size: 0.75rem; font-weight: 700; }
#mg-odds { font-variant-numeric: tabular-nums; font-size: 0.76rem; color: var(--muted); text-align: center; background: #0b120d; border: 1px solid var(--line); border-radius: 8px; padding: 0.45rem; }
#mg-odds:empty { display: none; }
#mg-odds b { color: var(--text); }

/* Actions become a grouped panel */
#mg-actions { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 0.6rem 0.7rem; display: flex; flex-direction: column; gap: 0.38rem; align-items: stretch; }
.mg-act-group { display: flex; flex-direction: column; gap: 0.15rem; }
.mg-swing-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
/* Floating player-stats tooltip (hover a roster row). */
.mg-tooltip {
  position: fixed; left: 0; top: 0; z-index: 200; pointer-events: none;
  background: var(--panel-2); border: 1px solid var(--accent); border-radius: 8px;
  padding: 0.4rem 0.6rem; font-size: 0.78rem; color: var(--text); line-height: 1.34;
  max-width: 300px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
}
.mg-tooltip[hidden] { display: none; }
/* NO WIDOWS IN A ROLLOVER (Brian, s27: "don't widow any rollover copy"). A
   two-line body that drops its last word onto a line of its own — "…managed for
   / you." — reads as a mistake in a box this small, and every one of the fifty
   controls converted this session carries a sentence of exactly that length.
   `balance` rather than `pretty`: these bodies are one to four lines, which is
   the range balance is for, and it evens the lines instead of merely refusing a
   short last one.
   ONLY THE BLOCKS THAT CARRY PROSE. `.tt-stat` is a label and a right-aligned
   figure in a flex row — there is no wrapping there to balance, and s26 records
   that Chrome will not balance a block containing a forced break anyway. The
   bare text node under `.mg-tooltip` is the tooltip BODY, which is why the rule
   sits on the box itself as well as on the named parts. */
.mg-tooltip, .mg-tooltip .tt-name, .mg-tooltip .tt-sub,
.mg-tooltip .tt-read, .mg-tooltip .tt-coach, .mg-tooltip .tt-head-line {
  text-wrap: balance;
}
/* THE DOCUMENT'S TITLE (s21). Named once at the top so everything under it —
   Batting, Baserunning, Glove, Notes — reads as a section of one report rather
   than as four boxes sharing an edge. Quiet and small: it labels the thing, it
   is not the thing. */
.mg-tooltip .tt-report {
  display: block; font-size: 0.6rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 0.15rem;
  padding-bottom: 0.15rem; border-bottom: 1px solid var(--line);
}
.mg-tooltip .tt-name { display: block; font-weight: 700; color: var(--accent); margin-bottom: 0.1rem; }
/* THE PLAYER ROLLOVER CARRIES LABELED ROWS NOW (Brian, s17), so it needs more
   than the 300px a run of prose wanted — a label and a right-aligned figure on
   one line at 300px wraps every row and the column stops lining up, which is
   the whole point of the shape. 340 fits "Plate appearances" against a
   four-digit number with room to spare, measured.

   340 -> 420 IN s34, AND IT COSTS THE PROSE TIPS NOTHING (Brian: "labels should
   never line break AND on every rollover all dots reach the stat numbers").
   s33's small-ball blocks carry labels of a different order — "He is sent once
   it is down" against "Hits" — and at 340 the LABEL TRACK IS 130px, because a
   one-value row also reserves 5.4rem for the column it does not use. A 170px
   label into a 130px track wraps, and the row above it had 5.7px left for its
   leader: one dot.

   NOTHING ELSE WIDENS, because `fitTooltipToCopy` measures the widest inked
   LINE of a box sitting at its ceiling and gives the slack back. A run of prose
   that did not need 420 is squeezed to its own ink on the next frame, which is
   why the cap can be raised for the rows that need it without every paragraph
   in the dugout growing 80px. Measured after the change: the safety-squeeze tip
   lands at 385 and the prose tips do not move. */
.mg-tooltip { max-width: 420px; }
/* THE SCOUTING REPORT FOLDS INTO TWO COLUMNS (Brian, s33: "shorten the scouting
   report" — batting one side, splits/running/fielding the other). A batter's
   report is around forty lines, and a rollover taller than the window is clamped
   to the top edge and runs off the bottom, which is the state this halves.

   THE CEILING RISES ONLY FOR THE BOX THAT HAS TWO COLUMNS IN IT. Every other
   tooltip on the site is prose or a label-and-figure pair, and 340 is a measured
   fit for those (see above) — raising it globally would let a sentence run to
   700px, which is the shape `noWidow` and `text-wrap: balance` exist to prevent.
   `:has()` keeps the two cases apart with no second class on the box, and the
   report is the only thing that ever writes `.tt-two`.

   EACH COLUMN GETS THE OLD 340 rather than half of a new number: the left one
   still has to fit "Runs Batted In" against a value cell that is 5.4rem by
   declaration, and squeezing it would wrap every row and lose the alignment the
   grid is for — the same reasoning that set 340 in the first place, applied
   twice. So the box is two of those plus the gap.

   AND THE CEILING IS NOT THE WIDTH. The box is `position: fixed` with no width
   set, so it shrinks to its content and only reaches this figure when something
   needs it: a pitcher's report comes out at 521, a batter's out of a game at
   620, and only a batter MID-SERIES — whose table carries a third track for the
   running book — actually spends the 712. Measured, all four. */
.mg-tooltip:has(.tt-two) { max-width: 712px; }
/* THE STAT TABLE TAKES WHAT IT NEEDS AND THE PROSE TAKES THE REST — `auto`
   rather than a second `1fr`. The left column is not one width: out of a game it
   is a two-track table, and in a series it grows a third track for the running
   book, which is the widest the report ever gets. Splitting the box down the
   middle would give that table dead space in the first case and squeeze it in
   the second, and everything on the right is prose that pays for width in LINES.
   Measured on a batter mid-series: 377px tall at this shape, 445 when the right
   column is starved. `minmax(0, 1fr)` beside it so the table yields rather than
   overflowing when there is genuinely not enough room. */
.tt-two {
  display: grid;
  /* THE RIGHT COLUMN GETS A FLOOR (Brian, s33: "none of the labels should be on
     two lines"). A bare `auto` on the left let the stat table take whatever it
     wanted, and in a SERIES it wants a lot — three tracks, and a live slash line
     reading "1.000/1.000/2.000" is the widest string in the report. Measured on
     his own screenshot's case: the tracks came out 398 / 275 and "Runs / 150
     games" broke over two lines in the squeezed remainder.

     320 IS MEASURED, NOT PICKED. At a 300px floor the labels already fit; at 340
     the stat table itself starts to overflow its own track. 320 sits between
     them with both true: no label wraps and nothing overflows, at 352 / 320. */
  grid-template-columns: minmax(0, auto) minmax(320px, 1fr);
  column-gap: 1.15rem; align-items: start;
}
/* A COLUMN, NOT A PANEL: no rule between them and no padding. The blocks inside
   already carry their own `.tt-head` separators, and a border here would read as
   two documents side by side rather than one folded in half. */
.tt-two .tt-col { min-width: 0; }
/* The first heading in EITHER column sits flush with the top of the box. `.tt-head
   :first-child` handles the left one by accident of source order; the right one
   is first inside its own column, which is what this says out loud. */
.tt-two .tt-col > :first-child { margin-top: 0; }
/* ONE COLUMN WHEN THERE IS NOT ROOM FOR TWO. The box follows the cursor and
   flips sides rather than clamping, so on a window narrower than about two
   columns plus the flip there is nowhere for a 712px panel to go — it would be
   pinned to the 8px gutter under the pointer. Folding back is the honest
   fallback, and it is the layout the report had until this session. */
@media (max-width: 860px) {
  .mg-tooltip:has(.tt-two) { max-width: 340px; }
  .tt-two { grid-template-columns: minmax(0, 1fr); }
  .tt-two .tt-col + .tt-col > :first-child { margin-top: 0.35rem; }
}
/* The year, club, position and hand — one identifying line under the name,
   above the numbers. Not a `.tt-stat`, because it is not a measurement. */
/* TIGHTER (Brian, s33, against his own screenshot of Pedro Guerrero's card). The
   two-column report was right in shape and loose in the vertical: measured at
   712x435, and the four figures below take it to 712x394 — 9% shorter — with the
   TYPE UNTOUCHED. Shrinking the text would have been the cheap way and the wrong
   one; a scouting report is read, not glanced at.

   WHAT ACTUALLY BINDS IS THE RIGHT COLUMN, which is the measurement that decided
   which knobs to turn. The box is as tall as its taller column, and that was the
   PROSE side at 361 against the stat table's 322 — so tightening the stat rows,
   the obvious move, bought exactly nothing. Cell padding and row margins were
   tried first and measured zero. The three that pay are the ones the prose side
   spends: the gap above each heading, the gap above each read, and the leading.

   AND IT BALANCES THE TWO COLUMNS AS A SIDE EFFECT — 304 against 324, from 322
   against 361 — because the right column had more of these gaps in it. */
.mg-tooltip .tt-head-line { margin-bottom: 0.16rem; }
.mg-tooltip .tt-head { margin-top: 0.22rem; margin-bottom: 0.06rem; }
.mg-tooltip .tt-read { margin-top: 0.16rem; }
/* THE FREE-STANDING ROWS ONLY. `.tt-cols > .tt-stat { margin: 0 }` further down
   wins over this by source order, and that is deliberate rather than lucky: the
   grid's rows must stay flush or a margin between two ruled cells breaks the
   dotted column rules into segments, which s31 records as not cosmetic. What
   this reaches is the fielding block in the right-hand column, whose rows are
   ordinary flex lines — and the right column is the one that sets the box's
   height, so it is the only place tightening a row pays. */
.mg-tooltip .tt-stat { margin: 0.04rem 0; }
/* A note UNDER a block rather than beside a label: the glove's component
   breakdown, the no-arm explanation, the caveat. Indented so it reads as
   belonging to the section above it rather than as another fact. */
.mg-tooltip .tt-sub { display: block; color: var(--muted); font-size: 0.92em;
  line-height: 1.35; margin-top: 0.1rem; }
.mg-tooltip .tt-head + .tt-sub { margin-top: 0.05rem; }
/* THE RULE DIVIDES THE SECTIONS, NOT THE SENTENCES (Brian, s21). It used to sit
   on top of `.tt-read`, which was right while the read CLOSED the box and wrong
   the moment each section got its own: three reads meant three yellow lines in
   arbitrary places, none of them where Batting ends and Baserunning begins.
   It hangs off the section HEAD now, so the report is ruled where it changes
   subject — hitting, then running, then fielding — and the notes sit inside
   their sections unruled. */
.mg-tooltip .tt-read { display: block; margin-top: 0.3rem; color: var(--accent-2); }
/* AND A READ THAT CARRIES A BAND IS COLORED BY IT (Brian, s25: "why is RF
   measurement green?"). The green above is the app's describing-a-player voice
   rather than a verdict — but over a 36th-percentile glove and the words
   "elevated error rate" it reads as approval. `average` takes the muted ink on
   purpose: "at the league rate" is a measurement, not a compliment. */
.mg-tooltip .tt-read-good { color: var(--accent-2); }
.mg-tooltip .tt-read-flat { color: var(--muted); }
.mg-tooltip .tt-read-poor { color: var(--danger); }
.mg-tooltip .tt-head {
  /* THE SECTION RULE IS VISIBLE NOW (Brian, s33: "separate the different
     sections of the report with lines or something"). It was already here at
     25% of the accent, which on a photograph-dark panel is a line you can only
     find once you know it is there — and the report is four sections in two
     columns, which is exactly when the reader needs telling where one ends. */
  border-top: 1px solid rgba(245, 197, 66, 0.45);
  margin-top: 0.55rem; padding-top: 0.45rem;
}
/* ...except the first one, which would be ruled off from the player's own name.
   Both the line that usually precedes it and the name itself, because the
   identifying line is omitted when there is nothing to identify with. */
.mg-tooltip .tt-head-line + .tt-head,
.mg-tooltip .tt-name + .tt-head { border-top: 0; margin-top: 0.1rem; padding-top: 0; }
/* The bolts are a hover target out on the field; in here they are just a grade
   next to its label, and there is nothing further to hover. */
.mg-tooltip .mg-bolt { cursor: inherit; }
/* WHAT THE CALL IS WORTH IN RUNS — bench coach Phase B. It sits under the
   scouting read and ABOVE the coach, and that order is the feature's whole
   model: these are FACTS, derived from the same run-expectancy table for all
   three men, and his opinion is what comes after them.

   It is set apart from `.tt-read` on purpose. The read is a scouting phrase
   about a PLAYER; this is a model's answer about a CALL, and a reader who
   cannot tell them apart will take one for the other. The heading is what does
   that work — a bare row of run figures under a tooltip reads as more of the
   same measurements, and these are an estimate from a model rather than
   something off a record book. Solid rule rather than the coach's dashed one:
   this side of the line is still the app talking. */
.mg-tooltip .tt-ev-head {
  display: block; margin-top: 0.4rem; padding-top: 0.32rem;
  border-top: 1px solid rgba(245, 197, 66, 0.25);
  color: var(--muted); font-size: 0.86em; letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* The rows under it are ordinary `.tt-stat` pairs, so a run figure lines up
   with every other number in the app — the "a multi-stat rollover reads DOWN,
   not across" rule, which these follow by using the same element. */

/* THE BENCH COACH'S OPINION, under the facts and under the scouting read.
   DELIBERATELY NOT `.tt-read`: that one is a fact about the player, this is an
   opinion about the CALL, and giving them the same treatment would make the
   opinion read as a measurement. Warmer color and italic — it is somebody
   talking, not the app stating. The name leads the line (see `coachLine` in
   coach-voice.js, which owns the attribution) and is what stops two coaches
   disagreeing from reading as the app contradicting itself. */
.mg-tooltip .tt-coach {
  display: block; margin-top: 0.4rem; padding-top: 0.32rem;
  border-top: 1px dashed rgba(245, 197, 66, 0.3);
  color: var(--accent); font-style: italic;
}
/* What the place was CALLED, at the head of the ballpark tooltip. Set in the
   serif the crest uses, because it is the one line in there that is a name
   rather than a measurement — and italic, because it is a quotation of what
   people said, not a label the app is applying. */
.mg-tooltip .tt-nick { display: block; margin-bottom: 0.35rem;
  font-family: Georgia, "Times New Roman", serif; font-style: italic;
  font-size: 0.95rem; color: var(--accent); }
/* The ball-strike count on a per-pitch beat. Leads the situation strip because
   mid-at-bat it's the number every call keys off. */
.mg-sit-count { font-weight: 800; font-size: 1.05rem; color: var(--accent);
  background: rgba(245, 197, 66, 0.14); border-radius: 6px; padding: 1px 8px;
  letter-spacing: 0.04em; }
.mg-approach-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
/* THE SAME WEIGHT AS "DEFENSIVE ALIGNMENT" (Brian, s33: "batting", "play small
   ball" and "baserunning" should be the same weight and size as "Defensive
   alignment"). They were 0.6rem in a dimmer grey against that head's 0.72rem
   `--muted`, so the four section labels on one screen read as two ranks when
   they are one — every group here is a call the manager makes. Matched to
   `.mg-tactics-head` exactly rather than nudged, so the two cannot drift. */
.mg-act-label { font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); line-height: 1.1; }
.mg-btns { display: flex; flex-wrap: wrap; gap: 0.4rem; }

/* Play-by-play footer */
/* Running box score, under the play-by-play. Reuses the results-screen .box
   tables; just needs the dugout-footer chrome and no extra top margin. */
.mg-boxscore { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 0.5rem 0.95rem 0.7rem; margin-top: 0.55rem; }
.mg-boxscore .mg-sub-head { padding: 0 0 0.35rem; }
.mg-boxscore .box-wrap { margin-top: 0; }

/* THE SECOND SENTENCE WAS DROPPING ONE WORD ONTO A LINE OF ITS OWN. Wrapping
   "Hopefully, it cools down for you soon." at 327px left `soon.` alone under a
   full line, which reads as a third thought rather than the tail of the second.
   `balance` splits a short run of lines evenly instead of filling greedily, so
   the sentence breaks near its middle and the `<br>` above stays the loudest
   break in the block. Browsers that do not know the keyword fill as before. */
/* SMALLER AND UNBOLD (Brian, s36). It is an `<h3>`, so it arrived at 1.4rem and
   the browser's own bold — a headline shouting two sentences of flavour over the
   score, which is the fact the reader came for. The score line under it keeps its
   weight, so the hierarchy now runs verdict-then-score rather than the reverse. */
.mg-final-head { color: var(--accent); font-size: 1.05rem; font-weight: 500;
  margin-bottom: 0.4rem; text-wrap: balance; }
/* Each sentence is its own block so the balancer above can act on it; no margin
   between them, so this still reads as the single line break it replaced. */
.mg-final-head span { display: block; }
/* CENTERED ON A MEASURE, not stretched to the window. The verdict is two
   sentences and the two lines under it are short, so a card the full width of a
   desktop would set them as a thin ribbon across 1100px. `max-width` gives it a
   readable measure and `margin-inline: auto` puts it in the middle of whatever
   space there is — which on a phone is simply all of it. */
.mg-final-card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.5rem 1.75rem; text-align: center;
  max-width: 44rem; margin-inline: auto; }
/* THE MORNING PAPER, UNDER THE VERDICT (s27). It takes the same measure as the
   card above it rather than the full width of a `.mg-final` grid that is now a
   single track — a newspaper set to 1,100px would run to twelve-word lines. */
.mg-final-recap { max-width: 44rem; margin: 1.1rem auto 0; }
/* THE LOGOS LEAD HOME ON THIS SCREEN ONLY (Brian, s27), so they only LOOK
   clickable on this screen — a pointer cursor on the scoreboard badge during a
   live at-bat would be advertising a door that is deliberately shut. */
.mg-final .mg-logo { cursor: pointer; }
.mg-final .mg-logo:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px;
}
.mg-final-recap:empty { display: none; }
/* Balanced for the same reason as the two lines around it: the extra-innings
   suffix pushes the score line to two on a 272px card, and greedy filling left
   `10.` by itself under a full line. */
.mg-final-score { font-size: 1.05rem; text-wrap: balance; }
/* THE VERDICT IS TWO SENTENCES, AND A PHONE HAS 327px TO SET THEM IN. At the
   card's 1.4rem the pair ran to FIVE lines, which buries the `<br>` that was put
   between them on purpose — the break stops reading as a pause and becomes one
   more wrap among four. Smaller type here takes it to three, so the two thoughts
   are still visibly two.
   NOT SMALLER STILL: exactly two lines needs about 0.95rem, which is UNDER the
   score line beneath it, and a headline that loses to its own subtitle is a
   worse answer than a headline that wraps once. The padding comes in with it so
   the words get the width rather than the margin. */
@media (max-width: 620px) {
  .mg-final-card { padding: 1.25rem 1rem; }
  .mg-final-head { font-size: 0.98rem; }
}
/* Same reason, one line down: a stat line breaks at every comma AND inside
   `2 2B`, so a greedy wrap put the count on one line and the double on the next.
   Balanced, the break lands at a comma — but balance alone will just as happily
   break BETWEEN A MAN'S FIRST AND LAST NAME (`Waite / Hoyt: 7.2 IP`), which is
   worse than the wrap it fixed. The name is held together explicitly and the
   balancer breaks around it. */
.mg-final-potg { margin-top: 0.5rem; font-size: 0.9rem; text-wrap: balance; }
/* THE EN DASH IN A SCORE IS A BREAK OPPORTUNITY, and at 320px a greedy wrap
   took it: `1927 New York Yankees win 8–` / `5.`. The figure is one token to a
   reader whatever the typesetter thinks, so it is held together — the line
   breaks between words instead. Same span, same job, on the name above. */
.mg-final-card .nobr { white-space: nowrap; }
.mg-final-potg strong { color: var(--accent); margin-right: 0.4rem; }
/* Game-over hold: sits in the actions area until the user clicks to the final score */
.mg-gameover { display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem; color: var(--accent); font-weight: 700; }
/* "That's the ballgame, folks!" — twice the size with a slow gold glimmer */
.mg-ballgame {
  font-size: 2em;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: 0.01em;
  background: linear-gradient(100deg, var(--accent) 35%, #fff6cf 50%, var(--accent) 65%);
  background-size: 220% auto;
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
  animation: ballgame-glimmer 2.8s linear infinite;
}
@keyframes ballgame-glimmer { from { background-position: 120% center; } to { background-position: -120% center; } }
@media (prefers-reduced-motion: reduce) { .mg-ballgame { animation: none; } }
/* Scoring-play hold: a compact one-line prompt to continue past the play */
.mg-hold { flex-direction: row; align-items: center; gap: 0.85rem; color: var(--accent-2); }

@media (max-width: 900px) {
  .mg-grid { grid-template-columns: 1fr; }
  .mg-center { order: -1; }
  /* Stacked single column: the center isn't bounded by a roster, so cap the
     play-by-play as a scroll box instead of letting it fill an open column. */
  .mg-pbp-panel { flex: none; }
  #mg-log { max-height: 45vh; }
}
.mg-btn {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 10px;
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.55rem 1.1rem;
  cursor: pointer;
}
.mg-btn.primary, #mg-actions .primary { background: var(--accent); color: #1a1a10; border-color: var(--accent); }
.mg-btn:hover { border-color: var(--accent-2); }
.mg-hint { color: var(--muted); font-size: 0.85rem; }
.mg-select-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.mg-select-row select {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.45rem 0.5rem;
  font-size: 0.85rem;
  max-width: 320px;
}
/* Sticky tactical controls (approaches + defensive alignments) sit below the
   primary action buttons, visually separated as a "settings" strip. */
/* ONE LINE: the label, its ⓘ, the coach chip and all five toggles (Brian, s33).
   It was `column`, which put the head on its own line above the toggles and
   spent a whole row of the dugout on four words. `row` + `wrap` keeps the
   one-line reading wherever there is room and degrades to the old stacked shape
   on a phone rather than overflowing — `.mg-toggles` wraps as a unit, so the
   fallback is the layout this replaced. */
.mg-tactics {
  border-top: 1px dashed var(--line);
  margin-top: 0.35rem;
  padding-top: 0.55rem;
  width: 100%;
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 0.75rem;
}
.mg-tactics-head { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; }
.mg-toggles { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.mg-toggle { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; cursor: pointer; }
.mg-toggle input { accent-color: var(--accent); }
/* A THUMB NEEDS MORE THAN A LINE BOX. The label wraps the input, so the tap
   area is the whole row — but only as tall as one line of 0.85rem text, which
   measured 19px on a phone. That is under WCAG's 24px minimum, let alone the
   44px Apple and Google ask for, and these three are the alignment calls a
   manager makes every batter, sitting directly under action buttons that are
   50px tall. The padding buys the height without moving anything: the checkbox
   and its words stay exactly where they were, the row around them grows.
   PHONES ONLY — a mouse hits 19px without complaint, and on a desktop the
   toggles sit in a row where the extra height would just be air. */
/* THE SAME FLOOR FOR THE REST OF THE DUGOUT'S SMALL CONTROLS (Brian, s25:
   "raise the exit game and select buttons too"). `Exit game`, `Stats` and the
   approach dials were all 26px — better than the toggles' 19 and still under
   every guideline. They were left at 26 in the first pass on the grounds that
   native controls carry their own hit slop; that is an argument for tolerating
   them, not for keeping a phone's two most-used dials smaller than the buttons
   beside them. All four now clear 44.
   `min-height` RATHER THAN MORE PADDING, because these three already have
   padding tuned to their shape — a pill and a dropdown — and adding to it would
   move the text off center. A min-height grows the box and leaves the type
   where it is. */
/* AND THE ACTION BUTTONS COME WITH THEM, which was not asked for and is the
   direct consequence of what was. Raising the dial to 44 left it TALLER than
   `Pitch to Dykstra` directly beneath it at 37 — a settings dropdown standing
   over the primary decision of the screen. One floor for every control in the
   dugout puts that right and costs nothing: the hierarchy here is carried by
   the gold fill and the weight, not by height, so the primary still reads as
   primary at the same size. */
/* AND THE OFFICE IS HELD TO THE SAME FLOOR (s26, measured on a 375px phone:
   every standalone button in `#locker` came out 30.3). The dugout got this in
   s25 and the Office did not, which is the usual shape — a rule written against
   the screen someone was looking at. The number is not restated; these join the
   same declaration.
   THE SHELF'S ROW CONTROLS COME TOO (Brian: "fix the shelf row buttons too"),
   AND THEY COST FAR LESS THAN THIS NOTE FIRST CLAIMED. It said raising them adds
   about 50px to every row — that was reasoned from the buttons' own height and
   never measured. A saved-game row is 78.9px tall because the TITLE and its line
   set it, with the actions sitting at the bottom under 46px of headroom; taking
   them from 25.9 to 44 is absorbed by a row that was already taller than either.
   Only the short rows grow at all. AN ESTIMATE STANDING IN FOR A MEASUREMENT,
   which is the thing this file keeps catching itself doing.
   `min-width` MATTERS AS MUCH AS `min-height` HERE, and only for these: `☆` and
   `✕` are single glyphs 29.5 and 26.8 wide, so height alone would have left two
   of the three targets narrow. `Review` is already 55.8 and unaffected. */
@media (max-width: 620px) {
  .mg-toggle { min-height: 2.75rem; padding: 0.25rem 0; }
  #mg-exit, #mg-stats { min-height: 2.75rem; }
  #mg-bug { min-height: 2.75rem; display: inline-flex; align-items: center; box-sizing: border-box; }
  .mg-approach select { min-height: 2.75rem; }
  #manage button { min-height: 2.75rem; }
  .locker-actions button, .league-buttons button, .sim-acts button { min-height: 2.75rem; }
  .saved-actions button { min-height: 2.75rem; min-width: 2.75rem; }
  /* AND THE ROWS YOU TAP TO MAKE A SUBSTITUTION (s26, found by playing a game at
     375px and enumerating the dugout's REAL controls: 26 of 35 were under 44px,
     and every one of them was a row rather than a button).
     THE BUTTONS WERE ALL FINE — s25 saw to that — which is exactly why this was
     invisible. A sweep of `button, select, label` reports the dugout clean; these
     are `<div>`s carrying `cursor: pointer`, a `data-subslot` and the scouting
     tooltip, so they answer to no selector anyone would think to check. The ⇄ on
     each row is a CUE, not the control: it is a 12x18 span with nothing clickable
     around it, and the row behind it is the target.
     `min-height` ALONE IS ENOUGH because both are already `display: grid` with
     `align-items: center` and a 30px floor — the content stays centered and only
     the box grows. One selector covers the bullpen too: `.mg-bench-row.pick`
     matches `mg-bench-row arm pick`.
     WHAT IT COSTS: the lineup's nine rows and the bench's seven go 30 -> 44 and
     the ten arms 39.5 -> 44. Worth it here more than most places — a mis-tap in a
     stack of nine adjacent rows does not miss, it substitutes the wrong man. */
  .mg-row.subbable, .mg-bench-row.pick { min-height: 2.75rem; }
  /* AND THE HOME SCREEN, WHICH WAS THE LAST SURFACE WITHOUT THIS (Brian, s26:
     "fix the tap targets on the home screen"). Swept at 375px: 21 distinct
     controls under the floor, in three families that had nothing to do with each
     other except that none of them had ever been measured.
     THE BANNER NAV IS THE ONE EXCEPTION, AND IT WAS TRIED. `.doc-nav a` got this
     floor when the document pages were fixed and `.fd-nav a` — the same two
     links, on the home banner — did not. Raising them looked like the obvious
     omission and it broke the banner: the two links wrap to two lines at 375px
     (the nav reserves the lockup's width so it cannot cross the mark), so at 44px
     each the nav measured 99.2px inside a 99px banner and the first link sat at
     -5.8px, above the artwork. A 99px strip cannot host two 44px rows and still
     be a banner.
     SO THEY STAY AT 22px, deliberately. It is the one target here whose miss
     costs nothing — a fingertip that lands wide hits the photograph and nothing
     happens — against a substitution row, where a miss changes your lineup. If
     the banner is ever given more height, this is the first thing to revisit.
     THE SETTINGS TARGET IS THE LABEL, NOT THE SELECT. `.era-toggle` is a
     `<label>` wrapping both its text and its dropdown, so raising the label gives
     the whole "Pitch detail [plate appearances]" pair one 44px target rather than
     leaving a tall select beside a short label.
     THE ROSTER BUILDER IS THE BULK OF IT — the fill pills, the club name field,
     the search box, and the depth chart's own row controls: demote, cut, drop,
     remove an arm, start a pitcher, auto-arrange. The crosses and carets get
     `min-width` too, for the reason the shelf's star did: a single glyph 23px
     across is not made tappable by height alone. */
  .fmt-btn, .who-btn, #btn-playball, #btn-playball-sticky { min-height: 2.75rem; }
  .era-toggle, #options-bar select, #park-bar select, .bench-move, .res-in, .bx-pick { min-height: 2.75rem; }
  .fill-btn, .ln-auto, .team-name, .search-box input { min-height: 2.75rem; }
  /* `Start` needs the WIDTH as well: five characters came out 41px across, so it
     cleared the floor on height and missed on the other axis — the same way the
     shelf's star did, and the reason that sweep checks both. */
  .sp-start { min-height: 2.75rem; min-width: 2.75rem; }
  .dc-demote, .dc-cut, .dc-drop, .rm-arm { min-height: 2.75rem; min-width: 2.75rem; }
  /* AND THE DISCLOSURES (s26, found by playing a game out and sweeping the Office
     with a result in it). `Play-by-play` was 22px and the team-code glossary 40 —
     both real controls, and the play-by-play one is the shortest tap target that
     was left anywhere in the app. The bench-coach drawer is already 51 and is
     unaffected.
     `display` IS DELIBERATELY LEFT ALONE. These are `list-item` boxes with
     `list-style-type: disclosure-closed`, so the ▶ in front of them is the
     BROWSER'S marker, not authored text — switching them to flex to center the
     label, which is the obvious way to do this, silently deletes the triangle
     that says the thing opens.
     `align-content` CENTERS A BLOCK BOX without that trade (checked: supported
     here), and where it is not supported the label simply sits at the top of a
     44px box — which is still a 44px target, so the degradation costs nothing
     that matters. */
  summary { min-height: 2.75rem; align-content: center; }
}
.mg-approach { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
.mg-shift-cue {
  color: var(--accent-2);
  font-size: 0.82rem;
  font-weight: 600;
  padding: 0.3rem 0;
}
.mg-approach select {
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.22rem 0.45rem;
  font-size: 0.82rem;
}
#mg-log { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* ESPN-style play-by-play: plays grouped under a half-inning header, newest
   first, each play tinted a little by outcome. */
.pbp-inning { margin-bottom: 0.5rem; }
.pbp-inn-head {
  position: sticky; top: 0; z-index: 1; background: var(--panel);
  display: flex; align-items: baseline; gap: 0.4rem;
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--accent); padding: 0.25rem 0 0.2rem; border-bottom: 1px solid var(--line);
}
.pbp-inn-head.home { color: var(--accent-2); }
.pbp-inn-team { color: var(--muted); font-weight: 600; text-transform: none; letter-spacing: 0; }
.pbp-play {
  font-size: 0.86rem; line-height: 1.35; padding: 0.28rem 0.1rem 0.28rem 0.6rem;
  border-left: 2px solid transparent; border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.pbp-play.hr { border-left-color: var(--accent); color: var(--accent); font-weight: 600; }
.pbp-play.hit, .pbp-play.steal { border-left-color: var(--accent-2); }
.pbp-play.dp, .pbp-play.err { border-left-color: var(--danger); }
.pbp-play.walk { border-left-color: #6aa2d8; }
.pbp-play.out { color: var(--muted); }
.pbp-empty { color: var(--muted); font-size: 0.85rem; padding: 0.4rem 0.1rem; }

@media (max-width: 800px) {
  .mg-main { grid-template-columns: 1fr; }

  .teams { grid-template-columns: 1fr; }
  .vs { justify-self: center; }

  /* THE GRID STACKED HERE ALREADY AND THE PANELS STILL RAN OFF THE SCREEN.
     `1fr` is `minmax(auto, 1fr)`, so the column cannot go below the panel's
     MIN-CONTENT width — stacking sets the count of columns, not a license to
     shrink. `min-width: 0` is what turns that `auto` minimum into zero and lets
     the column obey the viewport. Same reason on `.who`, which is the flex item
     the row's text lives in. */
  .team-panel { min-width: 0; }
  .dc-row .who { min-width: 0; }
  /* ...and the last thing holding the row open: the glove phrase is `nowrap` so
     it never breaks mid-description, which is right when there is room and is a
     width floor when there is not. It wraps down here instead. */
  .glove-why { white-space: normal; }
}

/* What the previous pitch was, on a per-pitch beat. A two-strike foul holds the
   count, so without this the action panel repeats the same count and the at-bat
   reads as frozen. */
.mg-lastpitch { margin-bottom: 0.5rem; font-size: 0.78rem; color: var(--muted); }
.mg-lastpitch b { color: var(--accent-2); }

/* Where the game is played: the picker's year box, and the one-line read on
   what the park does. The note is deliberately a sentence rather than a table —
   it sits under the Simulate button and has to be skimmable at a glance. */
.park-year { width: 5.2rem; margin-left: 0.35rem; }
/* Beside the ballpark picker, not under it: it reads as the consequence of the
   two dropdowns to its left, so it shares their line and their baseline. Wraps
   onto its own row only when the options bar runs out of width. */
.park-note { margin: 0; font-size: 0.78rem; color: var(--muted);
  align-self: center; }
.park-note:empty { display: none; }
/* The park name lives INSIDE the field box, upper right, where a scoreboard
   would put it (Brian, s11) — not as another chip in the situation strip. */
/* accent-2, the same green as the IN THE FIELD chip (.mg-tag.field) — it reads
   as a scoreboard label rather than as another piece of the field. */
/* The park's CREST in the corner of the field card, where its name used to be
   (Brian, s14). Still `cursor: help` and still the same hover — the mark
   replaced the label, not the tooltip behind it. The drop shadow is what keeps
   it off the grass it sits on; the text version needed one for the same reason. */
.mg-sit-park { position: absolute; top: 10px; right: 10px; z-index: 2;
  font-size: 0.74rem; font-weight: 700; color: var(--accent-2); opacity: 0.95;
  text-shadow: 0 1px 3px rgba(0,0,0,0.9); cursor: help; line-height: 0; }
/* IT SCALES WITH THE DIAMOND NOW (Brian, s25: "make the crest scale with the
   diamond"). It was a flat 5.25rem — 84px — while `.mg-diamond` is
   `width: 100%; max-width: 500px`, so the mark was 17% of a desktop field and
   27% of a phone's, and at 375 it sat on top of right field.
   THE EXPRESSION IS THE DIAMOND'S OWN. The diamond is `min(card, 500px)` wide
   and the crest was 84/500 = 16.8% of it at full size, so `min(16.8cqw, 5.25rem)`
   is exactly "16.8% of the diamond" at BOTH ends: below 500 the `cqw` term wins
   and tracks the card; at or above it the rem term caps at the size this has
   always been on a desktop, which is what stops the crest growing while the
   field it belongs to has stopped.
   `cqw` RESOLVES AGAINST `.mg-fieldcard`, which is the container declared above.
   Note the trap this file records elsewhere: `cqw` in a declaration ON a
   container queries its ANCESTOR, not itself — this rule is on a DESCENDANT, so
   it gets the card, which is what it wants.
   Measured: 84px at 1440 (unchanged), 53px at 375, 44px at 320 — where right
   field's chip ends at 288 and the crest now starts there instead of at 256. */
.mg-sit-park .park-mark svg, .mg-sit-park .park-mark img {
  width: min(16.8cqw, 5.25rem); height: min(16.8cqw, 5.25rem);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.6));
}
/* THE MEASURE STAYS, THE CENTERING GOES (Brian, s26: "footer left align with the
   boxes"). `margin: 0.35rem auto` on a 46rem paragraph inside a 1100px column
   splits the 182px difference either side, so the credits started at 272 while
   the block they sit in starts at 90 — and `text-align: left` could not show it,
   because the TEXT was ranged left inside a box that had been pushed right.
   IT WAS INVISIBLE UNTIL THE FOOTER MOVED. While the whole footer was centered,
   an auto margin agreed with everything around it; the moment the footer ranged
   left it became the only thing still centering, and the one that decides where
   the words actually start. `text-align` positions text in a box. This is the box.
   46rem SURVIVES because it is a reading measure, not an alignment: a credit
   line that runs the full 1100px is harder to read, and capping it costs
   nothing now that the cap grows to the right. */
.page-footer p { margin: 0.35rem 0; max-width: 46rem; }
.page-footer a { color: inherit; }
/* Auto-arrange sits on the Batting order heading, quiet until you want it. */
.ln-auto { float: right; font-size: 0.72rem; padding: 0.15rem 0.5rem;
  background: transparent; color: var(--muted); border: 1px solid var(--line);
  border-radius: 6px; cursor: pointer; }
.ln-auto:hover { color: var(--text); border-color: var(--accent-2); }


/* A stat rollover reads DOWN, not across: label left, the righty and lefty
   numbers in fixed columns. A park with six live numbers was four lines of
   prose before this. Any new multi-stat tooltip should use the same shape. */
.tt-head { display: block; font-weight: 700; margin: 0.35rem 0 0.1rem; }
.tt-head:first-child { margin-top: 0; }
.tt-stat { display: flex; align-items: baseline; gap: 0.5rem; margin: 0.12rem 0; }
.tt-stat b { flex: 1; font-weight: 500; color: var(--muted); }
/* DOT LEADERS, so the row is physically joined (s26, Brian picked treatment 02
   from the scouting-report study). `b` is `flex: 1` and the value is pushed to
   the far edge, which on the widest labels left the eye about 150px of empty
   panel to cross with nothing to follow — the complaint the study was drawn to
   answer. Leaders are the oldest fix in print and they belong to scorecards and
   programs, which is the world this app already lives in.
   NO MARKUP CHANGE, and that is why this is a rule and not a `ttRow` edit. The
   label is already the element that stretches, so making IT the flex container
   and hanging the leader off its `::after` fills exactly the gap between the
   label's text and the value. Every caller gets it for free and `ttRow` never
   learns that a tooltip has a house style.
   `currentColor` AT HALF STRENGTH rather than a named color: the leader is the
   label's own ink, held back, so it stays subordinate to both the name and the
   number and follows `--muted` wherever a theme moves it.
   NOT ON `.tt-dims`. That row is the one rollover that genuinely reads ACROSS —
   five park dimensions walked around the outfield — so there is no single value
   at the end for a leader to lead TO. It would draw a line to the first of five. */
.tt-stat:not(.tt-dims) b { display: flex; align-items: baseline; gap: 0.4rem; }
.tt-stat:not(.tt-dims) b::after {
  content: ""; flex: 1; height: 0.72em;
  /* AND THE LEADER HAS TO BE VISIBLE ENOUGH TO FOLLOW (Brian, s30: "the numbers
     are too far from the words", then "do 1 and 3"). At 0.5 it faded out over a
     long run — which is precisely the run where the eye needs it. The dots are
     the thing that makes an aligned column readable at all; a leader you cannot
     see is a gap. */
  border-bottom: 1.5px dotted currentColor; opacity: 0.75;
  /* A LEADER THAT LEADS NOWHERE IS A GAP (Brian, s34: "on every rollover all
     dots reach the stat numbers"). The leader is `flex: 1` inside the label, so
     it takes what the words leave — and a long label left it 5.7px, which draws
     ONE dot and reads as a stray period rather than a rule. The floor is what
     makes the row a row. */
  min-width: 1.5rem;
}
/* AND THE LABEL ITSELF NEVER BREAKS (Brian, s34: "labels should never line
   break"). It is a flex item beside the leader, so it SHRINKS before the leader
   does and wraps to a second line while the value stays on the first — the
   worst of both, because the pair stops reading as one row at exactly the
   moment the label is long enough to need the help. With the cap at 420 above,
   the longest label in the app fits on one line; if a new one does not, that is
   a copy problem and it now shows up as an overflow instead of a silent wrap. */
.tt-stat:not(.tt-dims) b { white-space: nowrap; }
/* A ROW WITH ONE VALUE MUST NOT RESERVE TWO COLUMNS (Brian, s30, "do 1").
   MEASURED FIRST, AND THE MEASUREMENT CORRECTED MY OWN DIAGNOSIS. I proposed
   sizing the label column to its widest word, on the theory that a stretching
   label was the cause. It is not. Ink-to-ink, from the last letter of the label
   to the first digit of the value:
     two-value rows   41–116px, mean 86   — already fine
     one-value rows  130–208px, mean 181  — the whole problem
   `b` is `flex: 1`, so on a row with a single value it grows into the second
   value column's width as well, and the leader runs an extra 5.4rem for nothing.
   Sliding that lone value left by exactly one column width puts it where the
   FIRST column sits and takes ~86px off every one of those rows, with no change
   to the twelve that were already fine.
   `:only-of-type` rather than `:has()`, so the rule needs no new selector
   support, and `.tt-dims` is untouched — the park's five dimensions have no
   single value for a leader to lead to, which is why they opt out above. */
/* AND IT SHRINKS TO ITS OWN DIGITS (Brian, s34: "on every rollover all dots
   reach the stat numbers", and "Fielding on scouting report, dots should go
   right up to the numbers").
   THE LEADER WAS STOPPING AT THE BOX, NOT AT THE NUMBER. `i` is a fixed 5.4rem
   cell with the text right-aligned inside it, so a two-character value sits ~73px
   from the cell's left edge and the leader — which ends where the cell begins —
   ran out that far short of the digits every single time. Measured on the
   fielding block: the dots ended at 159 and the numbers began at 194, 228 and
   236.
   A CELL SIZED TO ITS CONTENT LOSES NOTHING, which is the part worth stating,
   because the fixed width looks load-bearing. What the column needs is a common
   RIGHT edge, and that comes from the row plus the reservation below, not from
   the cell: with `width: auto` every value still ends at the same x and the
   leader now stops 8px — one flex gap — from the first digit.
   ONLY A LONE VALUE. A row with TWO values is a real two-column table (the
   season standing behind the live number) where the FIRST cell's width is what
   lines the columns up, so `:only-of-type` leaves those exactly as they were —
   the twelve rows s30 measured at a mean 86px ink-to-ink and called already
   fine. */
.tt-stat:not(.tt-dims) i:only-of-type { margin-right: calc(5.4rem + 0.5rem);
  flex: 0 0 auto; width: auto; }
/* The two-column HEADER shares this rule rather than restating it (s20): a
   header whose column width is written down twice is a header that stops
   lining up the first time either copy moves — which is exactly what happened,
   because `.stat-player` overrides this width and the duplicate did not know. */
/* A FIXED WIDTH, BECAUSE THAT IS WHAT A COLUMN IS (Brian, s28: "the alignment
   of this series and 2011 and the slash lines on the bottom and dots are all
   off, let's fix the UI on this").
   `min-width` let every row size its own cells. The counting stats all sat at
   the 3.4rem floor and lined up with each other by luck; `.375/.375/.875` is
   nearly twice that, so the slash row pushed BOTH its columns left, the dot
   leaders stopped at a different place on that row, and the header — sized off
   the same floor — lined up with the counts but not with the slash under them.
   Three different alignments in one panel.
   Sized to the WIDEST content rather than to the commonest: the slash line is
   the thing that has to fit, and once it does every shorter value right-aligns
   into the same edge. `flex: 0 0` so a long label cannot squeeze it back. */
.tt-stat i, .tt-head.tt-head-cols i { font-style: normal; flex: 0 0 5.4rem; width: 5.4rem;
  text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.tt-stat i em { font-style: normal; color: var(--muted); font-size: 0.85em; margin-left: 0.15rem; }
/* A HAND TAG INSIDE THE LABEL, not in a value column (s35 — see `dueUpHtml`).
   `b` is already the flex container the leader hangs off, so this lands between
   the name and the dots, which is where a roster row puts it too. */
.tt-stat b .tt-hand { color: var(--dim, #9bb3a2); font-weight: 600; font-size: 0.92em; }
/* TWO COLUMNS THAT READ AS TWO (s24, Brian: "format the stats better, clear
   delineation between this series and season"). Left is what he has done in the
   book you are playing; right is the season he really had. They sat in
   identical ink at identical weight with nothing between them, so a reader had
   to remember which was which from a heading four rows up.
   A rule down the gutter, and the REAL season is held back — it is the
   reference, and the live number is the one being read. The header's own
   labels get the same rule so the column and its name line up. */
/* "THE SECOND OF TWO", WHICH IS WHAT THIS ALWAYS MEANT (Brian, s26: "why is the
   RF measurements green?", then "why the lines?"). Both of these describe the
   TWO-COLUMN rows, where the last `<i>` is the real season standing behind the
   live one: held back in `--muted`, with a rule down the gutter between them.
   `i:last-child` DOES NOT SAY THAT. It says "the last one, however many there
   are" — so it fired on rows that have no second column at all, and the damage
   was in two different places at once:
   ONE VALUE — the ordinary scouting report, and any game with no season book
   behind it. The only number on the row got the divider (a rule dividing
   nothing) AND `--muted` (the figure grayed out as if it were a reference rather
   than the answer). Measured: `.290/.359/.430` came out #9ab3a0 with a 1px
   border-left and nothing to its left.
   FIVE VALUES — the park dimensions, which read ACROSS: LF, LC, CF and RC all
   rendered `--text` and RF alone `--muted` with a divider, so the right-field
   number looked like a different KIND of measurement for being fifth.
   `:nth-of-type(2):last-of-type` IS EXACTLY "the second, when it is also the
   last" — which is exactly two columns, and it rules out one and five without
   naming either. The header keeps `:last-child`: it is always a pair. */
.tt-stat i:nth-of-type(2):last-of-type, .tt-head.tt-head-cols i:last-child {
  border-left: 1px solid var(--rule, #2b4033); padding-left: 0.5rem;
}
.tt-stat i:nth-of-type(2):last-of-type { color: var(--muted); }
.tt-head.tt-head-cols i { color: var(--dim, #9bb3a2); font-size: 0.9em; }
/* Park dimensions are the one rollover that genuinely reads ACROSS — five short
   numbers walked around the outfield, which is how every ballpark reference has
   quoted them forever. The default .tt-stat cell is 3.4rem and sized for two
   values, so five of them would run the tooltip off the screen; these stack the
   label under the number instead and drop to a third of the width. */
.tt-dims i { display: flex; flex-direction: column; align-items: center; text-align: center;
  min-width: 0; flex: 1; gap: 0.05rem; }
.tt-dims i em { margin-left: 0; font-size: 0.78em; letter-spacing: 0.03em; }
.tt-dims b { flex: 0 0 auto; margin-right: 0.15rem; }
.tt-dims { gap: 0.3rem; }

/* ---- A real season: the setup pickers and the divisional standings ---- */
/* THREE TO A ROW, SO A LEAGUE IS A ROW (Brian, s36: "for division drop down, AL
   on one line NL on the next line"). It was `flex-wrap`, which packs by width —
   six pickers came out 4 + 2 and split the American League across both rows. A
   grid of three is the shape the content has: East, Central, West, twice. */
.szn-pick { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.75rem 1.5rem; align-items: center; margin: 0.6rem 0; }
@media (max-width: 700px) { .szn-pick { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.szn-pick select { min-width: 12rem; }
/* One button per club in the book, and the one you are reading is filled —
   the same "this is what is selected" green every other choice on the app uses.
   They wrap, because a league is however many clubs are in your locker. */
.stat-clubs { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.stat-club em { font-style: normal; color: var(--muted); margin-left: 0.35rem; }
.stat-club.on { border-color: var(--accent-2); box-shadow: inset 0 0 0 1px var(--accent-2); }
.stat-club.on em { color: var(--accent-2); }
/* Divisions read as separate tables side by side, the way a paper prints them,
   rather than one long list you have to find your club in. */
/* ONE ROW PER LEAGUE. The column count is the number of divisions IN that
   league (`--divs`), not whatever happens to fit — which is what let NL West
   wrap onto its own line under the other three. `minmax(0, 1fr)` rather than a
   19rem floor for the same reason: the floor is what forced the wrap. */
/* TWO LEAGUES ACROSS, DIVISIONS DOWN (Brian, s36: "make this two columns AL on
   the left and NL on the right"). Each league was a full-width row of three
   divisions, which worked at five columns a table and broke at eight — the name
   column was left with a sliver and wrapped to three lines. Half a screen a
   league, three tables stacked, which is how a standings page is read. */
.szn-leagues {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.9rem 1.4rem; margin-top: 0.8rem; align-items: start;
}
@media (max-width: 62rem) { .szn-leagues { grid-template-columns: 1fr; } }
/* A standings name is a club, not a sentence: it takes the room it needs and
   the numbers keep theirs. */
.standings .tname { white-space: nowrap; }

.szn-standings { display: grid; gap: 0.9rem 1.4rem;
  grid-template-columns: repeat(var(--divs, 3), minmax(0, 1fr)); margin-top: 0.8rem; }
/* A phone cannot hold three divisions across, so below the fold it stacks —
   still league by league, because each row is its own grid. */
@media (max-width: 62rem) {
  .szn-standings { grid-template-columns: 1fr; }
}
/* KEPT, THOUGH `.standings` NOW CARRIES THE WIDTH ITSELF: this one is the grid
   cell's own instruction and reads as such where the grid is defined. */
.szn-standings .standings { width: 100%; }
/* THE CUT LINE (Brian, s36: "do the wild card standings"). A wild card table is
   two groups printed as one list — the clubs holding a spot and the clubs chasing
   — and the only thing separating them is where the bracket stops. A rule under
   the last club IN says that, and says it without a legend. */
.wc-table { margin-top: 0.9rem; }
.wc-table tr.wc-cut > td { border-bottom: 2px solid var(--accent); }
.wc-table th:first-child { color: var(--accent); }
.szn-standings th:first-child { color: var(--accent); letter-spacing: 0.04em; }

/* ---- Season stat sheets ---- */
/* A full line is sixteen columns, which no phone will ever fit — so the sheet
   scrolls inside its own box rather than making the whole page scroll sideways. */
.stat-wrap { overflow-x: auto; margin: 0.6rem 0 1rem; }
/* AND WHEN THE BOX HOLDS MORE THAN ONE SHEET, each sheet brings its own scroller
   instead (`statTables`) and this one stands down — the batting, pitching and
   fielding tables are 769, 743 and 285 wide, and one scroller over the three
   gives the range to the widest and drags the other two past their own ends.
   Same shape, same answer, as `.box-team-wide`. */
.stat-wrap-wide { overflow-x: visible; }
.stat-sheet { width: 100%; min-width: 44rem; margin-bottom: 1rem; }
/* THE FIELDING TABLE SIZES TO ITS OWN COLUMNS (Brian, s25: "tighten up the
   fielding section"). It inherits `.stat-sheet`, whose full width and 44rem
   minimum are sized for BATTING's sixteen columns — four numbers stretched
   across that span put "OF assists" most of a screen away from the E beside it,
   which is what the screenshot shows. */
.stat-fld { width: auto; min-width: 0; max-width: min(38rem, 100%); }
.stat-fld th:not(.tname), .stat-fld td:not(.tname) { padding-left: 1.1rem; white-space: nowrap; }
.stat-sheet tr[data-player] { cursor: pointer; }
.stat-sheet tr[data-player]:hover { background: var(--panel-2); }
.stat-sheet tr.stat-on { background: var(--panel-2); outline: 1px solid var(--accent); }
.stat-player {
  background: var(--panel); border: 1px solid var(--accent); border-radius: 12px;
  padding: 0.7rem 0.9rem; margin: 0.6rem 0; max-width: 30rem;
}
.stat-player h4 { margin: 0 0 0.4rem; font-size: 1rem; color: var(--accent);
  display: flex; align-items: center; justify-content: space-between; }
.stat-player #stat-close { background: none; border: 0; color: var(--muted);
  font-size: 0.9rem; cursor: pointer; padding: 0 0.2rem; }
/* The full-size sheet has room for a wider column; it must override the WIDTH
   now rather than a floor, or the flex basis above would hold it at 5.4rem. */
.stat-player .tt-stat i, .stat-player .tt-head.tt-head-cols i { flex: 0 0 6rem; width: 6rem; }
/* THE LEADERS RUN TO THE FIGURE, NOT TO ITS COLUMN (Brian, s26: "the dots should
   go further across"). The dots stopped a good 30px short of a short number, and
   the reason was not the leader: it was the value cell. `min-width: 3.4rem` (5rem
   on the big card) reserves a column and right-aligns inside it, so `7.0` sat at
   the far end of a box whose empty left half the dots could never enter.
   THAT COLUMN IS ONLY WORTH RESERVING WHEN THERE ARE TWO OF THEM. With a second
   value behind it the width is what keeps the pairs lined up — but a row with ONE
   value has nothing to line up WITH, and the leaders are the thing joining label
   to figure now, which is the whole point of the treatment. `b` is `flex: 1`, so
   dropping the floor lets the leader take the slack and still leaves every figure
   flush to the same right edge.
   AFTER `.stat-player` ON PURPOSE: that rule is (0,2,1) and so is this one, so
   the tie goes to source order and the big card would otherwise keep its 5rem. */
.tt-stat i:only-of-type { min-width: 0; }
/* `.tt-counts` IS GONE, AND ITS THREE RULES WITH IT (s31). It wrapped the
   single-column block so that block alone could narrow its value cell, cap the
   row at 13rem and keep the slash row's weight — three separate arguments with
   flex, all of them to stop one column of right-aligned figures drifting away
   from their labels. `.tt-cols` is the same fix made structural and it serves
   one column and two from one set of rules, so the wrapper had nothing left to
   scope. The reasoning behind each of the three is in HISTORY.md; deleting them
   here rather than leaving them unmatched, because a rule nothing can select is
   a rule the next session has to disprove. */
/* THE TWO-COLUMN BLOCK SIZES ITS COLUMNS INDEPENDENTLY (Brian, s30: "size the
   columns independently"). This is the same disease the single column had, one
   block down and twice over — `.tt-stat i` is `flex: 0 0 5.4rem`, ONE basis
   serving both cells, so the series column carrying `0` and `1` was as wide as
   the season column carrying `648` and every row paid 5.4rem twice.
   FLEX CANNOT DO IT, WHICH IS WHY THIS IS A GRID. Sizing the two cells apart in
   flex means sizing them apart on each ROW, and then no two rows line up — the
   alignment and the independence are the same property, and only a shared set
   of tracks gives you both. `auto` measures each track against the widest thing
   in THAT COLUMN; the label keeps `1fr` so the dot leaders still fill the slack.
   SUBGRID RATHER THAN `display: contents` ON THE ROWS, and that is the whole
   reason this cut works where s30's did not. With `contents` every row must
   contribute exactly three items or the ones after it slide into the wrong
   tracks, and a row that is short by one puts five things on the header line —
   which is precisely what the reverted attempt rendered. A subgrid row keeps its
   own box, so it maps its own children onto the parent's tracks and a row with
   the wrong count can only be wrong about itself. It also keeps the row margins
   and `.tt-slash`'s own styling, which `contents` throws away. */
/* THREE EQUAL COLUMNS, AND THE VALUES SIT UNDER THEIR OWN HEADING — Brian's
   mockup, which is the specification. It settles the thing the arithmetic could
   not: the slash line STAYS in the tracks as the first row, both value columns
   size to hold it, and that is fine because the dead air it used to cause is not
   a width problem at all.
   THE DEAD AIR WAS THE RIGHT-ALIGNMENT, NOT THE COLUMN WIDTH. Measured on the
   shipped panel: a 5.4rem cell holding `2` right-aligned leaves ~78px of nothing
   between the end of the dot leader and the digit, and the leader was invented to
   carry the eye across exactly that. Centering the value in its column puts `2`
   directly under "this series" and beside `152` — so the gap closes without the
   column moving, and the leaders have no work left to do. That is why they come
   off here and stay on the single-column block, where there is only one column
   and right-alignment is still what lines the figures up.
   `1fr` THREE TIMES rather than `auto` twice: the mockup draws three equal cells,
   and an `fr` track keeps its automatic min-content floor, so the longest label
   ("Runs Batted In") and the widest value (`.356/.414/.511`) both still fit if a
   narrower tooltip ever squeezes this block. */
/* ONE MECHANISM, ONE COLUMN OR TWO (Brian, s31: "and one column for single
   game", "easy to read and easy to format"). A single game has no book behind it
   and the same report then carries one value column instead of two — which used
   to be a DIFFERENT layout with its own wrapper, its own width cap and its own
   leader rules, because flex had to be argued with once per shape. Here the only
   difference between the two is the track count.
   `width: max-content` IS WHAT MAKES IT READ. The tracks are equal `1fr`s, so
   stretching this to the panel would put a three-digit number in the middle of a
   155px cell with the label a screen away — the very complaint, re-created by a
   nicer mechanism. Sized to its own content the whole table is ~195px on one
   column and ~295 on two, and the figures sit next to the words they belong to.
   `max-width: 100%` so it can never push the panel wider than the panel is. */
.tt-cols { display: grid; column-gap: 0.5rem; width: max-content; max-width: 100%; }
.tt-cols-1 { grid-template-columns: repeat(2, 1fr); }
.tt-cols-2 { grid-template-columns: repeat(3, 1fr); }
/* SUBGRID RATHER THAN `display: contents` ON THE ROWS, and that is the whole
   reason this cut works where s30's did not. With `contents` every row must
   contribute exactly three items or the ones after it slide into the wrong
   tracks, and a row short by one puts five things on the header line — which is
   precisely what the reverted attempt rendered. A subgrid row keeps its own box,
   so it maps its own children onto the parent's tracks and a row with the wrong
   count can only ever be wrong about itself. It also keeps the row margins and
   `.tt-slash`'s own weight, which `contents` throws away. */
.tt-cols > .tt-stat, .tt-cols > .tt-head.tt-head-cols {
  grid-column: 1 / -1; display: grid; grid-template-columns: subgrid;
  align-items: baseline;
}
/* `width: 5.4rem` is a WIDTH, so it goes on applying to a grid item however the
   tracks are sized — the flex basis beside it is the part that stops mattering.
   Both have to come off or every column is 5.4rem again and the grid is
   decoration.
   LEFT, EVERY COLUMN (Brian, s31: "left align all columns"). The figures were
   right-aligned so they would line up with each other, which is what a column of
   numbers wants when nothing else marks where the column IS. The dotted grid
   marks it, so the alignment has no work left to do — and left-aligned, each
   value starts hard against its own rule instead of drifting to the far side of
   a cell it does not fill. That drift was the whole defect. */
.tt-cols .tt-stat i, .tt-cols .tt-head.tt-head-cols i {
  flex: none; width: auto; text-align: left;
}
/* A DOTTED GRID (Brian, s31: "create a dotting grid", and his mockup draws it).
   It replaces the dot leaders, and it is the same idea done structurally: a
   leader is a line drawn to carry the eye from a label to a figure, and a ruled
   cell does that for every row at once without any row having to be long enough
   to need one.
   THE LINES GO ON THE CELLS, NOT THE ROWS, because the vertical ones fall
   BETWEEN columns and a row is one box spanning all three — it has no interior
   edges to draw on. Each cell takes a right and a bottom rule; the table's own
   left and top edges close the box. That also means `column-gap` has to be 0:
   a gap puts a hole between a cell's right rule and the next cell's content, so
   the air comes from cell padding, inside the lines where it belongs. */
.tt-cols { column-gap: 0; }
.tt-cols > .tt-stat > b, .tt-cols > .tt-stat > i,
.tt-cols > .tt-head.tt-head-cols > b, .tt-cols > .tt-head.tt-head-cols > i {
  padding: 0.1rem 0.4rem;
}
/* AND THE ROW MARGIN GOES, which is not cosmetic. `.tt-stat` carries
   `margin: 0.12rem 0` from the days it was a free-standing flex line, and a
   margin between two ruled rows breaks the COLUMN rules into segments — the
   vertical dots stop at every row boundary and restart below the gap, so the
   table reads as thirteen separate strips. The air is the cell padding now. It
   also gets the panel 80px of its height back, on a box already 737 tall. */
.tt-cols > .tt-stat { margin: 0; }
.tt-cols > .tt-stat > b, .tt-cols > .tt-stat > i {
  border-right: 1px dotted var(--rule, #2b4033);
  border-bottom: 1px dotted var(--rule, #2b4033);
}
.tt-cols > .tt-stat > b { border-left: 1px dotted var(--rule, #2b4033); }
/* The first DATA row closes the top of the box. The heading is not part of it —
   the mockup puts "Series" and "Year" above the grid, captioning the columns
   rather than sitting in them, which is also what keeps the gold section rule
   above `Batting` reading as the heading's own. */
.tt-cols > .tt-head.tt-head-cols + .tt-stat > b,
.tt-cols > .tt-head.tt-head-cols + .tt-stat > i {
  border-top: 1px dotted var(--rule, #2b4033);
}
/* THE LEADERS COME OFF WITH THE RIGHT-ALIGNMENT, because they are the same
   decision. A dot leader exists to cross an empty column; the grid rules the
   column instead, and a leader running to a cell edge is a second line drawn
   along the first. `display: none` rather than `content: none` — the rule it is
   overriding sets `content: ""` and a flex `::after`, so the box has to go, not
   just its (already empty) content. */
.tt-cols .tt-stat b { display: block; }
.tt-cols .tt-stat b::after { display: none; }
/* The summary keeps the weight it has in the block it replaces, so the row that
   opens the table still reads as the line the counts add up to. */
.tt-cols .tt-slash i { font-weight: 600; }
/* AND THE OLD GUTTER RULE STANDS DOWN. A single solid `border-left` between the
   live column and the real season was the only column line this block had; the
   dotted grid draws every one of them now, and leaving it on puts a solid rule
   down one seam of a dotted table. Its indent goes with it — the cell padding
   above is the air, inside the lines. */
.tt-cols .tt-stat i:nth-of-type(2):last-of-type {
  border-left: 0; padding-left: 0.4rem;
}
/* AND THE ONE-VALUE HACK COMES OFF, because the grid is what it was standing in
   for. `i:only-of-type` carries `margin-right: calc(5.4rem + 0.5rem)` to slide a
   lone value out of a second column that flex had reserved and nothing was going
   to fill — a real fix for a real defect, and meaningless here: a one-column
   table has no second track to escape. Left on, it pushes every single-game
   figure a full column clear of its own cell. */
.tt-cols .tt-stat i:only-of-type { margin-right: 0; }
/* AND THE HEADING KEEPS ITS ADJACENCY. `.mg-tooltip .tt-head` draws a gold rule
   above every section and `.tt-head-line + .tt-head` cancels it for the first
   one — an ADJACENT-SIBLING rule, which a wrapper silently breaks: the heading
   is now `.tt-cols`'s child rather than the identifying line's sibling, so
   "Batting" came out ruled off from the player's own name. The old wrapper never
   hit this because it held the ROWS and left the heading outside; this one
   cannot, since the heading is a row of the table and shares its tracks. */
.mg-tooltip .tt-head-line + .tt-cols > .tt-head,
.mg-tooltip .tt-name + .tt-cols > .tt-head {
  border-top: 0; margin-top: 0.1rem; padding-top: 0;
}
.stat-player .tt-head { display: flex; justify-content: space-between; color: var(--accent-2);
  font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase; }
.stat-player .tt-head i { font-style: normal; font-weight: 500; min-width: 5rem;
  text-align: right; color: var(--muted); }
.leaders-head { margin-top: 1.2rem; }

/* ---- Sortable tables of totals ---- */
/* Only the heading is interactive, and it says so on hover. The arrow shows the
   live column so a sorted sheet never looks like an unsorted one. */
th.th-sort { cursor: pointer; user-select: none; white-space: nowrap; }
th.th-sort:hover { color: var(--accent); }
th.th-sort.sorted { color: var(--accent); }
th.th-sort.sorted::after { content: " ▾"; font-size: 0.8em; }
th.th-sort.sorted[data-dir="asc"]::after { content: " ▴"; }

/* The ninth spot when the pitcher hits: he is named, and the designated hitter
   who is sitting is named too, so the card never prints "DH" over a man who
   will not come to the plate. */
.ln-note { display: block; color: var(--muted); font-size: 0.7rem; font-style: italic; }

/* ---- ONE PROMPT BEFORE THE FIRST PITCH (Brian, s17) -----------------------
   A real overlay rather than a native confirm(): the browser's own dialog is
   the one piece of chrome in the app that cannot be styled, and it says
   "localhost:8173 says" above a question about your batting order.
   It is appended to `body` by `askPlayBall`, OUTSIDE `#app`, so the screen
   router never has to know about it — same posture as the footer glossary. */
.mg-confirm {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6, 10, 7, 0.72);
  padding: 1.5rem;
}
.mg-confirm[hidden] { display: none; }
.mg-confirm-box {
  max-width: 26rem; width: 100%;
  background: var(--panel); border: 1px solid var(--accent); border-radius: 14px;
  padding: 1.1rem 1.25rem 1rem;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
}
.mg-confirm-box h2 { color: var(--accent); font-size: 1.15rem; margin-bottom: 0.4rem; }
.mg-confirm-box p { color: var(--muted); font-size: 0.85rem; line-height: 1.45; }
.mg-confirm-actions {
  display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 1rem;
}
.mg-confirm-actions button {
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px;
  font-size: 0.85rem; padding: 0.45rem 1rem; cursor: pointer;
}
.mg-confirm-actions button.primary {
  background: var(--accent); color: #14210f; border-color: var(--accent); font-weight: 700;
}
.mg-confirm-actions button:hover { border-color: var(--accent); }


/* ---- THE SCHEDULE PAGE (Brian, s31) ---------------------------------------
   "bring result and record closer to opponent and make each row clickable."
   It inherits `.stat-sheet`, whose `width: 100%` and 44rem floor are sized for
   BATTING's sixteen columns — five columns stretched across that span put the
   result most of a screen from the club it belongs to, which is the same
   complaint, and the same cause, as the fielding table in s25. Sized to its own
   content instead, so the four things that make a row read as one line sit
   together. `.tname` keeps the slack because the club name is the long one. */
.sch-table { width: max-content; min-width: 0; max-width: 100%; }
.sch-table .sch-res, .sch-table .sch-rec { white-space: nowrap; padding-left: 1.1rem; }
/* AND THE CLUB COLUMN DOES NOT STRETCH. `width: max-content` sizes the table to
   its own widest row, which is what closes the gap — a 34rem cap still left the
   name column reserving the width of the longest club in the league, so a row
   reading "vs 1986 Reds" put its result 161px away from it. Measured before and
   after; the number is in HANDOFF. */
.sch-table .tname { width: 1%; white-space: nowrap; }
.sch-table .sch-rec { color: var(--muted); font-variant-numeric: tabular-nums; }
/* A ROW THAT OPENS SOMETHING SAYS SO. Only the played rows with a box still on
   the shelf are clickable — a future game has nothing to open, and one whose
   box has aged out of `keepBox`'s window would open the wrong game. */
.sch-table tr.sch-open { cursor: pointer; }
.sch-table tr.sch-open:hover { background: var(--panel-2); }
/* The stat sheet's division menus (s31). One per division once the book is a
   league rather than a handful of clubs — see `renderStatSheets`. They wrap the
   way the buttons they replace did, so a 26-club season is two tidy rows
   instead of four rows of buttons above the sheet you came to read. */
/* The league's name sits ABOVE its menu rather than beside it: beside costs
   about 120px a control and two of them stop fitting a phone, where stacked
   they keep the select at its own 11rem and simply wrap. Same small-uppercase
   accent the season picker's own labels use (`.szn-pick-lb`) — the page has one
   kind of control label, not two. */
.stat-div { display: inline-flex; flex-direction: column; gap: 0.2rem; }
.stat-div-lb {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--accent); font-weight: 700;
}
/* IN THE SITE'S OWN CLOTHES (Brian, s36: "pulldowns for season's team stats need
   to be in site style"). These were the only selects on a full screen with no
   rules of their own, so they rendered as native OS controls — white boxes on a
   dark panel, with the focused one drawing the system's blue ring. Same tokens
   the dugout's approach menus use; `.mg-approach select` is the pattern. */
.stat-div-sel {
  min-width: 11rem;
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.28rem 0.5rem;
  font-size: 0.82rem;
  font-family: inherit;
}
.stat-div-sel:hover { border-color: var(--accent); }
.stat-div-sel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ---- THE BOOK, FROM INSIDE THE GAME (Brian, s19) ---------------------------
   "Real-time W-L on the scoreboard, and a way to view series/season stats from
   there." The stats are a PANEL OVER the dugout rather than a screen, because
   the game is live in `mg` and inside a running generator — routing away and
   back is a restructure, an overlay costs nothing and cannot lose the game.
   Appended to `body` by `openDugoutStats`, outside `#app`, so the router needs
   no entry for it: the same posture as `.mg-confirm` above.
   It sits at a LOWER z-index than the play-ball confirm, which is a question
   that must be answered; this is a reference you dismiss. */
.dugout-stats {
  position: fixed; inset: 0; z-index: 280;
  display: flex; align-items: flex-start; justify-content: center;
  background: rgba(6, 10, 7, 0.78);
  padding: 1.5rem 1rem;
  overflow-y: auto;
}
.ds-panel {
  width: min(1100px, 100%);
  background: var(--bg); border: 1px solid var(--accent); border-radius: 14px;
  padding: 1rem 1.1rem 1.4rem;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
}
/* The sheets inside carry tables with a width FLOOR — the same reason
   `.stat-wrap` and `#mg-scoreboard` have their own scroll boxes. Without this
   a sixteen-column pitching table pushes the whole overlay sideways on a
   phone, which is the overflow this file has fixed four times. */
.ds-body { min-width: 0; overflow-x: auto; }

/* THE SERIES LINEUP CARD DOES NOT FOLD (Brian, s28). It is a plain block rather
   than a <details>, so it needs the heading a <summary> was giving it — matched
   to the summary's own weight and color so the two screens that show a card
   still read as showing the same card. */
.mg-hold-dials input { cursor: pointer; }

/* THE CARD'S OWN NAME, READ AT A GLANCE (Brian, s36: "make 'Lineup card — 2024
   New York Mets' a larger font"). It sat at the same size as the rows beneath
   it, so the heading of the panel was the quietest thing in it. */
.lc-head-big { font-size: 1.05rem; letter-spacing: 0.01em; }
@media (max-width: 40rem) { .lc-head-big { font-size: 0.98rem; } }

.lc-pinned > .lc-head {
  color: var(--accent); font-weight: 700; font-size: 0.92rem;
  margin-bottom: 0.6rem;
}
/* THE WAY OUT, INSIDE THE PANEL (Brian, s28: "on stats overlay during game need
   a back button"). It STICKS, because the sheets under it run to several
   thousand pixels and a button that scrolls away is the same complaint again a
   screen further down. The background is opaque so the tables do not run under
   it, and it sits above them without needing a z-index war — nothing else in
   the panel is positioned. */
.ds-nav {
  position: sticky; top: -1rem;
  margin: -1rem -1.1rem 0.6rem; padding: 0.75rem 1.1rem 0.6rem;
  background: var(--bg);
  border-bottom: 1px solid var(--line, rgba(255, 255, 255, 0.12));
  border-radius: 14px 14px 0 0;
}

/* ---- ASK THE COACH (Brian, s17) -------------------------------------------
   A face and a prompt beside the approach dial; his call arrives on rollover
   through the app's own green tooltip. It replaced an always-open block that
   put a sentence on screen every pitch whether you wanted it or not — this
   OFFERS the advice in the row where you are already making the decision.
   The portraits are why img/oldschool.jpg and its two siblings exist again;
   they went unreferenced when the coach cards moved onto the dugout scene
   earlier in the same session. */
.mg-ask {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.15rem 0.5rem 0.15rem 0.15rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel-2); cursor: help; white-space: nowrap;
}
.mg-ask:hover { border-color: var(--accent); }
.mg-ask-face {
  width: 26px; height: 26px; border-radius: 50%;
  object-fit: cover; object-position: 50% 22%;
  border: 1px solid var(--accent-2); background: var(--panel);
}
.mg-ask-cue { color: var(--accent-2); font-size: 0.7rem; font-weight: 600; }
/* The fielding chip sits inside `.mg-tactics-head`, which is `uppercase` for the
   section label — the chip is a name, not a label, and "ASK POPS" shouts
   (Brian, s17). Letter-spacing comes off with it, or the name reads spaced out. */
.mg-tactics-head .mg-ask-cue { text-transform: none; letter-spacing: normal; }
.mg-tactics-head .mg-ask { margin-left: 0.5rem; vertical-align: middle; }

/* The season campaign's exit, moved out of the heading row to the foot of the
   screen (Brian, s17) — beside the title it was the first control your eye met
   and the most destructive one there. */
.campaign-foot { display: flex; justify-content: flex-end; margin-top: 1rem; }

/* A bullpen arm who has been working. Deliberately quiet — almost every arm is
   rested, so this only ever marks the exception, and the figure in its rollover
   is read off the engine's own multiplier rather than restated. */
.mg-worn {
  font-size: 0.58rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--danger); border: 1px solid var(--danger); border-radius: 20px;
  padding: 0 5px; cursor: help; white-space: nowrap; opacity: 0.85;
}

/* ---- THE LINEUP CARD BETWEEN GAMES (s18, Brian) --------------------------
   Drawn on the three pregame screens — a managed series, the round-robin
   campaign and a real season. COLLAPSED BY DEFAULT, the same posture as the
   locker room and the footer glossary: a pregame screen already carries the
   standings and the starter picker, and most nights the card needs no edit.
   The <details> owns its own open state, so nothing has to remember it and
   only `.lc-body` is ever re-rendered.

   The row vocabulary is the mid-game realignment panel's on purpose — a "who"
   select, the word "at", a position select, and the same red flag for a man
   standing somewhere he has never played. It is the same act at a different
   moment, so it should not be a second idiom to learn. */
.lc {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 0.6rem 0.8rem;
  margin: 0.8rem 0;
}
.lc > summary { cursor: pointer; color: var(--accent); font-weight: 700; font-size: 0.92rem; }
.lc[open] > summary { margin-bottom: 0.6rem; }
.lc-body { display: flex; flex-direction: column; gap: 0.3rem; }
/* EVERY CHILD IS PLACED BY NAME, not by source order, because the drag grip is
   `display: none` on a touch device — and a hidden grid item does not hold its
   column, so positional placement would shift every cell after it. */
.lc-row {
  display: grid;
  grid-template-columns: auto auto auto minmax(0, 1fr) auto auto minmax(0, 1.4fr) auto;
  grid-template-areas: "ord grip move who at pos line flag";
  gap: 0.4rem;
  align-items: center;
  padding: 0.12rem 0.1rem;
}
.lc-ord { grid-area: ord; color: var(--muted); font-size: 0.72rem; width: 1.1rem; text-align: right; }
.lc-grip { grid-area: grip; }
.lc-move { grid-area: move; display: flex; gap: 0.15rem; }
.lc-who { grid-area: who; }
.lc-at { grid-area: at; }
.lc-pos { grid-area: pos; }
.lc-line { grid-area: line; }
.lc-flag, .lc-row .ln-note { grid-area: flag; }
.lc-move button {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 5px; width: 1.35rem; height: 1.35rem; line-height: 1;
  font-size: 0.7rem; cursor: pointer; padding: 0;
}
.lc-move button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.lc-move button:disabled { opacity: 0.3; cursor: default; }
.lc-who, .lc-pos {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 6px; padding: 3px 6px; font-size: 0.78rem; max-width: 100%;
}
.lc-who { min-width: 0; }
/* ---- Reordering: the INPUT chooses the control, not the viewport ----------
   ↑/↓ are the default because they work on every device and with a keyboard;
   HTML5 `draggable` fires on neither touch nor keys. A fine pointer — a mouse —
   gets the drag handle instead, which is how the builder's batting order has
   always worked and how a manager thinks about a lineup card.

   NOTE THIS IS A POINTER QUERY, NOT A WIDTH ONE. A narrow desktop window still
   drags; a wide tablet still gets buttons. Width decides the LAYOUT (the stack
   below); the pointer decides the CONTROL, and conflating them is what would
   hand an iPad Pro a drag handle it cannot use. */
.lc-grip { display: none; cursor: grab; color: var(--muted); font-size: 0.8rem; user-select: none; }
.lc-grip:active { cursor: grabbing; }
.lc-row.dragging { opacity: 0.45; }
.lc-row.drag-over { outline: 1px dashed var(--accent); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .lc-grip { display: inline; }
  /* The buttons stay in the DOM and in the tab order — faded, not removed, so
     `display: none` cannot make them unfocusable and strand a keyboard user
     with a drag handle he has no way to operate. */
  .lc-move { opacity: 0; transition: opacity 0.12s ease; }
  .lc-row:hover .lc-move, .lc-row:focus-within .lc-move { opacity: 1; }
}
/* THE PITCHER'S OWN ROW, when he takes the ninth turn (s35). Placed by the same
   named areas as everybody else so the columns line up down the card, but the
   two selects are plain text — there is nothing on this row to choose. The
   grip and the ↑↓ are not drawn at all rather than drawn disabled: the row
   cannot move, because the reseat puts the arm ninth however the card is
   arranged, and a faded control still invites the click. */
.lc-fixed-who { grid-area: who; padding: 3px 0; }
.lc-fixed-pos { grid-area: pos; padding: 3px 6px; font-size: 0.78rem;
  color: var(--muted); border: 1px dashed var(--line); border-radius: 6px;
  text-align: center; }
.lc-armrow .lc-fixed-who strong { color: var(--accent-2, var(--accent)); }
.lc-at { font-size: 0.68rem; color: var(--muted); }
.lc-line { font-size: 0.72rem; color: var(--muted); min-width: 0; }
.lc-row.oop .lc-who, .lc-row.oop .lc-pos { border-color: var(--danger); }
.lc-flag {
  font-size: 0.62rem; color: var(--danger); text-transform: uppercase;
  letter-spacing: 0.04em; white-space: nowrap;
}
/* ---- League leaders: the scope tabs (Brian, s20) -------------------------- */
.lead-tabs { display: flex; flex-wrap: wrap; gap: 0.4rem; }
/* **THE TABS WERE LIVING IN ONE CELL OF A THREE-COLUMN GRID (s49, Brian:
   "layout of the leagues in the stat sheet page and leaders page
   horizontally").** `.szn-pick` is the season picker's layout — three equal
   columns — and the leaders page reuses its shell to hold the league tabs. So
   "AL leaders / NL leaders / All of baseball" had a third of the width to sit
   in and wrapped onto two rows, which reads as a broken row rather than a
   deliberate one. Spanning the row gives them the width they were always
   drawn for; `flex-wrap` above stays as the narrow-screen fallback. */
.szn-pick > .lead-tabs { grid-column: 1 / -1; }
/* The last-10 / last-20 row on the stat sheet takes the same escape hatch, for
   the same reason: it is a row of tabs living inside a three-column grid, and a
   third of the page is not a row. It reuses `.lead-tabs` for the flex and the
   gap so the two rows of tabs in this app cannot drift apart. */
.szn-pick > .stat-wins { grid-column: 1 / -1; }
/* AND THE CLUB PICKER ITSELF, for the third time in this stylesheet (s50). The
   row of club controls lives in ONE CELL of `.szn-pick`'s three-column grid, so
   it had a third of the page to lay out in. With four division menus that came
   out as a 2x2 block; splitting them by league left TWO menus stacked one above
   the other, because `.szn-pick select` has a 12rem minimum and two of those do
   not fit in a third of the page. Spanning the row puts them side by side — and
   it is right for the BUTTON variant underneath it too, which is what s24 asked
   for in the first place ("every one of them fits on the line"). */
.szn-pick > .stat-clubs { grid-column: 1 / -1; }
.lead-tab.on {
  border-color: var(--accent); color: var(--accent);
  background: rgba(245, 197, 66, 0.12);
}
.leaders-head { margin: 0.9rem 0 0.2rem; }
/* ---- The season schedule (Brian, s20) ------------------------------------ */
/* THE SCHEDULE IS A TABLE, NOT A LIST (Brian, s21: "format schedule better,
   make it tighter and a lined table"). It inherited `.stat-sheet`, which sets a
   width and nothing else — no collapse, no cell padding, no rules — so 162 rows
   of four short cells came out as loosely-spaced prose with the columns drifting
   apart. A season's card is a thing you SCAN: one line per game, ruled, with the
   numbers on a fixed pitch so the eye can run down them. */
.sch-table {
  min-width: 26rem;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  font-size: 0.86rem;
}
.sch-table th, .sch-table td {
  padding: 0.2rem 0.6rem;
  line-height: 1.45;
  border-bottom: 1px solid var(--line);
  text-align: left;
  white-space: nowrap;
}
.sch-table th {
  color: var(--accent-2);
  font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
  font-weight: 700;
  /* The header stays put over 162 rows — the one column heading you lose first
     on a long card is the one telling you which number is the record. */
  position: sticky; top: 0; background: var(--bg); z-index: 1;
}
/* The game number is a gutter, not a column of data. */
.sch-table td:first-child { color: var(--muted); width: 2.5rem; text-align: right; }
/* The opponent takes whatever is left, so the three short columns after it stay
   packed together rather than being pushed to the far edge of a wide screen. */
.sch-table .tname { width: 99%; white-space: normal; }
.sch-table tr:hover td { background: var(--panel-2); }
.sch-w { color: var(--accent-2); }
.sch-l { color: var(--danger); }
/* A game not yet played is dimmed rather than hidden — the point of the page is
   that the future is on it. */
.sch-future td { opacity: 0.55; }
.sch-none { color: var(--muted); font-size: 0.9em; }

.lc-note, .lc-bench { font-size: 0.68rem; color: var(--muted); line-height: 1.4; padding-top: 0.25rem; }
/* Why the bench is one man longer tonight — see `benchList`. It heads the list
   rather than hanging off the displaced hitter's row, so it reads as a rule of
   the game and not as something wrong with him. */
.lc-bench-why {
  font-size: 0.7rem; color: var(--muted); font-style: italic;
  padding: 0.1rem 0 0.25rem;
}
/* ---- The bench, as clickable men -----------------------------------------
   A row here is the same shape as a lineup row minus the controls, so the eye
   reads down both without re-learning anything. It deliberately does NOT reuse
   `.lc-row`'s grid: that one reserves tracks for two selects, three buttons and
   a grip, and a bench row has none of them — inheriting it would leave the name
   floating in the middle of the row past four empty columns. */
.lc-bench-head {
  font-size: 0.62rem; color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.06em; margin: 0.55rem 0 0.15rem;
  border-top: 1px solid var(--line); padding-top: 0.45rem;
}
.lc-bench-row {
  display: grid;
  /* THE POSITION COLUMN HAS TO FIT WHAT IS IN IT (Brian, s36: "fix the
     formatting 2B/RF/LF/DHJeff McNeil"). A fixed 3.4rem could hold two spots;
     a four-position man overflowed his own column and printed straight into the
     name beside him. `max-content` lets the column grow to the longest list on
     the card and the gap does the rest. */
  grid-template-columns: minmax(3.4rem, max-content) minmax(0, auto) minmax(0, 1fr);
  gap: 0.4rem; align-items: baseline;
  padding: 0.16rem 0.25rem; border-radius: 6px; cursor: pointer;
}
/* `.lc-line` carries `grid-area: line` for the lineup row's named template.
   This grid has no such area, so that name resolves to an implicit line and
   drops the stat line onto a row of its own — every bench man two lines tall
   and right-ragged. Put it back in normal flow. */
.lc-bench-row .lc-line { grid-area: auto; }
/* **THE NAME LIGHTS UP, NOT THE ROW (Brian, s38: "the whole row shouldn't
   activate on rollover, too much").** A bench row is a name and then a long
   line of season figures, and washing the whole width of it on hover moved far
   more ink than the pointer was asking about — and it fought the scouting
   report, which is the thing the reader is actually reading at that moment.
   The row is still the click target; only the affordance moved. */
.lc-bench-row:hover .lc-bench-nm { color: var(--accent); }
.lc-bench-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* AN INJURED ROW IS NOT A CONTROL (s39). It takes the bench row's grid because
   it is the same three columns of information, and none of its affordances:
   there is no move to make on a man who is hurt, and a pointer saying otherwise
   is the inert-editor defect this file has recorded twice. */
.lc-hurt-row, .lc-pen-row { cursor: default; }
.lc-hurt-row:hover .lc-bench-nm, .lc-pen-row:hover .lc-bench-nm { color: inherit; }
.lc-hurt-row .lc-bench-nm, .lc-hurt-row .lc-line { opacity: 0.72; }
/* AN ARM WHO CANNOT PITCH TONIGHT (s41). The same dimming an injured row gets,
   and for the same reason: he is on the roster, he is on the card, and he is not
   an option right now. The row STAYS — Brian's rule is that everybody on the
   thirty shows up — so the state has to be legible rather than absent, which is
   what dropping him from the list used to do. Dimming the name and the line and
   not the note, because the note is the part that says why. */
.lc-pen-down .lc-bench-nm, .lc-pen-down .lc-line { opacity: 0.72; }
/* **THE PEN ROW HAS A FOURTH CHILD AND THE GRID HAS THREE COLUMNS (s49, Brian:
   "the bullpen notes, i.e. worked hard yesterday and the pitcher name indented,
   format cleaner").** `.lc-bench-row` is pos | name | line; a pen row adds the
   note, so it fell into an implicit second row at COLUMN ONE — flush to the far
   left, under the position badge, while the name it belongs to sits a column
   over. Spanning it from the name column puts the remark under the man.

   AND THE BADGE COLUMN IS SIZED FOR SOMEBODY ELSE. 3.4rem is there so a
   four-position bench man ("LF/1B/DH") does not print into his own name; a pen
   badge is always two letters, so every arm was indented by a column reserved
   for a list he does not have. `max-content` still grows if a role ever needs
   it — this only lowers the floor. */
.lc-pen-row { grid-template-columns: minmax(2rem, max-content) minmax(0, auto) minmax(0, 1fr); }
.lc-pen-row .ln-note { grid-column: 2 / -1; margin-top: 0.04rem; }
.lc-bench-pos { font-size: 0.6rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.lc-bench-nm { font-size: 0.8rem; }
/* THE RESERVE ROWS (s38) — the rest of the club, offered on the season's own
   card. Same grid as a bench row plus a trailing control, and NOT a `role`
   button: a bench row is armed by clicking it, and this one is operated by the
   select inside it, so making the row clickable too would arm a substitution
   every time somebody reached for the menu. */
.lc-res-row {
  display: grid;
  grid-template-columns: minmax(3.4rem, max-content) minmax(0, auto) minmax(0, 1fr) max-content;
  gap: 0.4rem; align-items: baseline;
  padding: 0.16rem 0.25rem; border-radius: 6px;
}
.lc-res-row .lc-line { grid-area: auto; }
.lc-res-row:hover .lc-bench-nm, .lc-res-row:hover .lc-res-nm { color: var(--accent); }
.lc-res-in {
  font: inherit; font-size: 0.7rem; max-width: 11rem;
  background: var(--panel2, rgba(255, 255, 255, 0.06));
  color: inherit; border: 1px solid var(--line); border-radius: 6px;
  padding: 0.1rem 0.25rem;
}
/* Below the phone breakpoint the four columns cannot all fit, and the control is
   the one that must stay reachable — so the stat line yields first, exactly as
   the bench row's does. */
@media (max-width: 620px) {
  .lc-res-row { grid-template-columns: minmax(3rem, max-content) minmax(0, 1fr) max-content; }
  .lc-res-row .lc-line { display: none; }
}
/* The armed man and the spots he can take share the manage screen's own
   `.sub-picked` / `.sub-target`, so the between-games move and the seventh-
   inning one look like the same gesture. Only the inset ring is wanted on a
   lineup row, which already carries its own background states. */
.lc-row.sub-target { border-radius: 6px; }
.lc-actions { margin-top: 0.5rem; }
.lc-actions .lc-auto {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 8px; padding: 0.3rem 0.7rem; font-size: 0.78rem; cursor: pointer;
}
.lc-actions .lc-auto:hover { border-color: var(--accent); color: var(--accent); }
/* On a phone the seven-column row cannot hold. It stacks into two lines — the
   order and its buttons above, the two selects below — rather than shrinking
   the selects, which a <select> will not do past its longest option anyway
   (the s14 overflow, and its cause). */
@media (max-width: 620px) {
  .lc-row {
    grid-template-columns: auto auto auto minmax(0, 1fr);
    grid-template-areas:
      "ord grip move who"
      ".   .    at   pos"
      ".   .    .    line"
      ".   .    .    flag";
    row-gap: 0.2rem;
  }
  /* THE FLAG SITS UNDER THE SELECTS, not at the left margin. Stacked, a flag in
     column 1 reads as a heading for the NEXT man rather than as a note on this
     one — it is three lines below the name it is about. */
  .lc-flag, .lc-row .ln-note { white-space: normal; }
  /* ...and a hairline, because three lines per man with no rule between them is
     one long column of controls rather than nine rows. */
  .lc-row + .lc-row { border-top: 1px solid var(--line); padding-top: 0.45rem; }
}

/* ---- THE ROTATION CARD (s18, Brian) --------------------------------------
   Shares the lineup card's reorder idiom exactly — grip on a fine pointer,
   ↑/↓ everywhere else, same two drag classes — because it is the same act on a
   different list and should not be a second thing to learn.
   NOT a <details>: the rotation is a decision you make every night, where the
   lineup usually is not, so it sits open. */
.rc {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 0.6rem 0.8rem;
  margin: 0.8rem 0;
}
.rc-head { color: var(--accent); font-weight: 700; font-size: 0.92rem; margin-bottom: 0.5rem; }
.rc-hint { display: block; color: var(--muted); font-weight: 400; font-size: 0.72rem; margin-top: 0.15rem; }
.rc-body { display: flex; flex-direction: column; gap: 0.3rem; }
/* THE LAST TRACK IS RESERVED, NOT `auto` (Brian, s20: "alignment in the three
   columns of the rotation is weird").

   Every row is its OWN grid — four grids pretending to be a table — so each one
   solved its column widths alone. The row taking tonight's start carries no
   rest note, so its `flag` track collapsed to zero and the two `fr` columns
   split the freed width between them: measured, the name column began at the
   same x on all four rows but the ERA line began at 557px on three of them and
   625px on the fourth. Nothing was centered; a column simply vanished on one
   row and the others grew into the hole.

   A FIXED track cannot collapse, so all four rows now agree. 11.5rem is sized
   against the widest string the column can hold — "started game 148 — back for
   game 152" measures 175px in a 162-game season — rather than against the 158px
   that happened to be on screen. */
.rc-row {
  display: grid;
  /* `member` joined in s33 — who is IN the rotation, which is a different
     decision from the order and needed its own column rather than crowding the
     arrows that set the turn. */
  /* THE FLAG COLUMN SHRINKS AND WRAPS (Brian, s35: "misaligned"). It was a FIXED
     11.5rem against `white-space: nowrap` copy, which is a width you declared
     against content that can override it — the trap this file records twice. s35
     made the sentence longer ("started game 1 · two games' rest — back for game
     4") to show the rest arithmetic, and it ran straight out of the column and
     past the row. `minmax(0, …)` lets it give way and `normal` lets it take a
     second line, so no wording can push it out again. */
  grid-template-columns: auto auto auto auto minmax(0, 1fr) minmax(0, 1.3fr) minmax(0, 11.5rem);
  grid-template-areas: "ord grip move member pick line flag";
  gap: 0.4rem;
  align-items: center;
  padding: 0.15rem 0.1rem;
  border-radius: 6px;
}
.rc-ord { grid-area: ord; color: var(--muted); font-size: 0.72rem; width: 1.1rem; text-align: right; }
.rc-grip { grid-area: grip; display: none; cursor: grab; color: var(--muted); font-size: 0.8rem; user-select: none; }
.rc-grip:active { cursor: grabbing; }
.rc-move { grid-area: move; display: flex; gap: 0.15rem; }
.rc-member { grid-area: member; display: flex; }
/* Same chip as the turn arrows, so the two controls read as one family — the
   accent on hover is what says this one CHANGES something rather than reorders. */
.rc-member button {
  background: var(--panel-2); color: var(--muted); border: 1px solid var(--line);
  border-radius: 5px; width: 1.35rem; height: 1.35rem; line-height: 1;
  font-size: 0.9rem; cursor: pointer; padding: 0;
}
.rc-member button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.rc-member button:disabled { opacity: 0.35; cursor: default; }
.rc-move button {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line);
  border-radius: 5px; width: 1.35rem; height: 1.35rem; line-height: 1;
  font-size: 0.7rem; cursor: pointer; padding: 0;
}
.rc-move button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.rc-move button:disabled { opacity: 0.3; cursor: default; }
.rc-pick { grid-area: pick; display: flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; cursor: pointer; min-width: 0; }
.rc-pick input { accent-color: var(--accent); }
.rc-line { grid-area: line; font-size: 0.72rem; color: var(--muted); min-width: 0; }
.rc-flag { grid-area: flag; font-size: 0.62rem; color: var(--danger);
  white-space: normal; text-align: right; line-height: 1.25; }
/* TONIGHT'S MAN, and a man who cannot go. The blocked row keeps its full line
   rather than being hidden — "why can't I start him" is the question the rule
   creates, so the answer has to be on the row that raises it. */
.rc-row.picked { background: rgba(245, 197, 66, 0.07); }
/* Bullpen men who started that season — below the rotation, out of its order,
   so they carry no ordinal and no grip. The rule keeps the same six columns so
   the names still line up with the four above them. */
.rc-xhead {
  margin: 0.5rem 0 0.15rem; padding-top: 0.4rem; border-top: 1px solid var(--line);
  font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em;
}
.rc-xrow .rc-grip { cursor: default; }
.rc-row.blocked .rc-pick { opacity: 0.45; cursor: default; }
.rc-row.dragging { opacity: 0.45; }
.rc-row.drag-over { outline: 1px dashed var(--accent); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .rc-grip { display: inline; }
  .rc-move { opacity: 0; transition: opacity 0.12s ease; }
  .rc-row:hover .rc-move, .rc-row:focus-within .rc-move { opacity: 1; }
}
@media (max-width: 620px) {
  .rc-row {
    grid-template-columns: auto auto auto minmax(0, 1fr);
    grid-template-areas:
      "ord grip move pick"
      ".   .    .    line"
      ".   .    .    flag";
    row-gap: 0.15rem;
  }
  .rc-flag { white-space: normal; }
  .rc-row + .rc-row { border-top: 1px solid var(--line); padding-top: 0.45rem; }
}

/* "TIRING" IN BOLD RED (s18, Brian). It sat in the same muted gray as the rest
   of the arm's line — a warning rendered as a footnote. It is the one word on
   that line you are supposed to act on, so it carries the danger color the
   out-of-position flag already uses rather than a color of its own. Not
   uppercased: the flags around it are labels, this is a word in a sentence. */
/* TIRING, IN CAPITALS AND BREATHING (s24, Brian: "in pitcher box 'tiring'
   should be 'TIRING' and slightly pulse"). It is the one line on the panel that
   is a warning rather than a fact, and it read as neither — same size, same
   weight, same case as the pitch count beside it.
   THE PULSE IS SLIGHT AND IT STOPS FOR ANYONE WHO ASKS IT TO. A thing that
   moves forever in the corner of the eye is a tax on reading the rest of the
   panel, and `prefers-reduced-motion` is the standing answer. */
/* The clause that says a run came in, inside a play that opens with an out. */
/* THE NEXT GAME IS WHAT YOU CAME BACK FOR (s24, Brian: "next game button on
   series play after game is over"). The row used to sit under the rotation card
   and both lineup cards — a screenful of setup between finishing one game and
   starting the next. */
.camp-actions-top { margin: 0.2rem 0 1rem; }
/* The way back, above the page it leaves. */
.camp-nav { display: flex; gap: 0.5rem; margin: 0 0 0.5rem; }
.camp-nav:empty { display: none; }
.mg-ann-run { color: var(--accent); font-weight: 700; }
/* UPPERCASED IN CSS, NOT IN THE SOURCE. Written "TIRING" in the markup it is an
   all-caps token, and `CODEY` in copy_lib correctly reads those as code — `SP`,
   `RP`, `HR` — so the word disappeared out of the copy document, which is where
   Brian edits copy. The case is presentation; the word is copy. */
.mg-tiring { color: var(--danger); font-weight: 800; letter-spacing: 0.04em;
             text-transform: uppercase;
             animation: tiring-pulse 2.4s ease-in-out infinite; }
@keyframes tiring-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) { .mg-tiring { animation: none; } }

/* GOING SOLO — the fourth answer to "who's your bench coach?" (Brian, s18).
   Shares `.manage-both`'s shape because it is the same KIND of answer: an
   override of the three faces above, not a different sort of control. Pressed
   it takes the accent fill, so "no coach" reads as a choice you have made
   rather than a button you have not pressed. */
#btn-solo[aria-pressed="true"] {
  background: var(--accent);
  color: #10231a;
  border-color: var(--accent);
  font-weight: 700;
}

/* WHO IS DUE UP NEXT INNING, on the club in the field (Brian, s18). Deliberately
   quieter than `.atbat`: that row is the game right now and takes the gold
   highlight, this one is a fact about the half-inning after this one. Same
   reason the marker is hollow — ▶ is happening, ▷ is next. */
.mg-row.duenext { background: rgba(245, 197, 66, 0.045); }
.mg-row.duenext .mg-marker { color: var(--muted); }
.mg-due {
  font-size: 0.6rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

/* ---- MVP OF THE SERIES (Brian, s19) ---------------------------------------
   Under the result, above the game log: the series is decided, and this is the
   first thing a reader wants after who won. A card rather than a line, because
   it is an award — and the name carries the weight, with what he did in the
   quiet row beneath rather than in a run of middots beside it. */
.ser-mvp {
  display: flex; flex-direction: column; gap: 0.15rem;
  max-width: max-content;
  margin: 0.9rem 0 1.1rem;
  padding: 0.6rem 1rem 0.65rem;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: 10px;
}
.ser-mvp-tag {
  color: var(--accent); font-size: 0.68rem; font-weight: 800;
  letter-spacing: 0.09em; text-transform: uppercase;
}
.ser-mvp-name { color: var(--text); font-size: 1.15rem; font-weight: 800; line-height: 1.2; }
.ser-mvp-line { color: var(--muted); font-size: 0.82rem; }

/* The resume offer on a setup screen (Brian, s19). Its own row under the
   primary action rather than beside it: starting fresh and picking up where you
   left off are different intentions, and a reader scanning for "Play ball"
   should not have to disambiguate two primaries on one line. */
.camp-resume {
  display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
  margin-top: 0.9rem; padding-top: 0.9rem; border-top: 1px solid var(--line);
}
.camp-resume-line { color: var(--muted); font-size: 0.82rem; }

/* THE GAME IN PROGRESS (Brian, s19). A thin bar above every screen, because a
   managed game can be launched from a single game, a series, a campaign or a
   season, and the reader should not have to remember which door he came out of.
   Hidden unless there is one, so it costs nothing the rest of the time. */
/* THE RESUME BARS SHARE ONE RULE (s29), AND THE SELECTOR NOW CATCHES THE ONES
   NOT WRITTEN YET (s35, Brian: "something on top of the live site").
   THE SEASON BAR HAD NO BAR. s29 wrote "the series bar is the game bar's sibling
   in every respect ... styled by the same declaration rather than by a copy that
   drifts" — and then s31 added a THIRD sibling, `#season-resume`, and did not add
   it to the selector. It has therefore rendered as an UNSTYLED BLOCK from the day
   it shipped: no panel, no border, no width, no margin — two bare buttons sitting
   above the banner at the top of every screen, for anyone with a season in
   progress. It was invisible to everyone else because `[hidden]` still hid it.
   **A COMMENT SAYING "ONE RULE, NOT A COPY" DOES NOT MAKE THE NEXT SIBLING JOIN
   IT.** The list was the thing that could go stale, so the list is gone: any id
   ending `-resume` is one of these bars, which is a rule a fourth bar satisfies
   by being named like the other three. Same trick, same reason, as `restOf` being
   policed rather than trusted. */
[id$="-resume"] {
  max-width: 1100px;
  margin: 0.6rem auto 0.9rem;
  padding: 0.55rem 0.9rem;
  display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap;
  background: var(--panel); border: 1px solid var(--accent);
  border-left-width: 4px; border-radius: 10px;
}
/* `display: flex` above out-specifies the UA's `[hidden] { display: none }`, so
   the hide has to be restated at the same weight or every bar shows always. */
[id$="-resume"][hidden] { display: none; }
/* THE BAR CLOSES FROM ITS CORNER (Brian, s57: "the 'Put It Away' is confusing
   ... take that button out and just have the 'back to...' button, and put a X
   button at the top right of the banner to close the banner"). The padding on
   the right keeps a long line from running under it. */
#series-resume, #season-resume { position: relative; padding-right: 2.4rem; }
.bar-x {
  position: absolute; top: 0.3rem; right: 0.45rem;
  width: 1.7rem; height: 1.7rem; padding: 0; border: 0; border-radius: 6px;
  background: transparent; color: var(--muted);
  font-size: 1.3rem; line-height: 1; cursor: pointer;
}
.bar-x:hover, .bar-x:focus-visible { color: var(--text); background: rgba(127, 127, 127, 0.15); }
/* AND THE BANNER STOPS EATING THE BOTTOM OF IT (Brian, s36: "make the top banner
   for the return game/series a complete box not hide behind the header image").
   `#front` carries `margin-top: -1.5rem` to cancel the body padding, which is
   right when the banner is the first thing on the page and wrong the moment one
   of these bars is above it: 1.5rem of negative pull swallows the bar's 0.9rem
   bottom margin and then 0.6rem of the box itself, so the border came out open
   at the bottom. Cancelled only when a bar is actually SHOWING, so the banner
   keeps its flush top on every screen that has no bar. */
[id$="-resume"]:not([hidden]) ~ #front { margin-top: 0; }
.gr-tag {
  color: var(--accent); font-size: 0.68rem; font-weight: 800;
  letter-spacing: 0.09em; text-transform: uppercase;
}
.gr-line { color: var(--text); font-size: 0.88rem; flex: 1; min-width: 0; }
/* The club you manage, marked inside the title rather than repeated after it (s56). */
.gr-line .sr-mine { color: var(--accent-2); font-weight: 700; }
/* STILL GOING, SAID IN A WORD (Brian, s29: "list in the office as IN PROGRESS").
   The `live` flag has driven a row highlight since s25, and a color is not a
   word — the shelf mixes finished results with games waiting for you.
   UPPERCASE IN CSS, NOT IN THE MARKUP: an all-caps word is a code token to the
   copy extractor and would drop the line out of the document Brian edits copy
   in. That is the s22 `TIRING` lesson. */
.sg-live {
  color: var(--accent); font-size: 0.62rem; font-weight: 800;
  letter-spacing: 0.09em; text-transform: uppercase; margin-left: 0.5rem;
  white-space: nowrap;
}

/* CONTINUE IS ONE LINE, NOT TWO (Brian, s20). The arrow was a bare character in
   the label, so the button wrapped it onto its own row whenever the column got
   tight and read as a two-line control with a floating glyph. Inline-flex keeps
   the word and the arrow on one baseline and lets the button size to them. */
.mg-btn.mg-continue {
  display: inline-flex; align-items: center; gap: 0.4rem;
  white-space: nowrap;
}
/* The keyboard route, said quietly on the button itself — space already
   continued and there was no way to know that (David, s33). */
.mg-btn.mg-continue small { font-weight: 400; opacity: 0.72; font-size: 0.78em; }

/* THE STAT ROLLOVER'S HEADER SITS OVER ITS OWN COLUMNS (Brian, s20). `.tt-head`
   is a plain block, so "Batting / this series / 1990" ran as inline text while
   the rows beneath it are a flex pair with a right-aligned value column — the
   labels and the numbers they head did not line up. This variant borrows the
   row's own geometry, so the header and the figures under it are measured by
   the same rule rather than by two. Only the two-column headers take it; the
   plain ones ("You give up:", "What it is worth") stay blocks. */
/* UNPREFIXED, like `.tt-stat` right above — and that is the whole reason the
   first cut did nothing. These headers are drawn by `playerSheet`, which renders
   into `.stat-player` on the STAT SCREEN and not into `.mg-tooltip`, so scoping
   them to the dugout tooltip meant no rule matched and the label kept its
   `flex-grow: 0`. The rows they head are global for the same reason; a header
   and its columns cannot live under two different scopes. */
.tt-head.tt-head-cols { display: flex; align-items: baseline; gap: 0.5rem; }
/* The label takes the slack exactly as `.tt-stat b` does — the header is the
   same three columns as the rows it heads, not a lookalike with its own rules.
   (A `::before` spacer would land ahead of the label and push it right.) */
.tt-head.tt-head-cols b { flex: 1; font-weight: 700; }
.tt-head.tt-head-cols i { font-weight: 500; color: var(--muted); font-size: 0.92em; }

/* A SUGGESTION, NOT AN ANSWER (s21 — PLAN-home.md §4, step 6). One named matchup
   offered to an empty board, and gone the moment there is anything to play. It
   spans both club columns because it is about the pair, and it is deliberately
   quieter than `Play ball`: an offer should not compete with the act. */
.suggest {
  grid-column: 1 / -1;
  /* LEFT-ALIGNED (Brian, s21), with every other block on the page. Centered, it
     was the one row that started somewhere else, which reads as a banner rather
     than as an offer sitting in the column. */
  display: flex; justify-content: flex-start; align-items: center;
  /* THE AIR BEFORE THE MANAGER'S OFFICE IS `#locker`'s, NOT OURS (Brian, s26:
     "less space between new here and the office"). s21 added 1.6rem here on the
     reading that the offer and the room ran together — but the room is a bordered
     box that already holds 2.6rem of margin above itself and 1rem of padding
     inside, and `#locker > .sec-rule` zeroes the heading's own top margin
     precisely because that box supplies it. This margin was a fourth helping,
     and it does not collapse into any of them: this is a grid item inside
     `.teams`, and `#locker` is a sibling of that section.
     SO IT IS ONLY THIS ONE STATE THAT WAS WRONG. The 1.6rem applied when the
     suggestion is SHOWING, which is the empty board a first-timer meets — and
     the only screen where the gap was ever complained about. Removing it leaves
     `#locker`'s own spacing to do the job it already does when the offer is
     hidden, so both states now sit at the same distance from the room. */
  flex-wrap: wrap; gap: 0.5rem; margin: 0.4rem 0 0;
}
.suggest[hidden] { display: none; }
.suggest-lead { color: var(--muted); font-size: 0.8rem; }
.suggest-btn {
  background: var(--panel-2);
  color: var(--accent-2);
  border: 1px dashed var(--accent-2);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.4rem 1.1rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.suggest-btn:hover { background: var(--accent-2); color: #10231a; }
.suggest-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A NAMED DIVIDER (Brian, s21). The page groups into two things — the decisions
   about the game, and the clubs playing it — and this is what says so. A rule
   with a word sitting on it, rather than a heading: the rows underneath already
   carry their own questions, so a heading would be a label over labels. */
.sec-rule {
  max-width: 1100px; margin: 1.7rem auto 0.9rem;
  display: flex; align-items: center; gap: 0.7rem;
  color: var(--accent-2);
  /* LARGER (Brian, s25: "The game, Rosters, Manager's Office headers — larger
     font"). It was 0.64rem, which at this letter-spacing read as a caption
     rather than as the heading of a section. The tracking comes DOWN as the
     size goes up: 0.14em on 10px is what makes small caps legible and on 14px
     it just spreads the word out. */
  font-size: 0.92rem; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase;
}
.sec-rule[hidden] { display: none; }
.sec-rule::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--line);
}

/* THE COACH DRAWER. Its summary IS the collapsed state — the question and the
   one-line answer — so the closed row says everything a settled reader needs. */
#coach-drawer > .coach-summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.15rem 0;
}
#coach-drawer > .coach-summary::-webkit-details-marker { display: none; }
#coach-drawer > .coach-summary::before {
  content: "\25b8"; color: var(--accent-2);
  display: inline-block; margin-right: 0.15em;
}
#coach-drawer[open] > .coach-summary::before { content: "\25be"; }
#coach-drawer[open] > .coach-summary { margin-bottom: 0.2rem; }

/* THE SERIES LENGTH, as pills (Brian, s21). Same control as `.fmt-btn` directly
   above it because it is the same kind of question — a short closed set with
   exactly one true — and the two rows should not answer it in two shapes. */
.ser-btn {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.35rem 0.95rem;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.ser-btn:hover { border-color: var(--accent-2); }
.ser-btn.on {
  background: var(--accent-2); color: #10231a; border-color: var(--accent-2);
  font-weight: 700;
}
.ser-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* THE PINNED `Play ball` (s21). It appears only when the real one has scrolled
   away, so the two are never on screen together — see `updateStickyPlay`.
   FULL BLEED AND FLAT-BOTTOMED, so it reads as part of the window rather than
   as a card floating over the page, and the roster underneath keeps scrolling
   past it. */
#play-sticky {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  flex-wrap: wrap;
  padding: 0.6rem 1.25rem;
  background: rgba(14, 26, 18, 0.94);
  border-top: 1px solid var(--line);
  backdrop-filter: blur(6px);
  /* Clears the home indicator on a phone without padding the desktop. */
  padding-bottom: max(0.6rem, env(safe-area-inset-bottom));
}
#play-sticky[hidden] { display: none; }
.ps-line {
  color: var(--muted); font-size: 0.78rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60vw;
}
/* The same gold as the real button, because it IS the real button — a pinned
   copy in a different color would read as a different act. */
#btn-playball-sticky {
  background: var(--accent); color: #1a1a10; border: 1px solid var(--accent);
  font-size: 0.95rem; font-weight: 800;
  padding: 0.5rem 1.7rem; border-radius: 999px; cursor: pointer;
  transition: filter 0.15s;
}
#btn-playball-sticky:hover { filter: brightness(1.08); }
#btn-playball-sticky:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; }
@media (max-width: 620px) { .ps-line { display: none; } }

/* ---------------------------------------------------------------------------
   THE SCORESHEET (s22, Brian) — PLAN-scoresheet.md phase B.
   Batting order down the side, innings across, a diamond in every square.

   IT SCROLLS INSIDE ITS OWN BOX. Nine innings of diamonds is wider than a
   phone, and the standing rule here is that wide content scrolls in an
   `overflow-x` container while the PAGE body never scrolls sideways.
   --------------------------------------------------------------------------- */
.ss-club { margin: 1.1rem 0; }
.ss-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* THE CARD IS THE BATTING TABLE NOW (s23, Brian's sketch). A club that carries
   nine squares AND six box-score columns cannot sit beside the other one at any
   width, so the two stack and each keeps its own scroller. The team column
   stops scrolling when it does — nested scrollers on the same axis mean the
   reader can never tell which one they are dragging. */
.box-wrap-stack { display: block; }
/* EACH CLUB IS ITS OWN SECTION (Brian: "the two teams' box scores should be in
   separate sections"). Stacked, two clubs ran together as one long column of
   tables and notes with a name somewhere in the middle of it; a panel around
   each says where one ends and the other starts. */
.box-wrap-stack .box-team {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 0.7rem 0.95rem 0.85rem; margin-bottom: 1.5rem;
}
.box-wrap-stack .box-team:last-child { margin-bottom: 0; }
/* THE COLUMN STOPS SCROLLING SO THE TABLES CAN. Nested scrollers on the same
   axis mean the reader can never tell which one they are dragging, so wherever
   a table brings its own `.ss-scroll` the column gives up its `overflow-x`.
   That was the scoresheet's arrangement first; the game box uses it for EVERY
   table now, because one scroller over two tables of different widths belongs
   to the wider one and drags the narrower one past its own end (see the note in
   `renderBoxScore`). Renderers that still put a bare `.box` in a `.box-team`
   leave this class off and keep the column's scroller. */
.box-team-wide { overflow-x: visible; }
.ss-grid.box { width: auto; margin-bottom: 0.5rem; }
/* ROOM TO READ A SQUARE (s23, Brian: "widen the inning columns on the
   scoresheet, give the diamond and the scoring more space around and between
   each other"). A real card's square is close to square; these were 3.2rem by
   2.6rem with the diamond and its notation jammed together at a 2px gap, which
   is legible but cramped — and the notation is the part a reader actually
   parses. The width floor rises with them, which is what `min-width` is for:
   the grid scrolls inside `.ss-scroll` and the page still never does. */
.ss-grid { border-collapse: collapse; font-size: .78rem; min-width: 54rem; }
.ss-grid th, .ss-grid td {
  border: 1px solid var(--rule, #2b4033);
  padding: 4px 7px; text-align: center; vertical-align: top;
}
/* THE HEADER'S FILL WAS A VARIABLE NOTHING DEFINES (fixed s27, found by printing
   the card). These three rules asked for `--panel2` — NO HYPHEN — against a
   palette that defines `--panel` and `--panel-2`, so all three have always taken
   their hard-coded `#16281c` fallback and the header could not follow a theme.
   Measured live before the fix: `--panel2` resolved to nothing and the header
   painted rgb(22, 40, 28).
   IT IS `--panel`, AND THE FALLBACK IS THE EVIDENCE. `#16281c` is `--panel`
   (`#16241b`) to within 4/255 of green — a hand-typed approximation of it —
   where `--panel-2` (`#1d2f23`) is a different tone entirely. So the one-hyphen
   reading is the wrong one, and the shipped appearance moves by 1.57% on a
   single channel, which is nothing.
   IT ALSO CLOSES A SEAM. The comment below says the header takes "the header's
   own fill so the pinned column is not a different color from its row" — and it
   was: the body's pinned cell read rgb(22, 36, 27) against the header's
   rgb(22, 40, 28). One token, one color, no seam. */
.ss-grid th { background: var(--panel); font-weight: 600; }
.ss-name { text-align: left !important; white-space: nowrap; min-width: 8.5rem; }
/* THE NAME STAYS WHEN THE CARD SCROLLS, for the reason the line score's club
   column does (see `.linescore tr > *:first-child`, which this follows). A
   phone gives the sheet a 262px window onto a 1,093px grid: four columns in,
   the names are 320px off the left edge and every square on screen belongs to
   nobody. Pinned, the reader keeps WHO while dragging through the innings.
   THE NAME AND NOT THE SLOT: `#` and `Pos` are 60px more of a 262px window, and
   the number tells you nothing the name does not. The slot column slides under
   the name once the travel starts, which is what the opaque background is for —
   a sticky cell paints over what passes beneath it, and without a fill the
   squares would scroll THROUGH the names. The header cell takes the header's
   own fill so the pinned column is not a different color from its row. */
.ss-grid td.ss-name { position: sticky; left: 0; z-index: 1; background: var(--panel); }
.ss-grid th.ss-name { position: sticky; left: 0; z-index: 1; background: var(--panel); }
.ss-slot, .ss-pos { color: var(--dim, #9bb3a2); }
.ss-tot { min-width: 1.9rem; }
/* A square is at least as tall as the diamond it holds, so an empty inning does
   not collapse the row and make the grid ragged. */
/* EVERY INNING THE SAME WIDTH, and the width is the ordinary play's — not the
   longest one's. Sizing all nine columns to `6-4-3 DP` made the grid a third
   wider for the sake of a few squares, so the rare long notations set in a
   smaller face instead and the column stays where it was. */
.ss-cell { width: 4.2rem; min-width: 4.2rem; max-width: 4.2rem; height: 3.2rem; }
/* The two fitted sizes live with `.ss-code` below, beside the face they are
   scaled against — a hand set at the old .68rem is unreadable where the app's
   sans was fine, so the ratio had to move with it. Kept as one pair rather than
   two competing ones, which is the cascade collision this file has been bitten
   by before. */
/* A NOTATION NEVER BREAKS ACROSS TWO LINES. `6-4-3 DP` and `FC 4-6` are single
   marks a scorer writes in one stroke; wrapped, they read as two things in the
   square.
   AND THE SQUARE SPLITS DOWNWARDS when the order comes round twice in an inning
   (Brian: "make every scoresheet column the same size, fit for the widest
   possible scoring"). Side by side, a second plate appearance doubled the width
   of one column and no fixed width could hold it; a scorer rules the square
   across and writes the second play underneath, which is what this is. */
.ss-play { display: flex; align-items: center; gap: 6px; position: relative;
           flex-wrap: nowrap; white-space: nowrap; }
/* A LONG NOTATION TAKES THE WHOLE SQUARE (s26) — see the note beside `fit` in
   the renderer for the measurement. Beside the diamond it has 34px and needs 56;
   on its own line it has 53 and breaks at its one space, which is where the
   sequence ends and its classification begins.
   THE PARTS ARE STACKED, NOT WRAPPED. Letting them wrap put `6-4-3 DP` on one
   shrunken line whenever it happened to fit the width it was given, which is the
   opposite of the ask — Brian: "two lines should 6-4-3 line break DP". `display:
   block` on each part makes the break structural, so the mark reads the same way
   every time instead of depending on how wide the square happened to be.
   IT STAYS BESIDE THE DIAMOND, so the square is two lines and not three: the
   diamond and `6-4-3` share the first, `DP` takes the second. Stacking the code
   UNDER the diamond would give it the full 53px, but a third line grows the row —
   and every row in the table grows with it, for the sake of a handful of squares.
   THE EXTRA STEP DOWN IS WHAT BUYS THAT. Beside the diamond a part gets 34px, and
   `6-4-3` needs about 35 at `.ss-code-xs`. Measured, not guessed — see the scan.
   `white-space: normal` UNDOES `.ss-play`'s nowrap for the code only; `.ss-pt`
   puts it back on each part, so `6-4-3` can never split. */
/* A PHONE WAS SPENDING 164 OF ITS 375 PIXELS ON NESTED PADDING (s26, measured
   while playing a game at 375px). Four boxes deep — body 48, `#locker` 42,
   `#results` 42, `.box-team` 32 — the card's scrolling window came out at 211px,
   and the pinned name column is 136 of those. SEVENTY-FIVE PIXELS OF INNINGS, on
   a grid whose columns are 67 wide: the reader got one square at a time and no
   way to see a square next to the one before it.
   THE PADDING IS RIGHT FOR PROSE AND WRONG FOR A WIDE TABLE. Every one of those
   insets exists to keep TEXT off the edge of a panel, and a card that scrolls
   inside its own window does not need any of them — it needs the width. So on a
   phone the two panels give back most of theirs and the box the card sits in
   gives back all of it, which is worth 72px, and the name column comes down to
   the widest name it actually has to hold rather than to a desktop measure.
   NOT A SMALLER GRID. Shrinking the squares would make the notation unreadable to
   buy the same room; this buys it from margins that were not carrying anything. */
/* AND THE SLOT AND THE POSITION COME OFF THE CARD ON A PHONE (Brian, s27: "hide
   # and Pos on mobile"), which is the SAME 60px s25 named and then spent.
   s25 measured `#` and `Pos` at "another 60px of a 262px window" and concluded
   "the number says nothing the name does not" — and used that to decide what to
   PIN rather than what to KEEP, so both columns stayed and went on costing the
   width. Measured again at 375 before this: `#` 23 + `Lineup` 141 + `Pos` 37 =
   201 of a 262px window, leaving 61px for a grid whose columns are 67 — under
   ONE inning visible at rest on the dugout's final screen, which is the tightest
   of the three places the card is drawn.
   IT IS A COLUMN, NOT A CELL, so the rule keys on the classes the renderer puts
   on all three rows — header, body and the totals line — rather than on
   `nth-child`, which would have to be right three times and would break the
   first time a column moved.
   THE NAME BECOMES THE FIRST COLUMN, which is a second gain and not a side
   effect: `.ss-name` is `position: sticky; left: 0`, so with `#` in front of it
   the pin only engaged once the reader had scrolled past the slot. Now it holds
   the left edge from the start.
   THE BATTING ORDER IS NOT LOST WITH IT — the rows are still in order, and the
   substitute rows still carry their `↳`, so who bats where is read off the
   column the way it is on a real card. */
@media (max-width: 620px) {
  #locker, #results { padding-left: 0.6rem; padding-right: 0.6rem; }
  #results .box-team { padding-left: 0; padding-right: 0; }
  .ss-grid .ss-name { min-width: 7rem; }
  .ss-grid .ss-slot, .ss-grid .ss-pos { display: none; }
}
.ss-play.ss-long { align-items: flex-start; }
.ss-play.ss-long .ss-code { white-space: normal; line-height: 1.06; font-size: .66rem; }
.ss-play.ss-long .ss-pt { display: block; white-space: nowrap; }
.ss-play + .ss-play { margin-top: 3px; padding-top: 3px; border-top: 1px dotted var(--rule, #2b4033); }

/* THE DIAMOND IS A ROTATED SQUARE and the fill is a conic sweep, so the four
   quarters are first, second, third, home going counterclockwise the way a scorer
   draws them. A `background-image` rather than four elements: one box, and the
   quarter count is the only thing that changes. */
/* THE MAN LEFT ON IS MUTED, AND ONLY THAT (s23). Brian read a card of stranded
   runners as a club that had been scoring all game — correctly, because a man
   who reached third and a man who came home were both a solid gold diamond. The
   accounting was never wrong (7 filled diamonds, 7 on the totals line, 7 on the
   scoreboard); the card was hard to read. So the bases a man REACHED are drawn
   in a held-back amber and the full diamond — the run — is the only mark at full
   strength. Everything else about the diamond is as it was. */
.ss-dia {
  width: 13px; height: 13px; display: inline-block; transform: rotate(45deg);
  border: 1px solid var(--dim, #9bb3a2); flex: 0 0 auto;
}
/* THE QUARTERS ARE A CONIC SWEEP, which is what the comment above has claimed
   since phase B while the code drew something else. Two stacked linear
   gradients and a clip-path gave: q1 = HALF the diamond, q2 = three quarters,
   q3 = three quarters again with a different corner missing. A man on first and
   a man on second were all but indistinguishable — Brian caught it on a walk
   that pushed the runner up ("this doesn't make sense"), and the play-by-play
   said exactly what the card could not.
   One layer, ninety degrees a base, filled home -> first -> second -> third the
   way a scorer fills it: counterclockwise on screen, which is clockwise inside an
   element rotated 45deg. `from` is where each sweep STARTS so they all end at
   the same place — the home corner. */
.ss-dia.q1 { background: conic-gradient(from 45deg, var(--onbase) 0 90deg, transparent 90deg); }
.ss-dia.q2 { background: conic-gradient(from -45deg, var(--onbase) 0 180deg, transparent 180deg); }
.ss-dia.q3 { background: conic-gradient(from -135deg, var(--onbase) 0 270deg, transparent 270deg); }
/* A run: the whole diamond. This is the mark a reader scans the card for. */
.ss-dia.q4 { background: var(--accent, #d8b24a); border-color: var(--accent, #d8b24a); }
/* Retired on the bases — the diamond he did fill, struck through. */
.ss-dia.ss-cs { border-color: var(--bad, #c2543f); }

/* THE MARKS ARE IN SOMEBODY'S HAND (Brian, s25: "lets use Caveat 700, pencil
   white, marks only. totals untouched"; s26: "wire Architects Daughter").
   ONLY `.ss-code`, which is the notation itself. The grid, the names, the
   column heads and `.ss-tot` keep the app's own face — that is the whole idea:
   a card is a printed form somebody writes ON, and a hand applied to the
   furniture as well would read as a novelty font rather than as a scorer.
   THE PENCIL IS OFF THE APP'S AXIS. `#f2efe6` against the interface's
   `--text: #e8f0e9` is two points warmer and off the green — a small move that
   does the work, because the marks stop being the same ink as everything around
   them and start reading as something added to the card.
   `tabular-nums` IS DROPPED HERE ON PURPOSE: it is a handwriting face and the
   figures are not drawn on a grid, so asking for tabular spacing gets the
   fallback's idea of it rather than the hand's. Nothing in a square lines up
   in a column anyway — `.ss-tot`, which does, is untouched and keeps it.
   THE CALLED THIRD STRIKE IS DRAWN INTO THE SUBSET (s26, Brian: "draw the
   glyph"). The book's backward K is U+A4D8 and NO handwriting face on Google
   Fonts ships an outline for it — checked against Caveat's full 753 glyphs, and
   true of Architects Daughter too — so that one mark used to fall out of the hand
   and land in `Avenir Next` while everything around it stayed handwritten. It is
   a mirror of the face's OWN K, reflected about the center of that letter's ink
   so it keeps the K's exact footprint and side bearings, with its contours
   reversed because a reflection flips winding direction and TrueType fills by the
   non-zero rule — skip that and the letter comes out hollow or inside-out. The
   glyph survived the change of face because it is cut from whichever K is in the
   file, not copied between them.
   THE FALLBACK STACK STAYS ANYWAY. It is no longer load-bearing for U+A4D8, but
   it is still what catches a failed font fetch, and naming the app's own face
   second means that lands somewhere chosen rather than wherever the browser goes.
   VERIFY BY ADVANCE WIDTH, NOT BY EYE, and this bit is a trap worth keeping: a
   font of the same name INSTALLED on the machine will render happily while the
   webfont is broken or the stylesheet is cached, and `document.fonts.check()`
   returns true for a fallback so it proves nothing. Caveat caught this exact way
   — our cut drew K at 508/1000 and the system copy at 778. Measure the advance
   against the file's own `hmtx`. */
.ss-code {
  font-family: "Architects Daughter", "Avenir Next", "Segoe UI", system-ui, sans-serif;
  font-weight: 400;
  /* SMALLER THAN CAVEAT'S 1.06rem, because this face sets bigger. Its K takes
     620/1024 of the em against Caveat's 508/1000 — about 22% wider at the same
     size — so matching the number would have grown every mark on the card. The
     three steps keep Caveat's ratios (.92 and .78 of the base) so the fitted
     sizes stay in proportion to the hand rather than to the old one. */
  font-size: 1.02rem;
  color: #f2efe6;
  letter-spacing: .02em;
}
/* The two fitted sizes, scaled against the hand's base rather than the sans's. */
.ss-code-sm { font-size: .885rem; }
.ss-code-xs { font-size: .751rem; letter-spacing: 0; }
/* Under the notation — a scorer's second line in the square, and quieter than
   the play itself because the play is what the square is for. Two colors, two
   meanings: the steal is his own doing (green, like every other thing a runner
   takes), the RBI is a run driven in (gold, like the filled diamond it made). */
.ss-sb, .ss-rbi { display: block; font-size: .6rem; font-weight: 700; letter-spacing: .04em; }
.ss-sb { color: var(--accent-2, #6fd08c); }
.ss-rbi { color: var(--accent, #f5c542); }
.ss-cs-mark { display: block; font-size: .6rem; font-weight: 700; letter-spacing: .04em;
              color: var(--bad, #c2543f); }
/* The out number and the in-square batter label are both gone from the render
   (s23) — the record still carries `outNo`, and the conservation law in
   `scoresheet-scan` still depends on it; the square simply no longer prints it.
   Their rules go with them rather than sitting here matching nothing. */
/* WHERE THE HALF-INNING ENDED (s23, Brian). A scorer rules a corner into the
   square the inning stopped in, so a reader can see at a glance where each half
   finished and where the order picks up — without it nine columns of squares
   say nothing about where the third out fell. On the CELL rather than the play,
   because a square holding two plate appearances ended the inning once. */
.ss-cell.ss-endinn { position: relative; }
.ss-cell.ss-endinn::after {
  content: ""; position: absolute; right: 0; bottom: 0; width: 0; height: 0;
  border-left: 11px solid transparent;
  border-bottom: 11px solid var(--inn-end, #2c6b42);
  pointer-events: none;
}
.ss-dnb { color: var(--dim, #9bb3a2); font-weight: 600; }
.ss-sum td { background: var(--panel); font-weight: 600; }
.ss-sum td:first-child { text-align: right !important; font-weight: 400; }
/* R · H · LOB in one cell. The separators are dimmed and given no width of
   their own beyond a hair space, because three figures plus two dots have to
   sit inside the 4.2rem inning column that a square already sizes — the dot is
   what makes it read as three numbers rather than one three-digit one. */
.ss-rhl { white-space: nowrap; letter-spacing: .01em; }
.ss-rhl i { font-style: normal; color: var(--dim, #9bb3a2); padding: 0 .12em; font-weight: 400; }
.ss-sum .ss-name { letter-spacing: .02em; }
.ss-note { font-size: .72rem; color: var(--dim, #9bb3a2); margin: .4rem 0 0; max-width: 52rem; }

/* The card in the DUGOUT (s22 phase C, merged into the box score in s23) —
   full width under the three columns, with the play-by-play below it. It is a
   sibling of `.mg-cols` rather than a panel inside the center column, because
   the grid does not fit in a third of the page.

   IT HIDES ON WHAT IT CONTAINS, not on being empty: the panel owns a heading of
   its own now, so it is never empty even before the first pitch is thrown. */
.mg-sheet { margin: 1rem 0 0; }
.mg-sheet:has(#mg-box-live:empty) { display: none; }
/* THE PLAY-BY-PLAY AT THE FOOT IS NOT A COLUMN PANEL (s23, Brian: "the play by
   play on the bottom is not working during gameplay"). `.mg-pbp-panel` was
   written for the center column of `.mg-cols`, where `flex: 1 1 0` and
   `overflow: hidden` make it share the height with the boxes above it. s22 moved
   it to the bottom of `#manage` — which is ALSO a flex column, and has no spare
   height to give it — so the panel collapsed to 20px around 881px of plays.
   It only ever looked right at the end of the game, because `.mg-final` sets
   `flex: 0 1 auto` and that is the state s22 verified. At the foot it sizes to
   its contents, always, and the log scrolls inside a height of its own. */
.mg-pbp-foot { margin-top: 1rem; flex: 0 0 auto; overflow: visible; }
/* A VIEWPORT UNIT INSIDE A SCALED BOX IS SCALED TOO, so this one is divided
   back out. `vh` resolves against the real viewport and is then multiplied by
   the dugout's `zoom`, which would quietly make 45vh into 63 at the top step —
   the one rule in the LIVE dugout that measures itself against the window
   rather than against its own type. `--mg-zoom` is 1 until a step sets it, so
   this is an identity everywhere the scale is not applied. The other two `vh`
   rules need no such thing: one is `.mg-final` (never scaled) and the other is
   inside the phone media query, which is below every step. */
.mg-pbp-foot #mg-log { flex: 0 0 auto; max-height: calc(45vh / var(--mg-zoom, 1)); }
/* The Player of the Game, announced with the last out. Sits under the call and
   above the button, quieter than the call and louder than the body text. */
.mg-potg { display: block; font-size: 0.88rem; color: var(--muted); margin: 0.35rem 0 0.1rem; }
.mg-potg strong { color: var(--accent); margin-right: 0.35rem; }
/* A man who came in for somebody: indented under the slot he inherited, and
   quieter than the starter so the batting order still reads down the page. */
.ss-subrow .ss-name { color: var(--dim, #9bb3a2); padding-left: .35rem; }
.ss-subrow td { border-top-style: dotted; }

/* ===================================================================
   PRINT — "Save as PDF" on the game report (Brian, s27)
   ===================================================================
   The browser writes the PDF; this decides what is on the page. There is no
   bundled PDF library on purpose — see `saveBoxScorePdf` in main.js. The
   consequence worth knowing is that this stylesheet prints WHAT IS REALLY
   RENDERED, so the PDF cannot drift from `renderBoxScore` the way a
   hand-drawn second layout would.

/* THE PRINT STYLESHEET IS GONE with Save as PDF (Brian, s45). It existed only
   to dress the box score for that button, and `.print-hide` — half its
   mechanism — was written by JS that no longer exists. See main.js. */

/* ---- THE END-OF-SERIES PAPER (s30, Brian's mockup) ------------------------
   Masthead, date, headline, and ONE three-across band: rosette, photograph,
   name and line. Deliberately thinner than the game Chronicle — no prose, no
   byline, no crest, no columns — which is what "simplify the content of the
   paper" asked for, so none of that is missing by accident.
   THE BAND IS A GRID rather than a flex row so the three parts keep their
   proportions when the photo is absent: a man with no free photograph is the
   common case, not the exception, and the layout must not collapse around the
   hole. `minmax(0, …)` on the middle column is what stops a wide photograph
   pushing the name off the page. */
.chron-series .ser-paper-band {
  display: grid; grid-template-columns: 9rem minmax(0, 1fr) 1fr;
  gap: 1.2rem; align-items: center; margin: 1.2rem 0 0.4rem;
}
.ser-paper-rosette { width: 9rem; height: auto; display: block; }
.chron-series .ser-paper-art { margin: 0; }
/* The slot holds the photograph's place before it arrives, so the headline does
   not jump when it lands — and shows nothing at all when it never does. */
.ser-paper-slot { display: none; }
.chron-series .chron-photo { width: 100%; height: auto; display: block; }
.ser-paper-who { text-align: center; }
.ser-paper-name { font-size: 1.5rem; font-weight: 700; letter-spacing: 0.01em; }
.ser-paper-line { font-size: 1.02rem; line-height: 1.5; margin-top: 0.35rem; }
.chron-series .chron-date { text-align: center; }
/* ON A PHONE the three-across band becomes one column: at 375px each part would
   be ~110px wide, which is a rosette the size of a postage stamp beside a
   thumbnail. Stacking is the only honest answer, and it is the same call the
   box score's own mobile rules make. */
@media (max-width: 620px) {
  .chron-series .ser-paper-band { grid-template-columns: 1fr; justify-items: center; }
  .ser-paper-rosette { width: 7rem; }
}
/* The club's own rollover, on the roster header in the builder and the dugout.
   Same `help` cursor as every other hover-only target (s30). */
.team-real[data-tip], .mg-club[data-tip] { cursor: help; }
/* AND THE BETWEEN-GAMES CARDS CARRY THE REPORT TOO (Brian, s35: "need scouting
   reports on the rotation and lineup screen of a series"). The tip hangs off the
   STAT LINE rather than off the name, because on these two cards the name is
   inside a `<select>` or a radio `<label>` — a control, and s33 recorded that a
   tip anchored to a control fires on the way to pressing it. The stat line is
   the thing a reader is already looking at to compare two men.
   NOTHING ELSE WAS NEEDED: the rollover's root has been the DOCUMENT since s27,
   so a `data-tip` on a new screen works by existing, and the glove chip nested
   inside keeps its own smaller box (`closest` takes the innermost). */
.lc-line[data-tip], .rc-line[data-tip] { cursor: help; }

/* SEND HIM? — the beat between the ball landing and the runner committing
   (Brian, s33: "ask before stretching a double", close calls only). It reuses
   the action panel's own vocabulary rather than inventing a look: this is one
   more call the manager makes, not a modal interrupting the game.
   IT LIVES IN THE SITUATION BANNER NOW (Brian, s35), which is why the row rules
   below are gone: the banner is a centered flex line, so the question and its two
   buttons stack under the play rather than sitting in a bordered card of their
   own. The banner turns from a row into a column for the duration — a decision
   is taller than an announcement — and goes back on the next render.
   THE ODDS SIT WITH THE QUESTION, not on the button: the button says what to DO
   and the number is a fact about the runner. Same split the contact play had to
   be taught, kept through the move. */
.mg-announce-ask { flex-direction: column; gap: 0.45rem; }
/* A COLUMN MUST NOT WRAP (s57). The coach banner carries the play above its
   buttons, and a play that ends the half brings `.mg-ann-over`, whose rule
   turns wrapping on — in a column that wraps the buttons into a second column
   off the right edge of the page. Two classes, so it outranks that rule. */
.mg-announce.mg-announce-ask { flex-wrap: nowrap; }
.mg-announce.mg-announce-ask .mg-ann-over { flex: none; }
.mg-ann-send { display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 0.55rem; }
.mg-ann-q { font-size: 0.9rem; font-weight: 600; color: var(--text); }
/* JUST BOLD (Brian, s36: "'45% chance he makes it' just bold"). The odds were
   set in the accent gold, which is the ink this app uses for a RUN — the
   scoreboard, the run clause in the banner, the save badge. On a question that
   has not been answered yet it read as though something had already happened. */
/* 700, NOT 800 (Brian, s36: "not black or extra bold, just bold"). The note
   above got the COLOR right and left the weight at 800 against a parent already
   at 600 — two steps up from the sentence around it, which reads as a different
   ink rather than as emphasis. One step is what emphasis is. */
.mg-ann-q b { font-weight: 700; }
/* The aside under an argument or challenge prompt (s57): challenges left, or
   "replay already upheld it". Quieter than the question it qualifies. */
.mg-ann-sub { font-weight: 500; font-size: 0.82rem; color: var(--muted); }
/* The umpire text box (s57, slice 4): one line under the argument's question. */
.mg-ump-talk { display: flex; gap: 0.5rem; align-items: center; margin: 0.5rem auto 0; width: 100%; max-width: 34rem; }
.mg-ump-talk input { flex: 1 1 16rem; min-width: 0; padding: 0.45rem 0.6rem; border-radius: 8px;
  border: 1px solid var(--accent); background: var(--panel); color: var(--text); font: inherit; }
.mg-ump-wait { color: var(--muted); }
.mg-ann-send .mg-btns { display: flex; gap: 0.4rem; }


/* THE TESTERS' BUG FORM (report.html, s51). One required box, two rows of
   pills, a screenshot well that takes a paste, and a name. It lives inside
   `.ft-prose` so it sits on the same measure as the other two pages.
   THE BUTTON IS `.doc-btn` GREEN, not gold — sending a report is not starting a
   game, and gold stays `Play Ball!`'s alone. */
.bug-form { display: flex; flex-direction: column; gap: 0.35rem; max-width: 46rem; }
.bug-q { display: block; margin: 1rem 0 0.35rem; font-weight: 700; color: var(--text); font-size: 0.95rem; }
.bug-form > .bug-q:first-of-type { margin-top: 0; }
.bug-opt { font-weight: 400; color: var(--muted); font-size: 0.8rem; margin-left: 0.35rem; }
.bug-form textarea, .bug-form input[type="text"] {
  width: 100%; box-sizing: border-box; font: inherit; color: var(--text);
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
  padding: 0.6rem 0.75rem;
}
.bug-form textarea { resize: vertical; min-height: 7rem; }
.bug-form textarea:focus, .bug-form input[type="text"]:focus {
  outline: none; border-color: var(--accent-2);
}
.bug-pills { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.45rem; }
.bug-pills legend { padding: 0; }
.bug-pills label { cursor: pointer; }
.bug-pills input { position: absolute; opacity: 0; pointer-events: none; }
.bug-pills span {
  display: inline-flex; align-items: center; min-height: 2.75rem; box-sizing: border-box;
  padding: 0 0.95rem; border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel); color: var(--muted); font-size: 0.88rem;
}
.bug-pills label:hover span { border-color: var(--accent-2); }
.bug-pills input:checked + span { background: var(--accent-2); border-color: var(--accent-2); color: var(--bg); font-weight: 700; }
.bug-pills input:focus-visible + span { outline: 2px solid var(--accent-2); outline-offset: 2px; }
.bug-drop {
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: 6rem; padding: 0.8rem; cursor: pointer; color: var(--muted); font-size: 0.88rem;
  border: 1px dashed var(--line); border-radius: 8px; background: var(--panel);
}
.bug-drop.over, .bug-drop:hover { border-color: var(--accent-2); color: var(--text); }
.bug-drop input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.bug-drop img { max-width: 100%; max-height: 16rem; border-radius: 4px; }
.bug-trap { position: absolute; left: -9999px; }
.bug-game {
  margin: 0 0 0.6rem; padding: 0.55rem 0.8rem; border-left: 3px solid var(--accent-2);
  background: var(--panel); border-radius: 0 6px 6px 0; color: var(--muted); font-size: 0.88rem;
}
.bug-status { color: var(--muted); font-size: 0.88rem; }
.bug-form button.doc-btn { border: 0; font: inherit; font-weight: 700; cursor: pointer; }
.bug-form button.doc-btn:disabled { opacity: 0.6; cursor: wait; }
.bug-done button.doc-btn { border: 0; font: inherit; font-weight: 700; cursor: pointer; }
