/* app/m/mobile.css — the mobile frontend's stylesheet (spec §4). The ONLY sheet
   the mobile document loads besides /shared/design-system.css: `./legacy.css`
   (the day-0 fork, retired area by area — see docs/mobile/audits/LEGACY-MAP.md)
   was deleted by Task 9, so nothing loads between the shared system and this
   file any more. Sections, in order:
   1 tokens · 2 resets · 3 type/colour/layout utilities (3b the accessibility
   floor) · 4 controls · 5 sheet · 6 shell (grid) · 7 auth/pick · 8 title ·
   9a hub screens (locker, free agents) · 9b hub screens (market, leaderboard,
   front office, Sunday Live) · 10 match · 11 final · 12 phone landscape ·
   13 tablet portrait · 14 tablet landscape.
   Rules here never use body.mob / body.port (both classes are gone from the
   document); orientation and size come from the three breakpoints of spec A12
   plus one `prefers-reduced-motion` query. */

/* ==== 1 tokens (spec §4.1, A1–A4, A12) — on body, never on :root (the shared contract) ==== */
body{
  --t-label:12px; --t-body:14px; --t-ui:16px; --t-head:20px; --t-title:26px; --t-num:34px;
  --t-led:28px; --t-led-lg:44px; --t-hero:clamp(32px, 11vw, 48px);
  --lh-tight:1.1; --lh-ui:1.25; --lh-body:1.45; --ls-label:.10em;
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px;
  --gut:16px; --col:calc(100% - 2*var(--gut));
  --hdr:56px; --nav:56px; --tick:28px; --rail:0px;
  --sat:env(safe-area-inset-top, 0px); --sab:env(safe-area-inset-bottom, 0px);
  --sal:env(safe-area-inset-left, 0px); --sar:env(safe-area-inset-right, 0px);
  --ctl-pri:56px; --ctl-std:48px; --ctl-sm:44px; --ctl-icon:44px; --ctl-input:48px;
  --danger:#FF8A80;
  /* The Kaiten mini-plate (continuous-flow spec Q9, section 9b): a tile
     `--mp-w` × `--mp-th` on a horizontal belt, a row `--mp-h` tall on a
     vertical one. game.js measures the rendered plate, never these numbers. */
  --mp-w:132px; --mp-th:192px; --mp-h:76px;  /* --mp-h / --mp-w are ceilings: section 9b fits them to the belt's pitch. 132, not the day-0 88: the carrier is a CARD now and a full-width deck has the pitch for one (section 9b). The ceiling is inert wherever the pitch is tighter — phone landscape still resolves 78. */
  --mp-wmin:64px;                            /* …and the tile's floor: 12 px text and a 44 px target still fit */
  /* The gutter between two cards on a horizontal deck. It is not a `gap`
     property and cannot be: game.js#beltFrame places every plate from server
     time, so two cards are always (stage + card)/n apart and the GAP is
     whatever that pitch has left once the card is sized — a narrower card IS a
     wider gutter. This token is the gap the card's width solves for. 4 px read
     as a wall of touching dominoes; 12 costs 9 px of card at 1280 and buys the
     deck its rhythm. Phone landscape keeps 4 (section 12): there the pitch is
     already tight and 12 would take the card to 69 px, under the 78 the name
     column needs (docs/FOLLOWUPS.md, the guillotined-surname line). */
  --mp-gap:12px;
}
/* ==== 2 resets ==== */
button{font-size:inherit}              /* MS-06: 20 class-less buttons rendered at the UA 13.333px */
input,select,textarea{font:inherit}
/* The UA's `[hidden]{display:none}` is an ORIGIN below every author rule, so
   any class that sets `display` (`.btn` inline-flex, `.stack` flex, `.pnl`…)
   silently shows a `hidden` element. Settings' delete-confirm box is `.stack`
   and starts hidden; the arm button is a `.btn` and hides itself. Without
   this line neither works, with no error anywhere. */
[hidden]{display:none !important}

/* ==== 3 utilities ==== */
.t-label{font:700 var(--t-label)/var(--lh-tight) var(--ui);letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--dim)}
.t-body{font:600 var(--t-body)/var(--lh-body) var(--ui)}
.t-ui{font:600 var(--t-ui)/var(--lh-ui) var(--ui)}
.t-head{font:var(--t-head)/var(--lh-tight) var(--pix);letter-spacing:.01em;text-transform:uppercase}
.t-title{font:var(--t-title)/var(--lh-tight) var(--pix);letter-spacing:.01em;text-transform:uppercase}
.t-num{font:var(--t-num)/1 var(--pix)}
.t-led{font:600 var(--t-led)/.85 var(--led)} .t-led-lg{font:600 var(--t-led-lg)/.85 var(--led)}
.t-hero{font-size:var(--t-hero)}
.t-wide{letter-spacing:.3em}
.c-gold{color:var(--gold)} .c-dim{color:var(--dim)} .c-cream{color:var(--cream)} .c-danger{color:var(--danger)}
.c-green{color:var(--green)} .c-blue{color:var(--blue)} .c-warn{color:#FFB300}
.on-gold{color:#1E1400} .on-green{color:#062012} .on-tier{color:#101512}
.tag.ok{background:var(--green);color:#062012} .tag.bad{background:var(--red);color:#fff}
.row{display:flex;align-items:center;gap:var(--s2)} .row-wrap{flex-wrap:wrap}
.stack{display:flex;flex-direction:column;gap:var(--s2)}
.gap-1{gap:var(--s1)} .gap-2{gap:var(--s2)} .gap-3{gap:var(--s3)} .gap-4{gap:var(--s4)}
.mt-1{margin-top:var(--s1)} .mt-2{margin-top:var(--s2)} .mt-3{margin-top:var(--s3)}
.grow{flex:1} .grow-min{flex:1;min-width:0} .no-shrink{flex:none}
.pane{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2)}
.center{text-align:center} .right{text-align:right} .left{text-align:left}
.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.reserve-line{min-height:var(--t-ui)}
/* FIXED numeric tracks, not `auto auto`: with `auto` each row sized its own
   PWR/CAP columns from its own digit count, so the PWR column started at a
   different x on every row of the lineup (measured on the locker: 4 distinct
   x positions across 24 rows). 44 px is the small-control tier and holds a
   three-digit number over its label — a FOUR-digit one ("+1,050") ellipsises to
   "+10…", which reads as a plausible number. That is why the final screen's
   drive rows (since replaced by the quarter rows, which declare their own
   tracks in section 11) stopped using these two tracks once four-digit drive
   finals became the normal case. The locker's lineup rows keep 44 / 44:
   their numbers are PWR and CAP, and no finding reports one past three digits.
   If one ever does, widening THIS rule moves every locker row, so it needs its
   own six-profile `gate-mobile.js --strict` run and a measured test. */
.lrow-grid{display:grid;grid-template-columns:34px minmax(0,1fr) 44px 44px;align-items:center;gap:2px var(--s1)}
/* The button reset the lineup rows need — `all:unset` also zeroes box-shadow,
   which would kill the app-wide `:focus-visible` ring; this resets only the
   UA button defaults and leaves box-shadow/outline alone (accessibility
   review finding, carried over from the inline `ROW_RESET` string). */
.row-reset{border:0;margin:0;font:inherit;color:inherit;text-align:left;-webkit-appearance:none;appearance:none;cursor:pointer}
.menu-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s2)}
/* state (A16) — the look is defined where each component is styled; these are the hooks */
.is-locked{opacity:.55;pointer-events:none} .is-taken{opacity:.6} .is-marked{text-decoration:line-through}

/* ---- 3b the accessibility floor (spec §3.9 / §4.1: 12 px labels, 14 px prose)
   This block is legacy.css's `a11y-floor` area, re-authored on the scale before
   that file was deleted (Task 9). It was 19 rules inside a
   `@media (orientation: portrait)` block scoped on `body.port` / `body.mob.port`,
   so every floor it enforced applied on a portrait phone and NOWHERE else — a
   landscape phone and both tablets read the /shared desktop sizes. Everything
   below is unscoped, which is the whole point: the floor is the same at all four
   breakpoints now.
   The `!important`s did NOT come across. Each one existed to beat an INLINE
   `font-size`/`min-height` on a `.lbl`/`.tag` emitted by screens.js, and
   `tools/portrait-audit/inline-lint.mjs` is at 0 as of this task — there is no
   inline size left in app/m for a floor to lose to. Rules whose only job was to
   restate a value /shared already carries (`.lbl` 12 px, `.tag` 12 px,
   `.lrow` 14 px, `input[type]` 44 px — section 4's own 48 px tier supersedes it)
   are simply gone; what is below is what the area still uniquely provided. */
.hint{font-size:var(--t-body);line-height:var(--lh-body)}
.lbl{letter-spacing:var(--ls-label)}                        /* /shared says .12em; the label role on this scale is .10em */
.chip{font-size:var(--t-label)}                             /* /shared: 11px */
/* A11Y T3 (locker-04, office-live-06): the position code is painted from
   `D.POSC` (app/shared/data.js), and /shared writes it in `#fff`. Measured
   against the real fills, every one of the five fails AA at 12 px — OL #D9B36C
   1.98, QB #FF8A3C 2.35, RB #3DBE6C 2.39, WR #2F8BFF 3.36, TE #B36BE8 3.40 —
   so the code that identifies the row is its least legible cell. Dark ink on
   the same fills reads 9.48 / 7.99 / 7.84 / 5.59 / 5.52: the position colour
   is untouched, only the ink flips. One class marks "this glyph sits on a
   position hue", carried by hand at each site rather than inferred, because
   POSC also paints things whose ink must NOT flip: the old scout-reveal chip
   (#5D6F8C, 5.10 — gone with Scouting Reports, 2026-09-24) and the playbook's
   low-severity threat chip (#5F7268, 5.13) kept their white, which ink would
   take DOWN to 3.68 and 3.65.
   SEVEN sites carry it, in two shapes. FIVE are text-bearing CHIPS — the
   locker list row, the lineup row (`.tag.lrow-pos`), the playbook recipe, the
   Sunday Live row, the FA suggestion — where the fill is the element's own
   background and `.chip`/`.tag`'s plain `#fff` is beaten at (0,2,0).
   TWO are STRIPES, where the code is absolutely positioned OVER a sibling
   band: `.c-pos` on `.c-stripe` (cards.js#cardHTML) and `.mini .mp` on
   `.mini .ms` (screens.js#mini). Those two were MISSED in the first pass, and
   the miss was visible rather than theoretical — on `51-match-hand-iphone` the
   eight hand cards wrote OL in white on the same gold the chips beside them
   wrote in ink, 400 px apart in one frame. The contrast probe cannot see them
   either: it walks up from an absolutely-positioned node to the dark `.face`,
   not to the stripe the glyph actually sits on.
   They need `!important` because /shared declares `color:#fff!important` on
   both (design-system.css:121 and :174) — that `!important` is the DESKTOP
   renderer's too and must not be touched, so the mobile copy out-specifies it
   instead: `.mini .mp.pos-fill` is (0,3,0) against /shared's (0,2,0) and
   `.c-pos.pos-fill` (0,2,0) against (0,1,0), so these win on specificity, not
   merely on `mobile.css` being the later link (index.html:13-14).
   The black `text-shadow` goes with the white it was propping up — under dark
   ink it only muddies a 12 px pixel-font letterform. */
.chip.pos-fill,.tag.pos-fill{color:var(--ink)}
.mini .mp.pos-fill,.c-pos.pos-fill{color:var(--ink)!important;text-shadow:none}
.stepstrip{font-size:var(--t-body)}                          /* the how-to scoring chain's arrows (`.arr`) inherit it — /shared: 13px */
.c-photo .ph{font-size:var(--t-label)}                       /* the card face's PHOTO placeholder — /shared floors it at 11 */
/* The thumbnail anatomy: /shared sizes each part from `--mw` with an 11 px
   floor. The proportions are kept verbatim; only the floor moves 11 → 12. */
.mini .mp{font-size:max(var(--t-label),calc(var(--mw)*.15))}
.mini .mn{font-size:max(var(--t-label),calc(var(--mw)*.16))}
.mini .mm{font-size:max(var(--t-label),calc(var(--mw)*.1))}
.mini .mst span{font-size:max(var(--t-label),calc(var(--mw)*.13))}
.mini .mst small{font-size:max(var(--t-label),calc(var(--mw)*.07))}
/* match-core-09 — the two stat cells, on BOTH card anatomies.
   /shared paints them as a blue pill (#3FB1FF→#0F6FC7) and a green one
   (#5FE88A→1B8F3A) sitting 60 px under a stripe painted from `D.POSC`, where
   WR is #2F8BFF and RB #3DBE6C. So on one card face blue meant both "wide
   receiver" and "power", and green both "running back" and "cap": a WR card
   carried a blue stripe over a blue PWR chip, an RB card a green stripe over a
   green CAP chip (crops/card-zoom.png — TAYLOR green-on-green, WILSON
   blue-on-blue; 51-match-hand-iphone puts the "■ PWR ■ CAP" legend in those
   same two hues three rows above a grid of blue-striped WRs and green-striped
   RBs). Reading position off eight cards at a glance is the hand's one visual
   task, and the same two hues were answering a different question about the
   same card. Worse, the chip hues are CONSTANT — every card's PWR chip is
   blue and every card's CAP chip is green whatever the position — so they
   carry no information at all while costing the one code that does.
   The position code is the identity and keeps its hue. The stats move OUT of
   colour rather than into a sixth hue the palette has no token for (QB orange,
   RB green, WR blue, TE purple, OL gold already spend five, and red is the
   threat code). They are one neutral steel pair now, told apart by TONE — a
   raised light plate for PWR, a recessed dark one for CAP — which is a form
   dimension this product already speaks everywhere (every button and panel is
   an inset highlight over an inset shadow). The step is large — PWR's stops
   are Y .1718/.0595 against CAP's .0380/.0103, ~4.5x apart at the top stop
   and ~5.8x at the bottom — so it survives the ~17 px band a `.mini` gives
   it at `--dcw:84`; neither plate is saturated, so neither can be read as a
   position; order is the second cue (PWR always left, CAP always right) and
   the labelled legend is the third — `#pwrCapLegend` on the match screen and
   the draft board's own `.sw-legend`, both wearing these same two plates.
   This also normalises the POLARITY flip the audit called out: CAP was
   near-black #062012 on a bright fill while PWR was white on a dark one, so
   the constraint the player must respect read weaker than the number that
   flatters them. Both are white on dark now. The figures below are computed
   per WCAG on the GRADIENT STOPS, which is where a plate can actually fail —
   the 7.1/16.9 this comment used to claim were roughly gradient MIDPOINTS and
   do not reproduce at either end:
     PWR  #5B7597 -> #31456A   white 4.73:1  and  9.59:1
     CAP  #26384F -> #0F1A2E   white 11.93:1 and 17.40:1
   4.73 IS THE NUMBER THIS BLOCK GUARDS. The PWR plate's TOP stop clears the
   4.5:1 that the 11-13 px `.mini`/`.c-stats` label sizes require by 0.23, and
   nothing else in the build re-checks it — so lightening #5B7597, whether to
   lift the plate off the card ground or to warm it, drops the whole product's
   PWR figures below AA silently. Re-measure the top stop before touching it.
   CAP'S SIDE EDGES ARE THE FILL, NOT THE RING. `box-shadow:inset 0 0 0 1px`
   is painted on the rectangular border box and only then clipped by the
   parallelogram, so the ring's VERTICALS are cut away: measured on pixels, a
   horizontal scan at mid-height stepped from the card body rgb(13,22,40)
   straight to the old CAP fill rgb(17,29,50) with no light pixel between —
   1.06:1, no left or right boundary at all, the plate closed only by two
   horizontal hairlines that are themselves 1.26:1 and at the edge of existing
   at dpr 1 (which `app/m/` also serves on a desktop browser). So the fill is
   lifted to #26384F -> #0F1A2E, which puts the boundary in the FILL, where no
   clip-path can eat it. Re-measured on the same scan of
   en/portrait-shots/51-match-hand-iphone.png: ground rgb(12,22,40) Y .0081
   stepping to fill rgb(25,40,61) Y .0206 — 1.22:1, a 2.56x luminance step,
   against 1.09:1 / 1.62x before. Still ~5x below PWR's, so recessed stays
   recessed, tone order is unchanged and no new hue enters.
   The ring is KEPT rather than replaced — it is not eaten on
   the horizontals, and the bottom stop is 1.05:1 against the ground on its
   own, so dropping it would trade two missing edges for one.
   Written HERE and not in /shared on purpose: `app/index.html` links
   design-system.css too, and the same collision on the desktop document is the
   owner's call to make with the desktop gate in front of them, not a side
   effect of a mobile audit. mobile.css is linked second, so these win on order
   at equal specificity — the same mechanism the fork already uses above. */
.c-pwr{background:linear-gradient(180deg,#5B7597,#31456A)}
.c-cap{background:linear-gradient(180deg,#26384F,#0F1A2E);color:#fff;text-shadow:0 1px 0 rgba(0,0,0,.4);box-shadow:inset 0 0 0 1px rgba(255,255,255,.22)}
.mini .mst .mw{background:linear-gradient(180deg,#5B7597,#31456A)}
.mini .mst .mc{background:linear-gradient(180deg,#26384F,#0F1A2E);color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.22)}
/* A11Y T3 locker-03: the portrait fork's own disabled wash USED to live here —
   `opacity:.6;filter:saturate(.15) brightness(.8)`, same (0,2,0) as /shared's
   rule in the sheet linked first, so on mobile it won on order and /shared's
   never applied at all. Measured off the captures it read SAVE 2.68:1 and
   PLAY/DISCARD 2.50:1 — a softer wash than /shared's, and still a wash. It is
   deleted rather than re-tuned so the one disabled SKIN in design-system.css
   lands on both documents; the owner has accepted the ~20 disabled buttons on
   the desktop document changing appearance with it. Nothing replaces it here:
   a second opinion in this file is how the first one became a dead letter. */

/* ==== 4 controls (A3) — override the shared .btn sizes; keep its clip-path, gradient, colours ==== */
.btn{min-height:var(--ctl-std);padding:0 var(--s4);font-size:var(--t-ui);line-height:var(--lh-ui)}
.btn.pri{min-height:var(--ctl-pri);padding:0 var(--s5);font-size:var(--t-head)}
/* `min-width` as well as `min-height`, and `flex-shrink:0`: a bare "✕" close
   button (mode picker, bid sheet, scouting) cleared the height floor already but
   measured 30.5 px wide, and an EXPLICIT min-width replaces a flex item's default
   `min-width:auto` — which is what stops nowrap button text shrinking below its
   own content width (Flexbox §7.1.4). Without `flex-shrink:0` a long sheet title
   beside a "CLOSE ✕" rendered it as "LOSE" at a forced 44.5 px. Both came from
   legacy.css's `a11y-floor` area, where they were portrait-only `!important`s;
   they are the tier's own metrics here, at every breakpoint. */
.btn.sm{min-height:var(--ctl-sm);min-width:var(--ctl-sm);flex-shrink:0;padding:0 var(--s3);font-size:13px}
.btn.icon{min-height:var(--ctl-icon);min-width:var(--ctl-icon);width:var(--ctl-icon);padding:0;font-size:18px;display:inline-flex;align-items:center;justify-content:center;flex:none}
.btn.wide{width:100%}
.tab{min-height:var(--nav);padding:var(--s1) var(--s1);font:var(--t-label)/1 var(--pix);letter-spacing:.06em;text-transform:uppercase;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-width:0}
.tab[aria-current="page"]{box-shadow:inset 0 0 0 2px var(--gold)}
/* The SKIN as well as the size: nothing in /shared styles a text field beyond
   its placeholder and its focus ring, so every input in app/m carried the same
   nine inline declarations. Screens that still set their own colours inline
   (the bid amount) keep winning on inline specificity.
   `height` as well as `min-height`: legacy.css's a11y-floor rule
   `body.port input[type]{min-height:44px}` (0,2,2) used to out-specify this one
   and pin a 16/13-padded field at its 46 px content height. That area is gone
   (Task 9) and this is the only tier left, but `height` stays — it is what makes
   the 48 a real number rather than a floor the padding can grow past. */
input[type],input:not([type]){height:var(--ctl-input);min-height:var(--ctl-input);padding:13px var(--s3);font:600 var(--t-ui)/var(--lh-ui) var(--ui);box-sizing:border-box;width:100%;color:var(--cream);background:#050B16;border:none;outline:none;box-shadow:inset 0 0 0 2px rgba(255,255,255,.1);clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%)}
/* THE PANEL EDGE (finish review item 7). The 4 px coloured left rail is gone from every panel:
   a `.pnl` is framed the match mock's way — a full 1 px hairline all round, cut by the chamfer
   at its top-right corner (the notch), `/shared`'s clip-path doing the cut. The hairline's
   colour is `--edge`: a quiet white by default, and the MEANING colour where the old rail
   carried one (the boss red, a Front Office tier, a coloured HOW TO PLAY step, a selected team,
   the mode sheet's RANKED/PRACTICE cards) — screens.js writes `--edge`, never a border.
   Padding: the rail + 12 used to put the content 16 px in; the hairline + 15 does the same, and
   the other three sides lose the hairline's pixel too. `.pnl.boss` is re-declared here because
   `/shared`'s `.pnl.boss{border-left-color}` (0,2,0) would out-rank a bare `.pnl` rule. */
.pnl,.pnl.boss{--edge:rgba(255,255,255,.12);border:1px solid var(--edge);padding:calc(var(--s3) - 1px) calc(var(--s4) - 1px)}
.pnl.boss{--edge:rgba(255,59,59,.55)}
/* A panel inside a panel is a subdivision: a quieter edge. `:where()` keeps this at (0,1,0),
   so every component that names its own `--edge` further down wins on order. */
.pnl :where(.pnl){--edge:rgba(255,255,255,.08)}
/* `.tag.tap` is the SHARED tap-target tier for a chip that is really a
   button: the match HUD's COACHES/AUDIBLES chips and the locker / deck-view
   position filters. /shared floors it at 40 px and gives it no width floor at
   all, so the 2-letter filters (QB/RB/TE/OL) measured 42-44 wide. Moved here
   out of legacy.css's portrait-only block (area `match`, day-0 L741) so the
   floor holds in landscape and on a tablet too; each family's SKIN stays with
   its own component (`.mchip` section 10, `#deckView .tag.tap` section 9a). */
.tag.tap{min-height:var(--ctl-sm);min-width:var(--ctl-sm);justify-content:center}
/* `<a class="btn">` is a real control on the Settings sheet (PRIVACY,
   SUPPORT — they open a document, so they must be links, not buttons);
   /shared only ever styled `.btn` for <button>, which has no underline. */
a.btn{text-decoration:none}
/* THE keyboard focus ring (A20 "pointer parity"). NOT in the A20 breakpoint:
   a ring is a KEYBOARD affordance, not a pointer one — a tablet with a
   Bluetooth keyboard needs it as much as a desktop does — and `:focus-visible`
   already keeps it off a tap (a pointer-driven focus never matches it).
   /shared ships its own ring as a two-layer `box-shadow` and kills the outline
   with `:is(button,[data-act],a,input,.tag.tap):focus-visible` — specificity
   (0,3,0). An unscoped restatement here would lose to it whatever the source
   order, so the two containers every control in this document lives in carry
   the rule instead: `#app` and `#overlay` take it to (1,2,0). The shadow ring
   is zeroed in the same breath so a focused control shows ONE ring, not a gold
   outline wrapped around a gold halo; /shared already drops a `.btn`'s inset
   bevel while focused, so nothing is lost that it kept. `.card` is a div today
   (the hand's cards are dragged, not tabbed) — it is in the list so a future
   `tabindex` inherits the ring rather than needing its own rule. */
#app :is(button,[role=button],input,.tab,.card):focus-visible,
#overlay :is(button,[role=button],input,.tab,.card):focus-visible{outline:2px solid var(--gold);outline-offset:2px;box-shadow:none}

/* ==== 5 sheet — one chrome for every overlay (COVERAGE.md §2, spec §4.7/A18) ====
   Before this, four widths, seven header sizes, five close-control shapes and
   three gaps across 17 renderers, all of it inline on each modal. The chrome
   is here and ONLY here now; a renderer contributes markup, never geometry. */
#overlay{padding:0;align-items:flex-end;justify-content:center}
#overlay .modal{width:100%;max-width:100%;max-height:min(86dvh, calc(100dvh - 56px));padding:var(--s4) var(--s4) calc(var(--s4) + var(--sab));gap:var(--s3);display:flex;flex-direction:column;box-sizing:border-box;overflow-y:auto;overscroll-behavior:contain;clip-path:polygon(0 16px,16px 0,100% 0,100% 100%,0 100%);animation:sheetIn .26s cubic-bezier(.2,1.2,.3,1) both}
#overlay .modal::before{content:"";flex:0 0 4px;width:44px;margin:0 auto var(--s1);background:var(--steel);border-radius:0}
.sheet-hd{display:flex;align-items:center;justify-content:space-between;gap:var(--s2);min-height:48px;flex:none}
.sheet-title{margin:0;font:var(--t-title)/var(--lh-tight) var(--pix);letter-spacing:.01em;text-transform:uppercase;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* `.wrap` for the three sheets whose title IS the thing the sheet exists to
   show — a player's name (card info, a bid, a boss). Ellipsising "Christian
   McCaffrey" out of the sheet you opened to read it defeats the sheet. The
   header is `min-height:48`, not `height`, so a two-line 26 px title simply
   grows it; `overflow-wrap:break-word` covers the one unbreakable surname the
   league eventually invents. Fixed titles (SETTINGS, PLAYBOOK, KICKOFF…) keep
   the one-line ellipsis — they are labels, and their length is ours. */
.sheet-title.wrap{white-space:normal;overflow:visible;text-overflow:clip;overflow-wrap:break-word}
#overlay .modal .scroll{overflow-y:auto;min-height:0;max-height:min(52dvh, 480px);-webkit-overflow-scrolling:touch}
/* Both came over from the `sheet-modal` area of legacy.css (LEGACY-MAP §4 T7 —
   the keyframes, the rule that reads it and the reduced-motion kill travel
   together). Not a breakpoint @media, so it does not count against A12's three. */
@keyframes sheetIn{from{transform:translateY(100%)}to{transform:translateY(0)}}
/* THE document's one reduced-motion query (A12 sanctions it; it is not a
   breakpoint). It now carries the ticker marquee too — that kill was the last
   live rule in legacy.css's `keyframes-misc` area, and it had to move here
   before the file could go (Task 9). `#ticker .tk-mq` (1,1,0), not `.tk-mq`:
   this query sits ABOVE section 6's `.tk-mq{animation:marquee …}` (0,1,0) in
   source order, so an equal-specificity kill here would lose to it and leave the
   marquee running for a reduced-motion reader. The id level makes the kill
   order-independent. `.kaiten .banner-ad .mq`, the legacy rule's other target,
   is not re-authored: no banner ad exists in the mobile document.
   The staggered Sunday Live row entrance, the market's live-wire line and the
   LIVE badge's pulse joined the sheet here in Task 7 — legacy's own block never
   covered them, so `popIn`/`blink` rode straight past it.
   T3 (match-core-22, onboarding-13) adds the two that were still running
   forever with reduce on, and repairs three of the five that were already
   listed. `.pips i.cur` is the current-drive pip in the match HUD and the
   draft's round pip — `blink 1.4s infinite` from design-system.css:68, on
   screen for a whole five-minute session; plain (0,2,1) is enough for it,
   because that sheet is linked BEFORE this one and equal specificity then
   wins on order. Everything else in this list is declared LATER in this same
   file, where equal specificity LOSES — measured, not reasoned: with reduce
   on, `.sun-status.is-live` (section 12) still returned `animationName:
   blink`, and `.sun-row` and `.mk-feed span` still returned `popIn`. They had
   been dead letters here since the day they were written, for exactly the
   reason the `#ticker .tk-mq` paragraph above gives. Each now takes one extra
   element to clear its own rule: `body .sun-row` (0,1,1), `body
   .sun-status.is-live` (0,2,1), `body .mk-feed span` (0,1,2), plus the two
   new ones — `body #ticker .tk-lbl i` (1,1,2 vs the dot's own 1,1,1 at
   section 6), the red LIVE dot whose own marquee was already stopped here, so
   with reduce on the strip froze and the dot kept blinking; and `body
   .auth-wrap .logo.auth-logo span` (0,3,2 vs 0,3,1 at section 9), the CUT DAY
   letter drop. `#overlay .modal` and `#ticker .tk-mq` are left plain: both
   are declared ABOVE this query, so both already land. Every killed animation
   here fills `both` or is a pure opacity blink, so its resting state IS what
   the element should look like, and colour still carries each state. */
@media (prefers-reduced-motion: reduce){#overlay .modal,#ticker .tk-mq,.pips i.cur,body .sun-row,body .sun-status.is-live,body .mk-feed span,body #ticker .tk-lbl i,body .auth-wrap .logo.auth-logo span,body #belt[data-belt]::before{animation:none}.m-jokers .joker,.m-hand .card,.m-hand .card .face,.m-counter .n,.m-counter .box,.m-field .ball{transition:none}.m-counter .odds .ev,.m-counter .m-banner{animation:none}}

/* ---- 5b what a sheet CONTAINS — the parts that are not chrome ----
   The 14 px prose floor for `.hint` used to be declared here, sheet-scoped;
   section 3b carries it for the whole document now (legacy.css's portrait-only
   `body.port .hint{font-size:14px!important}` was the other half of it).
   The pix cells of a `.rank` row inside a sheet were authored at 16/18/22px per
   renderer and are one UI size here. */
#overlay .modal .rank .pix{font-size:var(--t-ui)}
#overlay .modal .rank{font-size:var(--t-body)}
/* A `<button>` needs its UA border and text alignment reset before `.pnl`
   reads as a panel (same reason as `.team-tile` in section 7) — the mode
   picker's two cards and nothing else. */
.sheet-cards{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:stretch}
.sheet-card{flex:1 1 240px;display:flex;flex-direction:column;gap:var(--s2)}
button.sheet-card{--edge:var(--blue);border:1px solid var(--edge);text-align:left;color:var(--cream);cursor:pointer}   /* the edge, for the same reason as button.title-tile (section 8) */
.sheet-card[disabled]{opacity:.55;pointer-events:none}
.sheet-foot{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:center;justify-content:space-between;flex:none}
/* KICKOFF's starters rows (P4 Task 9, RD55/D10): one row per position with 2+ cards, the
   player's pick pressed. A chip is the 44 px `.btn.sm` tier; the pressed one takes the orange
   the match gives the starting QB (Design DNA), the rest stay grey. The gate line under the
   cards is the sheet's reason text, never a control. */
.mode-starters{flex:none}
.starters .starter-chip{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.starters .starter-chip.is-active{background:linear-gradient(180deg,#FF9A4A,var(--orange));color:var(--ink);box-shadow:inset 0 0 0 2px rgba(255,255,255,.35)}
.mode-gate{flex:none}
.mode-gate p{margin:0}
/* How to play — seven numbered steps, one column on a phone. */
/* HOW TO PLAY is seven steps in a scrollport that shows two of them on a
   landscape phone — `auto-fit,minmax(280px,1fr)` makes two columns at 852 px,
   and the pane is flex-SHRUNK to 140 px by the modal's own max-height, which is
   exactly one grid row. Steps 3-7 begin on the next ROW, below the clip, so
   unlike portrait (where a half-line of glyphs leaks) nothing peeked: the sheet
   read as complete on the one screen whose job is to teach the loop.
   The clip announces itself with a mask fade instead of a fourth breakpoint —
   it is geometry-independent, so it holds whatever the grid does. The matching
   `padding-bottom` is what keeps it honest: at full scroll the gradient covers
   24 px of PADDING, not the seventh step's last line, so the fade means "there
   is more" and nothing else. `scrollbar-*` draws the second affordance on the
   platforms that have one; iOS keeps its overlay bar and the fade carries it
   there. Not motion — nothing to gate on prefers-reduced-motion.
   `scrollbar-width` is NOT repeated here: the element carries `.scroll`, which
   already declares `scrollbar-width:thin` in /shared. Only the COLOUR is this
   pane's own. And the two-column shape is undone in landscape (section 12),
   because a mask over a clipped-to-one-row grid dims finished content instead
   of announcing more — see there. */
.howto-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--s3);padding-bottom:var(--s5);-webkit-mask-image:linear-gradient(180deg,#000 calc(100% - var(--s5)),transparent);mask-image:linear-gradient(180deg,#000 calc(100% - var(--s5)),transparent);scrollbar-color:var(--steel) transparent}
.howto-step{display:flex;gap:var(--s3);align-items:flex-start}
.howto-step .t-num{min-width:34px;line-height:1}
/* The scoring chain in step 4 (it opened the Playbook until 2026-09-22): its
   label sits a step clear of the prose above so it reads as a diagram caption. */
.howto-chain{margin-top:var(--s2)}
/* The Cut Day Gazette prints on cream paper (§4.7 / COVERAGE §3): `--dim`
   is scoped so every `.lbl`/`.t-label` on it reads against #F4F0E6 instead
   of failing contrast at #93A3BC, and a cut row is `34px 1fr` with its
   score/cap pair on a SECOND line — the four-column row wrapped and
   overlapped the row above at 362 px of content width (C-06). */
/* `.center` centres TEXT; the revealed card is a block, so its own flex row
   has to be told as well or a one-card `.grid` sits hard left under a centred
   headline. */
.sheet-signed{background:linear-gradient(180deg,#182742,#0B1424)}
.sheet-signed .grid,.sheet-signed .row{justify-content:center}
.gazette{background:#F4F0E6;color:#101512;box-shadow:0 8px 0 #7A6F5A;--dim:#5B655E}
.gazette .pnl,.gazette .headline,.gazette .hint{color:#101512}
.gazette .rank{background:rgba(0,0,0,.06)}
.gazette .rank.is-fav{box-shadow:inset 0 0 0 2px rgba(255,196,45,.55)}
.gazette .is-marked{text-decoration-color:rgba(232,72,59,.8)}
.gazette-date{color:#7A6F5A}
.gazette .c-cut{color:#B33A2F} .gazette .c-sign{color:#1F7A4A}
.gazette .tag.gold-fav{background:var(--gold);color:#1E1400}
/* v0.2 Task 7: the quiet week. A bare `.pnl` inside the gazette keeps the
   shared dark gradient under `.gazette .pnl{color:#101512}` — dark on dark.
   Every other panel in this sheet paints itself from the headline palette
   inline; this one is static, so it is a class. */
.gazette .quiet{background:rgba(0,0,0,.05);--edge:#7A6F5A}
/* Two rows, two columns, declared: a 34 px position chip, the name beside it,
   and everything numeric on a second line under the name. Task 4 needed
   `!important` on all three declarations to beat legacy.css's
   `body.mob.port .rank{display:flex!important;flex-wrap:wrap!important;
   gap:4px 10px!important}` (area `ranks`), which was written to defeat the
   eight INLINE `grid-template-columns` the `.rank` call sites carried and
   could not tell this class apart from those. That area is GONE (Task 7), so
   the `!important`s are gone with it: `#overlay .modal .rank.sheet-row`
   (1,2,1) out-specifies section 9b's own `.rank` (0,1,0) on its own. */
.sheet-row{align-items:center}
/* THE BOSS SHEET's scheme rows (D-52, app/m/screens.js#bossInfoSheet): the
   /shared `.threat` row — code chip · name + the server's sentence · strength —
   at the phone scale. The sentence is 14 px body copy (it is the rule the
   player reads), the name line a 14 px head; the code chip stays a 12 px
   label. The strength column (`.sev`: word + pips) hugs its content, so the
   sentence takes whatever the sheet has left. A resting scheme (`.is-off`) is
   one line — code, name, "off this week" — never a card with nothing in it. */
.threat.bs-row{grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:var(--s2);margin:0;padding:var(--s2) var(--s3) var(--s2) var(--s2);font-size:var(--t-body);line-height:var(--lh-body)}
.threat.bs-row b{align-self:start;font-size:var(--t-label)}
.bs-name{font-size:var(--t-body);line-height:1.25}
.bs-copy{font-size:var(--t-body);line-height:1.3;color:#D5DEEB;font-weight:500}
.threat.bs-row .sev{align-self:start;gap:var(--s1)}
.threat.bs-row.is-off{padding-block:var(--s1)}
.threat.bs-row.is-off b{align-self:center}
#overlay .modal .rank.sheet-row{display:grid;grid-template-columns:34px minmax(0,1fr);gap:var(--s1) var(--s2)}
#overlay .modal .rank.sheet-row .sheet-meta{grid-column:2}
/* The recipe book (P4 Task 14, D12, RD59/RD64/RD65) — screens.js#recipeBookSheet, from the deck
   screen (with the respec) and read-only from the match. One group per size, one row per recipe:
   the name and its level on the first line, its cards and starting MULT under it, the meter to
   the next threshold across the foot (none at the cap, none while locked). Two tracks, not four
   fixed ones: a 358 px sheet on a 390 phone gives the name every pixel the LVL tag does not take. */
.recipe-group-hd{margin:0;color:var(--dim);letter-spacing:.08em}
.recipe-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:2px var(--s2);padding:var(--s2) var(--s3);background:rgba(8,17,31,.55);box-shadow:inset 0 0 0 1px rgba(255,196,45,.28)}
.recipe-row .recipe-name{min-width:0;font-size:var(--t-ui);color:var(--cream)}
.recipe-row .recipe-lv{font-variant-numeric:tabular-nums;white-space:nowrap}
.recipe-row .recipe-cards{grid-column:1 / -1;letter-spacing:.06em}
.recipe-row .recipe-meter{grid-column:1 / -1;height:4px;margin-top:var(--s1)}
.recipe-row.is-locked{box-shadow:inset 0 0 0 1px rgba(62,82,115,.6)}
.recipe-row.is-locked .recipe-name,.recipe-row.is-locked .recipe-cards{color:var(--dim)}
/* The respec block: the cost tag in gold (money), the two pickers as wrapping chips, the one
   gold CONFIRM. A chosen chip carries the gold inset every other toggle of the client uses. */
.respec{--edge:rgba(255,196,45,.45)}
.respec-hd{margin:0;color:var(--gold)}
.respec .t-head{margin:0}
.respec-why{margin:0}
.respec-chip{padding:0 var(--s2)}
.respec-chip.is-active{box-shadow:inset 0 0 0 2px var(--gold);color:var(--gold)}
/* Settings (§4.6). The danger zone is a panel inside the sheet, not a
   different sheet — one tap to arm, one to confirm, both in view. */
#settingsSheet .pnl.dark{--edge:rgba(255,59,59,.55)}

/* ==== 6 shell (A12) — #app is a grid; #side holds the hub header + tab bar, #main the screen ====
   Nothing in the shell is position:fixed any more (A12) and the safe-area
   insets are applied once, on the grid container. The four viewport-fixed
   LAYERS below are siblings of #app, not part of the grid — they came over
   verbatim from the `base-shell` area this task deletes (legacy.css L2, L6,
   L9-L13, L29-L30) because every screen needs them and no other sheet has
   them. */
#bg{position:fixed;inset:0;width:100%;height:100%;z-index:0}
#cards{position:fixed;inset:0;z-index:5;pointer-events:none}
#top{position:fixed;inset:0;z-index:6;pointer-events:none;overflow:hidden}
#overlay{position:fixed;inset:0;z-index:30;display:none;background:rgba(3,8,18,.72)}   /* its padding/alignment: section 5 */
#overlay.on{display:flex}
#fx{position:fixed;inset:0;z-index:40;pointer-events:none;overflow:hidden}
#crt{position:fixed;inset:0;z-index:90;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(0,0,0,.16) 0 1px,transparent 1px 4px);opacity:.35}
#crt::after{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 115% 105% at 50% 50%,transparent 60%,rgba(0,0,0,.55) 100%)}

#app{position:fixed;inset:0;z-index:2;display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr auto auto;grid-template-areas:"hd hd" "side main" "tick tick" "nav nav";padding:var(--sat) var(--sar) var(--sab) var(--sal);box-sizing:border-box}
#app.noside{grid-template-columns:1fr;grid-template-rows:1fr auto;grid-template-areas:"main" "tick"}
#app.noside #side{display:none}
/* #side is the element every renderer fills (game.js#go, match.js#renderSide).
   On a hub screen it holds the header + tab bar and stops being a box, so its
   two children take the `hd` and `nav` areas directly. In a match it is still
   match.js's own rail — landscape fills it, the portrait branch empties it —
   and it takes the `side` column, which is 0 wide when it holds nothing. */
#side{grid-area:side;min-height:0;display:flex;flex-direction:column;overflow:auto;overflow-x:hidden;scrollbar-width:none}
#side:has(.hub-hd){display:contents}
#side:empty{display:none}
#side::-webkit-scrollbar{display:none}
.hub-hd{grid-area:hd;min-height:var(--hdr);display:flex;align-items:center;gap:var(--s2);padding:0 var(--gut);background:linear-gradient(180deg,#0B1424,#05080F);box-shadow:0 1px 0 rgba(255,255,255,.06);container-type:inline-size}
.hub-hd .hub-play{min-height:44px;padding:0 var(--s4);font-size:var(--t-ui);flex:none}
/* 345 px is the header's own arithmetic, not a device width. The fixed chrome
   is 44 (back) + 102.5 (the PLAY pill) + 44 (gear) + three 8 px gaps = 214.5,
   so `.hub-stats` gets `container - 214.5` = 113.5 at 360. Neither block
   inside it shrinks below its min-content — the coins block is 49.2 (set by
   « PIÈCES », not by the value) and the French cap block at "120/120" is 76.4,
   plus the 12 px row gap = 137.6, measured.
   Below 345.5 px of header those two cannot both be drawn, and the one that
   loses paints UNDER the opaque PLAY pill instead of being clipped: fr `small`
   read "120/12", which is the worst kind of truncation because the remainder
   is still a plausible number ("over cap by 10x").
   So the cap block is DROPPED there rather than squeezed — the same call the
   final screen already makes two rules up, and the Locker prints the identical
   cap line 40 px below this header.
   SHRINKING the value instead was tried and MEASURED, because section 12 ships
   exactly that for phone landscape (`.hub-stats .t-head{font-size:var(--t-ui)}`)
   and it would have kept both readouts. It misses. At 360 in French, with the
   widest string this element can draw ("120/120"), `--t-head` 20 → `--t-ui` 16
   takes the cap block from 83.8 to 61.1 — the `.t-label` « CAP COMPO » wraps to
   two lines at this width, so its min-content is only ~53 and the VALUE is what
   sets the box — and the pair still asks 49.2 + 12 + 61.1 = 122.3 against the
   113.5 available: an 8.8 px miss, the cap's right edge 0.8 px INSIDE the
   opaque PLAY pill, and `overflow:hidden` then clips the last glyph back to exactly
   the "120/12" this rule exists to prevent. `--t-body` (14, the floor for a
   value) gets it to 115.2 — still over — and a server that moves the cap to
   "1200/1000" adds another 15 on top of that.
   Measured `.hub-hd` border-box, small → tabl: 360 / 390 / 412 / 780 / 820 /
   1060. The CONTAINER is the content box, which is what this query reads:
   328 / 358 / 380 / 764 / 600 / 1028. Only 360 drops; every other profile keeps
   the block and clears the pill ("120/120": +8 at 390, +28.5 at 412, +412.5 in
   landscape, +248.5 / +676.5 on the tablets).
   What the drop was missing was the RE-BALANCE. `.hub-stats` kept its full flex
   width with only the coins block left in it, so the Kaiten market header read
   « ◂ | PIÈCES $100 | 64 px of void | JOUER | ⚙ » — and a hole on the screen
   where the player spends against that cap reads as a failed render, not as a
   decision. So the row also stops claiming the space: `.hub-stats` sizes to its
   content (`0 1 auto` — it may still SHRINK, which is what keeps the guard
   below meaningful) and the PLAY pill takes the 64 px. Nothing is missing on
   that line any more; the cap is one tap away in the Locker, where it is
   reprinted in full.
   The pixel threshold is derived from INPUTS, not constants: `G.capTotal` is
   server-driven and has already moved 100 → 120, and `fmt(G.coins)` grows with
   the balance. `overflow:hidden` on `.hub-stats` is the content-level guard for
   every container this query does NOT catch — at 390 a "1200/1000" cap already
   overruns by 13.5 — so an unforeseen value is CLIPPED, visibly wrong, instead
   of painting a plausible wrong number under the pill.
   `@container`, not a fourth `@media`: the question is how much room THIS
   header has, and the header is not re-rendered on resize, so the answer has
   to be re-evaluated by the style engine rather than by a render pass. */
.hub-hd .hub-stats{overflow:hidden}
/* The cap block this comment measured is gone (B5 Task 5, RD38); the re-balance below still sizes the
   coins block and the PLAY pill on the narrowest header. */
@container (max-width: 345px){
  .hub-hd .hub-stats{flex:0 1 auto}
  .hub-hd .hub-play{flex:1}
}
.tabbar{grid-area:nav;display:grid;grid-template-columns:repeat(5,1fr);min-height:var(--nav);background:linear-gradient(180deg,#0B1424,#05080F);box-shadow:0 -1px 0 rgba(255,255,255,.08)}
/* A `.tab` is a bare <button>, not a `.btn` — without a skin it renders in the
   UA's white button chrome. Active = gold label + wash, on top of section 4's
   gold inset ring. */
.tab{position:relative;background:none;border:0;color:var(--dim);cursor:pointer;text-shadow:0 1px 0 rgba(0,0,0,.55)}
.tab[aria-current="page"]{color:var(--gold);background:linear-gradient(180deg,rgba(245,183,41,.16),rgba(245,183,41,.04))}
.tab:active{background:rgba(255,255,255,.07)}
#main{grid-area:main;min-width:0;min-height:0;display:flex;flex-direction:column}
/* `display:flex;flex-direction:column` is load-bearing, not decoration: every
   screen that bounds a list to the viewport (#lockerRoot's ACTIVE/RESERVE
   `.scroll` panes, the market's list) does it with `flex:1;min-height:0` on a
   child, which only has anywhere to grow into if THIS box is a column flex
   container. Without it #lockerRoot grew to its content height (measured 1648
   px inside an 820 px viewport) and #screen scrolled the whole roster instead
   of each pane scrolling inside its own box. */
#screen{position:static;flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;padding:var(--s2) var(--gut);box-sizing:border-box}
/* A single very tall panel scrolls inside itself. PORTRAIT ONLY — this came
   from legacy.css's `body.port #screen>.pnl`, and generalising it to
   landscape breaks the locker: at 852x393 the in-flow header (44) + ticker
   (28) leave #screen 321 px, #lockerRoot needs 314 with the sack bar open,
   and clipping it inside the panel put the SACK button behind #lockerRoot's
   own box (measured: tests/locker.spec.js:76 could not click it). The two
   landscape sections below hand the scroll back to #screen. */
#screen>.pnl{overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain}
/* The match screen draws to its own edges (match.js#renderTable's own margins)
   and is the one screen that must not scroll (PORTRAIT-SPEC.md §4.1). */
#app:has(#zone) #screen{padding:var(--s1);overflow:hidden}

/* Ticker — 28 px, in the `tick` grid row, never fixed (§4.5). The whole skin
   lives here now: the `ticker` area of legacy.css (base 58 px / body.mob 34 /
   portrait 44, all position:fixed and stacked on --nav + --cta) is gone. */
#ticker{grid-area:tick;position:static;height:var(--tick);display:flex;align-items:center;background:linear-gradient(180deg,#0B1424,#05080F);box-shadow:0 -1px 0 rgba(255,255,255,.08),inset 0 2px 0 #2A3A55;overflow:hidden}
/* `position:relative;z-index:1` — the label is the one part of this strip that
   does NOT scroll, and it comes FIRST in the DOM, so without a stacking order of
   its own the marquee's items painted OVER it: on the owner's Android shot
   "Andy Dalton" was sitting on top of "LIVE". The gradient below is opaque, so
   raising the label out of document order is the whole fix — measured with
   elementFromPoint swept across the label at 360/390/412/852, five frames each:
   only the label answers, at every sample.
   A clipping LANE around the marquee was tried first and reverted: a clip on a
   transformed box travels with the box, so the track has to be a second element,
   and probe.js names an element with an id `div#tkmq` — which
   tools/portrait-audit/gate-mobile.js's `CLIP_LAYER` (`\.tk-(mq|it)\b`, a CLASS
   token) cannot exempt. A track whose own box is the full content width then
   reads as real horizontal overflow and took the strict gate from 0 to 2 on both
   profiles. Keeping the marquee as ONE `flex:1;min-width:0` box — its BOX inside
   the lane, its CHILDREN overflowing it — is what keeps that exemption landing. */
#ticker .tk-lbl{flex:none;position:relative;z-index:1;height:100%;display:flex;align-items:center;gap:5px;padding:0 var(--s3) 0 var(--s2);white-space:nowrap;background:linear-gradient(180deg,#3A2A12,#1A1408);font:var(--t-label)/1 var(--pix);letter-spacing:.14em;color:var(--gold);clip-path:polygon(0 0,100% 0,calc(100% - 10px) 100%,0 100%)}
#ticker .tk-lbl i{width:6px;height:6px;flex:none;border-radius:50%;background:var(--red);box-shadow:0 0 8px var(--red);animation:blink 1s steps(2) infinite}
/* `.tk-mq` (0,1,0) — deliberately BELOW the reduced-motion kill in section 5,
   which is `#ticker .tk-mq{animation:none}` (1,1,0). The two rules are in one
   file now (legacy.css owned the kill until Task 9) but the mechanism is
   unchanged and still order-independent: raise this selector to the id level and
   the marquee starts running again for a reduced-motion reader, because section
   5's query sits ABOVE this line. tests/mobile-scale.spec.js guards it. */
.tk-mq{flex:1;min-width:0;display:flex;align-items:center;gap:var(--s2);white-space:nowrap;animation:marquee-px var(--mq-t, 40s) linear infinite;will-change:transform;padding-left:var(--s2)}
#ticker .tk-it{flex:none;display:flex;align-items:center;gap:var(--s2);padding:2px var(--s2) 2px var(--s1);background:rgba(255,255,255,.05);clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%)}
/* 18 px (C-07 / spec §4.5): the item is ONE line now — screens.js#tickerItems
   stopped stacking the name over its stat line with a `<br>`, which was two
   12 px lines inside a 28 px strip and clipped the second on every screen the
   ticker is on. 18×24 is the tallest card that still leaves the strip its
   2 px of padding above and below. */
#ticker .tk-it .mini{width:18px;height:24px;--mw:18px;pointer-events:none;box-shadow:inset 0 0 0 1px #5B7597}
#ticker .tk-it .mini .mm,#ticker .tk-it .mini .mst,#ticker .tk-it .mini .mn,#ticker .tk-it .mini .mp{display:none}
#ticker .tk-it b{font:var(--t-label)/1 var(--pix);color:var(--cream);white-space:nowrap}
#ticker .tk-it b.c-gold{color:var(--gold)}   /* the coach item's own label — `.c-gold` (0,1,0) loses to the rule above */
#ticker .tk-it small{font-size:var(--t-label);letter-spacing:.08em;color:var(--dim);white-space:nowrap}
#ticker .tk-it .pr{font:var(--t-label)/1 var(--pix);color:var(--gold)}
#ticker .tk-it.coach{background:rgba(255,196,45,.12)}
.tk-sep{flex:none;font-size:var(--t-label);color:rgba(255,255,255,.25)}
body:not(.tk) #ticker{display:none}
#fsBtn,#fsPrompt{display:none !important}      /* fullscreen lives in the Settings sheet (Task 4) */

/* ==== 7 auth + pick team (spec §4.3 / D8; LOGIN-HOME-AUDIT §3.2 and M-16) ====
   Both screens render inside `#screen` with `#app.noside`, so the gutter and the
   safe areas are already applied once by the shell — nothing below re-applies
   either (M-02). `#screen` is `padding:var(--s2) var(--gut)`, which IS the
   `--col` column: a panel at `width:100%` measures 358 at 390, the audit's
   number, where a literal `width:var(--col)` inside that padding box would
   subtract a second 16 px gutter per side. */
.auth-wrap{display:flex;flex-direction:column;align-items:center;gap:var(--s4);padding:var(--s2) 0}
/* The audit's rhythm is 8 after the logo and 24 after the header block, on top
   of the 16 px default step above (§3.2): two corrections, not two more gaps. */
.auth-tag{margin-top:calc(-1 * var(--s2));white-space:nowrap}
/* `.logo span` is the shared title lettering; its base (family, style, shadow)
   lives in section 8 with the screen that owns the component, and legacy's two
   copies — the unscoped clamp(64px,13vw,168px) and the `body.mob` (0,2,2)
   clamp(40px,11vw,92px) that was the T1 line's `title-menu` half — are gone with
   the `title-menu` area. The three-class selector is what beat that (0,2,2) rule
   while it lived; measured then, `.auth-wrap .auth-logo span` (0,2,1) lost and
   the logo rendered at 42.9 px on a 390 phone and 92 px in landscape. */
.auth-wrap .auth-logo{gap:2px;margin:0;transform:none;flex:none}
.auth-wrap .logo.auth-logo span{font-size:clamp(28px, 9vw, 44px);line-height:1}
.auth-wrap .auth-logo .logo-sp{width:var(--s2)}
/* One panel, one column, centred to the pixel. Padding is 14/16/14/12 on top of
   the 4 px rail, so the content column has 16 px on BOTH sides (M-05) — the
   same symmetry section 4's `.pnl` default now carries, with 2 px more air
   above and below because this panel is the whole screen. */
.auth-panel{width:100%;max-width:420px;margin:var(--s2) auto 0;padding:14px var(--s4) 14px var(--s3)}
.auth-seg{gap:2px}
/* Two halves of one control: same width, same 6 px lean, same height (M-13 —
   the shared `.btn` lean is 9 px and only the outer edges used to carry it).
   Height is the `.btn.sm` tier's 44, NOT the 48 that LOGIN-HOME-AUDIT §3.2's
   diagram asks for. Three reasons, in order: the audit's own §3.4 scale table
   and spec §4.1 both list "segmented halves" under the 44/13 small tier;
   legacy.css pins them there anyway with `body.mob.port .btn.sm{min-height:
   44px!important}` (area `a11y-floor`, another task's), which only `!important`
   in this sheet could beat; and that rule is portrait-only, so winning it
   would make the control 44 in portrait and 48 in landscape. Flagged in the
   task report for the controller. */
.auth-seg .btn{flex:1;min-width:0;padding:0 var(--s2);clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%)}
.auth-seg .btn.is-active{box-shadow:inset 0 0 0 2px var(--gold)}
.auth-hint{min-height:48px;display:grid;place-items:center}
.auth-howto{width:100%;max-width:420px}

.pick-wrap{display:flex;flex-direction:column;gap:var(--s4);flex:none}
.pick-wrap>*{flex-shrink:0}
.pick-intro{margin:0}
.pick-conf{display:flex;flex-direction:column;gap:var(--s2)}
.pick-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--s2)}
.pick-div{display:flex;flex-direction:column;gap:var(--s1);min-width:0}
/* A `<button>` needs its UA border and text alignment reset before the shared
   `.pnl` rail reads as a rail; the tile's own colour comes from screens.js as
   `border-left-color` (the one data-driven declaration left inline). */
.team-tile{min-height:56px;--edge:var(--blue);border:1px solid var(--edge);color:var(--cream);text-align:left;cursor:pointer;align-items:center;padding:var(--s2) var(--s3) var(--s2) var(--s2);width:100%}
.team-tile .disc{width:28px;height:28px}
/* Name over abbreviation, both at the tile's full inner width. Never
   ellipsised (M-16): two lines are allowed and the box is what grows, which
   only holds because `overflow-wrap:anywhere` can break the one unbreakable
   word every NFL team name is — without it a narrow tile shrinks the box
   below the word and the glyphs paint outside it. */
/* The 8 px of air to the tile's inner right edge is DECLARED, not hoped for:
   with the text free to fill its box, a line packs to within a pixel or two of
   the edge and the `.pnl` chamfer is only 14 px away at the top corner. This
   holds at any column count and for any name the league invents. */
.team-id{display:flex;flex-direction:column;justify-content:center;min-width:0;padding-right:var(--s2)}
/* Carry-over from Task 3's fix round: at 360 the tile is 160 px and
   "Commanders" broke mid-word at 16 px. `min()` keeps 16 px from 400 px up and
   never drops below 14.4 px at 360 — clear of the 14 px body-copy floor. */
.team-name{display:block;min-width:0;white-space:normal;overflow-wrap:anywhere;line-height:var(--lh-ui);font-size:min(16px, 4vw)}
.team-tile.is-active{box-shadow:inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 2px var(--gold)}
/* CONFIRM sticks to the bottom of `#screen`'s scrollport instead of the
   position:fixed bar that used to sit ON the last row of tiles (measured on
   day 0: Playwright could not click the last tile, `.pick-cta` intercepted). */
.pick-cta{position:sticky;bottom:0;z-index:6;background:var(--panel2);padding:var(--s2) 0;box-shadow:0 -8px 20px rgba(3,8,18,.8)}
/* A sticky `bottom:0` lands on the CONTENT box, so `#screen`'s bottom padding
   stayed a band of scrolling tiles under an opaque bar (measured: 8 px at 390,
   12 px on the tablets). Dropping that one padding on this screen only is
   breakpoint-independent; `.pick-cta` supplies its own 8 px of air. */
#screen:has(.pick-wrap){padding-bottom:0}
/* swirl.js's `#lights` is a fixed full-viewport layer whose `.turf` owns the
   bottom 58 % at 0.78-0.95 alpha green, with 0.45-0.5 alpha yard lines and
   `clamp(16px,2.4vw,34px)` yard NUMERALS. A screen that is one opaque slab
   hides it. These two are not: the team picker is 32 tiles with 8 px gaps and
   bare heading text, and the draft is a board of separated cards, so from
   about the halfway point down, every gap and every division heading sits on
   lit turf. In landscape the four NFC headings landed on the same baseline as
   the numerals and read "EAST 10 20 30 NORTH 40 50 40 30 SOUTH 20 10 WEST",
   with the blue NFC on the 50-yard line and on the midfield roundel.
   Precedent: `.mact` (section 13) carries its own opaque fill for exactly this
   — "swirl.js's turf numerals paint straight through the bar". The field stays
   as a GROUND here, it just stops competing with UI text. `body:has(...)`
   is (1,2,1) against swirl's (1,1,0), and swirl never sets `opacity` on
   `.turf`, so its later-appended sheet has nothing to win. */
body:has(.pick-wrap) #lights .turf,body:has(.draft-wrap) #lights .turf{opacity:.25}

/* ==== 7b draft (v0.2, API.md §3.3ter) — onboarding step 3 ====
   Third `#app.noside` screen, so the gutter and the safe areas are the shell's
   again (M-02) and nothing below re-applies either. `/shared` owns the
   COMPONENT (`.draft-wrap` grid, `.draft-board`, `.draft-card`, `.draft-tier`,
   `.draft-kept`, `.draft-round`) because both documents render the same DOM;
   what is here is the mobile SCALE of it, and it is four corrections /shared
   cannot make without moving desktop: the card column, DROP's control tier,
   the tier badge's label floor, and the kept ROW (last block below).
   `--dcw` is the draft's own card column. It is deliberately NOT `--cw`: that
   token is the MATCH card width and match.js rewrites it at runtime, so a
   board sized from it would change under a screen that has nothing to do with
   a match. */
.draft-wrap{--dcw:84px;flex:none;gap:var(--s3)}
/* `#screen` is the scrollport on a phone (section 6) and this wrap is
   content-sized, exactly like `.pick-wrap` above. */
.draft-wrap>*{min-width:0}
/* The pitch, the drop hint and the error line are PROSE (14 px floor, section
   3b), not labels — `.hint`'s own 6 px top margin would double the panel's
   `.stack` gap, so it is dropped here as it is in the locker. */
.draft-wrap .hint{margin-top:0}
/* THE BOARD (finish review item 8): the hub's one card face (`.bface`, section 15), three to a
   row, each as wide as its track — a surname is never ellipsised on a board (owner ruling: the
   board scrolls in portrait rather than shrinking the faces). The PWR tier is the card's FRAME
   (`data-tier`, section 15); the text tag under the card is gone. */
.draft-board{grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s3) var(--s2);justify-items:stretch}
.draft-board .draft-card,.draft-kept .draft-card{display:grid;gap:var(--s1);align-content:start;min-width:0;width:100%}
.draft-board .card.bface,.draft-kept .card.bface{width:100%;height:auto;aspect-ratio:70/100}
/* The kept pile: the same face at the same size, each round's picks under its R label. */
.draft-round{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s3) var(--s2);align-items:start}
.draft-round>.lbl{grid-column:1 / -1;width:auto;padding:0}
.draft-round.is-flat{grid-template-columns:repeat(3,minmax(0,1fr))}
/* DROP is the small CONTROL tier (44, A3), not `/shared`'s 30 px desktop chip. */
.draft-card.kept .btn.sm{min-height:var(--ctl-sm);width:100%;padding:0 var(--s2)}
.draft-kept{gap:var(--s3)}
/* ---- 7b bis. the Balatro drafts (P4 Task 10, spec §4.3, RD60/RD72) ----
   The mini-card's ONE value chip (screens.js#miniStats, the gazette's piles): YARDS on the
   design DNA's blue, `+{n} MULT` on its red. */
.mini .mst .my{background:linear-gradient(180deg,#3FB1FF,#0F6FC7)}
.mini .mst .mx{background:linear-gradient(180deg,#FF5E5E,#B81E1E)}
/* The board header's legend names the two chips by colour AND word: YARDS left, MULT right. */
.sw-yards,.sw-mult{display:inline-flex;align-items:center;gap:5px}
.sw-yards::before,.sw-mult::before{content:'';flex:none;width:14px;height:10px;
  clip-path:polygon(5px 0,100% 0,calc(100% - 5px) 100%,0 100%)}
.sw-yards::before{background:linear-gradient(180deg,#3FB1FF,#0F6FC7)}
.sw-mult::before{background:linear-gradient(180deg,#FF5E5E,#B81E1E)}
/* A refused card (the deck needs something else first, RD72): the frame stays so the board
   still reads 10, the face goes grey — the reason is the `.draft-need` line. */
.draft-card.is-refused .bface{opacity:.45;filter:grayscale(1)}
.draft-card.is-full .bface{opacity:.6}
/* The header: the week's boss rules as red tags, the round line with its pips, then BEST PICKS
   and FINISH sharing one row (`min-width:0`, so a long label never pushes the pair off the
   panel), LATER under them as a text action (the weekly redraft only). */
.draft-rules{margin:0}
.draft-round-hd .pips{flex:none;width:calc(var(--s4) * 4);margin-top:0}
.draft-cta>.btn{min-height:var(--ctl-std);min-width:0;flex:1 1 0}
.draft-counted.c-green{color:var(--green)}

/* ==== 8 title / home (spec §4.4 / D7; LOGIN-HOME-AUDIT §3.3 and M-01/M-07/M-08/
   M-09/M-10/M-15) ====
   One centred column inside `#screen` (`#app.noside` = main + ticker): user row,
   logo, tagline, menu, boss/mutator strip — DOM order IS visual order, so the two
   `order:` rules that used to push PLAY to the bottom are gone with the legacy
   area (M-15). Nothing here is position:fixed (M-01) and nothing re-applies the
   safe areas or the gutter — `#screen` already owns both (section 6, M-02). */
/* `min-height:100%` (every box is `border-box`, /shared L5) makes the column as
   tall as `#screen`'s scrollport whenever the content is shorter, and the two
   auto margins below then split the leftover EQUALLY: the user row stays pinned
   to the top and the logo→tagline→PLAY→menu→strip group floats to the middle of
   what is left, on every breakpoint. When the content is taller the autos resolve
   to 0 and the column simply scrolls — auto margins only absorb positive free
   space. */
/* `container-type:inline-size` is here for the identity row below (and nothing
   else queries it yet): the row needs to know how much room IT has, which is a
   question about this column, not about the window — `.title-wrap` is the only
   box that answers it identically in the flex portrait shape and the landscape
   grid one. Inline-size containment only; the block size still resolves against
   `#screen`, so `min-height:100%` and the two auto margins below are untouched,
   and nothing in this section is absolutely positioned. */
.title-wrap{width:100%;min-height:100%;display:flex;flex-direction:column;align-items:center;gap:var(--s4);padding:var(--s2) 0;container-type:inline-size}
.title-hero{margin-top:auto}
.title-strip{margin-bottom:auto}
/* Top bar, IN FLOW as the first row. `.tag` is `display:inline-block` in /shared
   and rendered 21.6 px tall next to a 44 px button (M-08); it has to STAY a block
   box — `display:flex` would kill the `text-overflow:ellipsis` `#titleUser`
   depends on — so the 44 px row comes from min-height plus a matching
   line-height, not from flex centring. `#titleUser` is the only item allowed to
   truncate (`.grow-min.ellipsis`); the TEAM chip, the stats chip and the two
   icons never do. (The row is a grid now — see below — but a grid item is
   blockified exactly as a flex item is, so `.tag` reaches `block` either way
   and the ellipsis still has a block box to work in.) */
/* Five items, and on a phone they do not fit one 44 px line: identity chip,
   team chip, stats chip and two 44 px icons need ~458 px against the 328 the
   360 px phone gives them. As a flex row the only item that could give was
   `#titleUser` (`.grow-min` = flex-basis 0), so it gave everything — 76 px, and
   « Aud… ». The row is a GRID with a second line instead, and what moves down
   is the STATS chip: the identity and the two controls keep the top line, in
   DOM order, on every profile.
   The container is `.title-wrap` (a row cannot query its own size) and the
   query is on the ROW's own room, not the viewport, so no fourth breakpoint and
   no `setCW` pass is involved. The container width is not always the row width,
   though: in TABLET landscape (section 15) `.title-wrap` becomes a
   `minmax(0,360px) minmax(0,520px)` grid with `justify-content:center`, so the
   row it contains is capped at 896 while the container measures 1132. The
   decision is unaffected — 896 is still far over 460 — but the query reads the
   CONTAINER, and only in the phone shapes are the two numbers the same.
   Only `#titleStats` is placed explicitly; the other four auto-flow into row 1
   in DOM order, skipping the cell the stats chip already holds.
   `justify-self:start` on both chips is what keeps them CHIPS. A grid item
   stretches to its area by default, so `1 / -1` turned the stats chip into a
   ~360 px black slab with its text on the left edge and ~70 % of the box empty
   — two stacked dark bands above the wordmark, the second reading as a header
   panel. `#titleUser` had the milder half of the same defect: its `minmax(0,1fr)`
   track ran the chip ~80 px past a short name. It keeps `max-width:100%` so the
   track still buys the name its width and the ellipsis still fires at the same
   point; only the empty tail goes. */
.title-user{width:100%;min-height:var(--ctl-sm);flex:none;grid-area:user;display:grid;align-items:center;grid-template-columns:minmax(0,1fr) auto auto auto}
#titleStats{grid-column:1 / -1;grid-row:2;justify-self:start}
/* 460 px is measured, not chosen: the one-line row asks for identity (a name
   worth reading, ~144) + team (~44) + the French stats chip (~150) + 44 + 44 +
   four 8 px gaps = ~458. Measured `.title-wrap`, small → tabl: 328 / 358 / 380
   / 836 / 600 / 1132. Every phone portrait container is under 460; phone
   landscape (836), both tablets (600 / 1132) and the A20 stage are over, and
   those all held one line before this change. */
@container (min-width: 460px){
  .title-user{grid-template-columns:minmax(0,1fr) auto auto auto auto}
  #titleStats{grid-column:3;grid-row:1}
}
.title-user .tag{min-height:var(--ctl-sm);line-height:var(--ctl-sm);padding:0 var(--s3)}
/* The team is the half that used to die first, so it never shrinks and never
   ellipsises: 2-3 characters of server data, beside the boss disc that already
   colour-codes the same franchise. What holds it is the row's `auto` TRACK, not
   the `no-shrink` class the markup still carries — that class is `flex:none`,
   and the row became a grid, so it is inert on this element. It is left in the
   markup as the row's own declaration of intent (the same class marks the three
   other items that must never give), not because anything reads it. */
#titleTeam{font-size:var(--t-body)}
/* The one `.tag` on the app that carries PROSE (a coach name and a team name,
   both server data) rather than a label — so it takes the 14 px body size, not
   the 12 px chip one. It is also the one element whose probe selector reaches
   the gate as `span#titleUser`: probe.js#sel drops the class list as soon as an
   element has an id, so a `.t-label`/`.tag` allow-list entry could never have
   covered it and the size is the honest fix.
   `justify-self`/`max-width` are the grid half (see `#titleStats` above): the
   chip sizes to the name instead of to its `minmax(0,1fr)` track, and the track
   still sets the point at which the ellipsis fires. */
#titleUser{font-size:var(--t-body);justify-self:start;max-width:100%}
/* The logo is shared with the auth screen (section 7 only resizes it), so its
   base lives here, with the screen that owns it — legacy.css's `.logo`/`.logo
   span` die with this area. `letterIn` is /shared's keyframe: the TITLE logo has
   been static on mobile since day 0 (legacy's `body.mob .title-wrap .logo
   span{animation:none}`) and stays static; the auth screen is the one that
   animates, and this is the declaration that keeps it doing so. */
.logo{display:flex;align-items:flex-end;gap:2px}
.logo span{display:inline-block;font-family:var(--pix);font-style:italic;line-height:.95;text-shadow:0 3px 0 rgba(0,0,0,.55)}
.logo .logo-sp{width:var(--s2)}
.title-wrap .logo span{font-size:clamp(34px, 11vw, 52px)}
.auth-wrap .logo.auth-logo span{animation:letterIn .55s cubic-bezier(.2,1.3,.3,1) both}
/* M-07: the tagline used to render at 18.57 px over two lines because
   screens.js carried `font-size:clamp(14px,2.2vh,20px);letter-spacing:.34em`
   inline. It is a `.t-label` now — 12 px, `--ls-label` (.10em, the sheet's own
   label tracking rather than the audit's .12em) — on ONE line. The −8 px pull is
   the audit's rhythm (8 px after the logo, 24 px before the menu) expressed as
   two corrections on the 16 px default step, the same way `.auth-tag` does it. */
.title-tag{margin-top:calc(-1 * var(--s2));white-space:nowrap;color:var(--cream)}
/* The header block: logo, tagline, the lineup note and the primary CTA. It is a
   real flex column so that the two landscape breakpoints can centre it against
   the destination column with `justify-content` instead of trying to align two
   independent stacks on shared grid rows. Its 16 px step carries the audit's
   rhythm with two corrections: −8 pulls the tagline to 8 px under the logo, +8
   pushes PLAY to 24 px under the tagline (§3.3). */
.title-hero{width:100%;align-items:center}
/* +8 only in the no-note state, where PLAY is the tagline's own next sibling:
   16 + 8 = the audit's 24 px before the CTA. When the lineup note is there it
   takes that slot at the plain 16 px step and its own −8 puts PLAY 8 px under
   the sentence that explains it, which is what the note is for. */
.title-hero>.title-tag+.btn.pri{margin-top:var(--s2)}
/* `#screen` already owns the gutter, so the menu is 100 % of it — a literal
   `var(--col)` would subtract a second gutter per side (the ruling `.auth-panel`
   got in Task 3). */
.menu{width:100%}
.menu-grid{width:100%}
/* M-10: /shared's 9 px lean was drawn for auto-width buttons; stacked full width
   it reads as a 9 px saw-tooth on BOTH edges of the column. The DNA's own rule is
   that the lean is proportional to the element (`.btn.sm`/`.tag` 5–6 px), and
   6 px is that value at 340×48 — the lean stays, its size stops shouting. */
.title-hero .btn,.menu .btn{clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%)}
/* `minmax(0,1fr)` (section 3) makes the two columns equal whatever the labels
   need; this is what keeps the longest of them, KAITEN MARKET, inside its own
   box at 360 (measured: it wants a 166 px box and gets 160). 4.1vw hits the
   16 px cap from 390 up and reads 14.76 px at 360 — above the 14 px floor. */
.menu-grid .btn{padding:0 var(--s3);font-size:min(16px, 4.1vw)}
.title-note{margin-bottom:calc(-1 * var(--s2))}
/* One badge component: the tab bar's own rule (section 6) keeps only its
   position now, and the FREE AGENTS tile's free-credit count reads as the same
   object. Inside a `.btn` it inverts — a gold pill on the gold tile is invisible. */
.badge{font:700 var(--t-label)/1 var(--ui);color:#1E1400;background:var(--gold);padding:1px 4px}
.btn .badge{background:rgba(0,0,0,.35);color:var(--gold)}
/* The unread Cut Day report: a skin, not `.btn.red` (that tier is destructive
   actions). It used to be an inline gradient on this one call site.
   NB, noted not fixed: this is (0,2,0) in the LATER sheet, so it out-orders
   design-system.css's `.btn[disabled]` background. A disabled report button
   therefore keeps its red plate while taking the disabled `--dim` label — the
   one skin the T3 disabled treatment does not fully land on. It is never
   rendered disabled today (game.js only mounts it when there is a report to
   read — screens.js:233 and :1513 both gate on `G.cutDayReport`), so it is
   left standing and reported rather than patched blind. */
.btn.report{background:linear-gradient(180deg,#5A2226,#3A1518);box-shadow:inset 0 0 0 2px rgba(255,59,59,.55)}
/* Boss strip — one 56 px row that puts back on the phone what
   `body.mob .title-wrap .title-boss{display:none}` used to hide (spec §4.4);
   the mutator tile that shared it left with the mutators (bêta v0.1, R27).
   `.pnl` is `flex:none` in /shared, which is why the tile carries `.grow`
   (section 3, and this file loads last). */
/* An OPAQUE ground under the tile (Beta hotfix 2, owner's Android shot). The
   tile itself already is opaque — `.pnl` / `.pnl.boss` are hex gradients
   with no alpha — but it is cut by `.pnl`'s 14 px corner chamfer, and the
   title screen's ambient turf paints straight through the openings: a yard
   numeral ("5") sat in the boss tile's own top corner. The plate fills the
   chamfers with the panel colour, so the strip reads as one object and
   nothing shows through it. The rail and the accent colour are untouched —
   they are on the tile. */
.title-strip{width:100%;align-items:stretch;background:linear-gradient(180deg,#182742,#0F1A2E)}
.title-tile{min-height:56px;min-width:0;padding:var(--s1) var(--s3) var(--s1) var(--s2);align-items:center}
/* The label is a two-line stack (what it is / which one) and WRAPS rather than
   ellipsising: at 360 each tile is 160 px wide and its text column 96, so
   "BOSS · TEXANS · ANTE 5" on one line (171 px) would be cut to "BOSS · TEX…" —
   losing exactly the information the strip exists to restore. `min-height` (not
   `height`) lets a long team name grow the row rather than be clipped by
   `.pnl`'s clip-path, which crops overflow with no ellipsis of its own. */
.title-tile .t-label{white-space:normal;overflow-wrap:anywhere;line-height:1.2}
/* A `<button>` needs its UA border, centring and colour reset before `.pnl`
   reads as a panel — the same three lines `.team-tile` (section 7) and
   `button.sheet-card` (section 5) carry. The tiles are real buttons so Enter and
   Space activate them natively; `role="button" tabindex="0"` on a div would not,
   and a global key handler could not be added safely (game.js already binds
   Enter to `match.handle("play")` while the match screen's own boss tile has
   focus). The offseason placeholder stays a div: it has nothing to open. */
/* `border-left` is RE-DECLARED, not merely left alone: a bare `border:none`
   here is (0,1,1) and beats /shared's `.pnl{border-left:4px solid var(--blue)}`
   (0,1,0), which killed the rail — the red boss colour override had nothing
   left to paint and the content slid 4 px left. Same shape as `.team-tile`
   (section 7). */
button.title-tile{border:1px solid var(--edge);color:var(--cream);text-align:left;-webkit-appearance:none;appearance:none;cursor:pointer}
/* `.disc` carries no background of its own in /shared — the boss's team colour
   is the one data-driven inline declaration left on this screen (INVENTORY §6),
   and this is the fallback the offseason placeholder uses. */
.title-tile .disc{width:32px;height:32px;background:#33445F}
/* A11Y T3 hub-04: `.disc.on-light` is the glyph flip for the three boss
   discs (the title tile, the boss sheet, the match HUD), where the fill is a
   team colour chosen at runtime rather than a token — screens.js#discPaint
   sets it. The ring is `/shared`'s `inset 0 0 0 3px rgba(255,255,255,.25)`,
   which disappears on exactly the fills light enough to have flipped the
   glyph, so `.on-light` re-polarises it to the same 28 % of `--ink`. */
.disc.on-light{color:var(--ink);box-shadow:inset 0 0 0 3px rgba(10,18,32,.28)}
/* Landscape only (sections 12 and 14) — inert here: `grid-area` on a child of a
   FLEX container is ignored, so the two landscape blocks below only have to
   switch `.title-wrap` to a grid, instead of repeating these assignments twice
   (`.title-user` carries its own, folded into the rule above). A12 allows no third landscape breakpoint to hold them. */
.title-hero{grid-area:hero}
.menu{grid-area:menu}
.title-strip{grid-area:strip}
/* RD71 — the pending-Cut-Day reminder (P4 Task 9): one full-width bar above PLAY, the whole bar
   the tap. A hairline red edge (the boss/danger colour, `.pnl`'s own edge variable) and the red
   GO tag carry it; the sentence is body copy, wrapping rather than ellipsising. */
.cut-banner{width:100%;min-height:var(--ctl-std);align-items:center;--edge:rgba(255,59,59,.7);border:1px solid var(--edge);color:var(--cream);text-align:left;cursor:pointer;-webkit-appearance:none;appearance:none;background:linear-gradient(90deg,#3A1518,#1A0F1C)}
.cut-banner .tag.is-danger{background:var(--red);color:#fff}
.cut-banner .t-body{white-space:normal}
.cut-banner:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
/* A short portrait phone with the Cut Day banner shown (final review cv-2): at 375×667 the title ran
   15 px past `#screen` and put the boss strip under the ticker. Below 700 px of height the title's
   gaps close from 16 to 8 px; nothing else changes size. */
@media (orientation: portrait) and (max-height: 700px){
  .title-wrap{gap:var(--s2)}
}
/* The boss tile's two lines (Final O-B4 on the title): the week's rule names in the boss's red,
   then today's modifier as the match line's gold keycap. Names WRAP rather than ellipsise — the
   tile exists to name them. */
.title-boss .tb-rules{column-gap:var(--s1);row-gap:2px;align-items:baseline}
.title-boss .rule{font:400 13px/1.15 var(--pix);color:#FF8A80;letter-spacing:.03em;white-space:nowrap}
.title-boss .tb-rules i{font-style:normal;color:rgba(255,230,226,.35)}
.title-boss .mod{font-size:var(--t-label);color:var(--ink);background:linear-gradient(180deg,#FFD866,#E8A80F);padding:2px 7px}

/* ==== 9a hub screens · THE DECK (Balatro spec §4.1–§4.2, master §3.4.5; the screen id stays
   `locker`) ====
   Every card the player owns, in five position groups (QB · LINE · RB · WR · TE), the server's
   counts on top, one row per card: the card itself (a button to its sheet) and RELEASE. No cap,
   no salary, no reserve (RD38), so nothing here is a meter or a pane. Nothing below is scoped on
   body.mob / body.port, and no rule of this screen lives in a breakpoint block: the groups flow
   into columns by their own width (`columns`), so landscape is the portrait cut into
   portrait-width columns side by side, every row at the same pixel size (RD26). */

/* ---- the screen ---- */
/* ONE SCROLL at every breakpoint: the panel is content-sized and `#screen` scrolls it (a
   53-card deck is several screens tall; a bounded well inside a panel is the worse trade, the
   locker's fix round 1 ruling kept). (2,0,0) beats section 6's `#screen>.pnl{overflow-y:auto}`
   and section 13's tablet copy of it. */
.deck-root{flex:none}
#screen>.deck-root{overflow:visible}
.deck-root .t-title{margin:0}
/* The counts line: a readout plaque on the blue rail while the deck is legal, the red rail and
   the missing counts under it when it is short. The state is a class (A16); the colour is not
   the only signal — `.deck-needs` says in words what is missing. */
.deck-counts{display:flex;flex-direction:column;gap:var(--s1);padding:var(--s2) var(--s3);background:rgba(8,17,31,.55);box-shadow:inset 0 0 0 1px rgba(30,155,255,.35)}
.deck-counts .t-ui{font-variant-numeric:tabular-nums}
.deck-load-err{justify-content:space-between;align-items:center}
.deck-load-err .t-body{min-width:0}
.deck-counts.is-short{background:rgba(255,59,59,.08);box-shadow:inset 0 0 0 1px rgba(255,59,59,.6)}
.deck-needs .t-label{margin:0}

/* ---- the five groups ---- */
/* 300 px is the narrowest column a row keeps whole at (name ≥ 120, value 52, RELEASE ≈ 96 plus
   gaps): one column on every phone in portrait (296–358 px of panel), two in a landscape phone
   (732) and a portrait tablet, three on a landscape tablet. `break-inside:avoid` keeps a group
   in one column; QB and LINE are short, so the tall RB/WR groups balance around them. */
.deck-groups{columns:300px;column-gap:var(--s4)}
.deck-group{break-inside:avoid;margin:0 0 var(--s3)}
.deck-group-hd{margin:0;min-height:var(--t-head)}
.deck-group-hd .tag{min-width:44px;justify-content:center}

/* ---- one card row ---- */
/* The shared `.lrow` skin (chamfered, recessed) with its own two tracks: the card button takes
   the width, RELEASE its label. The card button is itself two tracks, identity then the value,
   so the value column lines up down a group whatever the name's length. */
.deck-row{grid-template-columns:minmax(0,1fr) auto;gap:var(--s2);min-height:var(--ctl-std);padding:0 var(--s2) 0 var(--s3);cursor:default}
.deck-main{display:grid;grid-template-columns:minmax(0,1fr) 52px;align-items:center;gap:var(--s2);min-width:0;min-height:var(--ctl-std);padding:var(--s1) 0;background:none}
.deck-id{min-width:0;gap:2px}
.deck-name{min-width:0}
.deck-meta{min-width:0;overflow:hidden;flex-wrap:nowrap}
.deck-meta>*{flex:none}
.deck-meta .t-label{letter-spacing:.06em}
.deck-traits{display:inline-flex;gap:2px}
.deck-traits .c-trait{display:grid;place-items:center;width:16px;height:16px;background:rgba(3,8,18,.78);color:var(--gold)}
.deck-traits .c-trait svg{width:11px;height:11px}
.deck-meta .tag{min-height:16px;padding:0 var(--s1);font-size:var(--t-label)}
.deck-abs{background:var(--red);color:#fff}
.deck-fr{background:var(--gold);color:#1E1400}
/* The value: number over its unit, blue YARDS or red MULT (DESIGN-DNA: blue = YARDS, red = MULT). */
.deck-val{display:flex;flex-direction:column;align-items:flex-end;justify-content:center;line-height:1.05}
.deck-val b{font:400 var(--t-ui)/1 var(--pix);font-variant-numeric:tabular-nums}
.deck-val.is-yards b{color:var(--blue)}
.deck-val.is-mult b{color:var(--red)}
.lrow-sub{font:700 var(--t-label)/1 var(--ui);letter-spacing:.06em;color:var(--dim)}
.deck-release{min-width:0;padding:0 var(--s3)}

/* ---- the card sheet (screens.js#cardInfoSheet) ---- */
.deck-box-yards .t-num{color:var(--blue)}
.deck-box-mult .t-num{color:var(--red)}
.deck-sheet p{margin:0}
.deck-sheet-traits .c-trait{display:grid;place-items:center;width:20px;height:20px;background:rgba(3,8,18,.78);color:var(--gold)}
.deck-sheet-traits .c-trait svg{width:14px;height:14px}

/* ---- coaches ---- */
/* The coach row (P4 Task 14, D11, RD50): the equipped coaches, read left to right, each tile
   with its place, its effect line, its level and ◂ ▸ to swap it with its neighbour; the bench
   under it. `auto-fill` instead of a breakpoint (the grid the shipped block used): one column in
   a 326 px phone panel, two or three wherever the panel is wider. A legendary ×MULT coach wears
   the purple rail (DESIGN-DNA: purple = a legendary ×MULT coach), every other one the gold.
   The column floor is 280 px, not 240: a tile carries UPGRADE $200 and its neighbour button side by
   side, and at 240 the four-column 1280 px grid gave each button 104 px for a 107 px label
   (tests/smoke-widths.spec.js). */
.coaches-block{margin-top:var(--s2)}
.coaches-hd{margin:0}
.coach-row,.coach-bench{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--s2);align-content:start}
.coach-tile,.coach-bench-row{padding:var(--s2) var(--s2) var(--s2) var(--s3);--edge:rgba(255,196,45,.4);box-shadow:0 4px 0 var(--line)}
.coach-bench-row{--edge:rgba(62,82,115,.8)}
.coach-tile.is-rare{--edge:var(--purple)}
.coach-name{font-size:var(--t-ui);color:var(--gold)}
.coach-tile.is-rare .coach-name{color:var(--purple)}
.coach-pos{font-variant-numeric:tabular-nums}
.coach-effect{margin:0;min-height:0}
.coach-ctl{flex-wrap:nowrap}
.coach-ctl>*{flex:1 1 0}
.coach-ctl .btn.sm{min-width:0;padding:0 var(--s2)}
.coach-lv{align-items:center}
.ccard-max{min-height:var(--ctl-sm);display:inline-flex;align-items:center;justify-content:center}

#deckView .hint,#deckView .meter{margin-top:0}
.meter.is-over i{background:var(--red)}
/* ---- the in-match DECK VIEW sheet (screens.js#lockerBody's one caller) ----
   The view/filter chips and the list shape left with the v0.2 locker (bêta
   v0.1, B5 Task 5): the sheet is the card grid below. */
/* The mobile thumbnail size, moved out of legacy's `body.mob .grid.zoom .mini`
   with the `locker` area. Shared by the deck view and the market plates —
   /shared's own 116×160 is the desktop value. */
.grid.zoom .mini{width:100px;height:138px;--mw:100px}
/* …and the DECK VIEW's own copy of that grid, which is a different problem
   from the market plates' and the card-reveal sheets' (the three other users
   of the selector above), so it is scoped rather than shared.
   ONE: 100 px was a hard pixel width at every breakpoint, so `.mm` had an
   88 px box whether the viewport was 360 or 1112 — eleven ellipsised names on
   a 820 px tablet ("Amon-Ra St. …" wants 116, "TreVeyon Henderson" 127,
   "Jacory Croskey-Merritt" 146) while the last row left six empty card slots
   of width doing nothing. A fluid `auto-fill` track spends that width, and
   `aspect-ratio` keeps the face's proportions while it does. Not `100cqi`:
   nothing in `#deckView > .scroll > .grid.zoom > .mini` is a container, so
   `cqi` would fall back to the small-viewport size and blow the type up.
   TWO: a wider track alone is not enough (105 px against a 146 px name), so
   the load-bearing half is a two-line clamp (measured on the v0.2 Free Agent
   board, since gone) with the photo band shortened to make
   the room. `bottom` is a PERCENTAGE here, not that board's literal 30px:
   `.mst` sits at `bottom:5%;height:15%`, i.e. its top edge is at 20% of the
   card's height, and a fluid card has no one height to hard-code against.
   THREE: the stat chips are cut by `.mini .mst span`'s own parallelogram
   (`clip-path:polygon(4px 0,…)`) — at this size the diagonal passes through
   the first and last glyph, so PWR painted as "PWF" on every card and the
   tens digit of a two-digit value was shaved to a bare stem ("19 PWR" as
   "|9 PWR"). Same repair as `.draft-card`, `.plate` and `#bidOverlay`: the
   WORD goes, the numbers stay in their colours, and the match screen's own
   "■ PWR ■ CAP" legend names them. */
#deckView .grid.zoom{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));align-content:start}
#deckView .grid.zoom .mini{width:100%;height:auto;aspect-ratio:100/138}
#deckView .grid.zoom .mini .mph{height:40%}
#deckView .grid.zoom .mini .mm{top:auto;bottom:22%;font-size:12px;line-height:1.2;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
#deckView .grid.zoom .mini .mst small{display:none}

/* ==== 9b hub screens · KAITEN MARKET, LEADERBOARD, FRONT OFFICE, SUNDAY LIVE
   (spec §4.4/§4.7/§4.8, SIZING-AUDIT §2.3, LEGACY-MAP §5.11/§5.13/§5.14) ====
   Three legacy areas die here: `market` (94 chunks and 117 lines, the file's
   largest — a whole mouse-only stage set), `ranks` (8) and `sunday` (4). The
   last two were opposite contracts on ONE component: `body.mob.port .rank`
   forced every `.rank` into a wrap-anywhere flex row with `!important`, to
   beat the eight inline `grid-template-columns` its call sites carried, and
   `body.mob.port .rank.sun-row` then forced Sunday Live back out of it with
   four more. `.rank` is a grid here, once, and the call sites ask for it by
   class. Nothing below is scoped on body.mob / body.port. */

/* ---- the shared `.rank` row (leaderboard, Sunday Live, My Bids, the
   Gazette, the playbook, the final screen) ---- */
/* `34px 1fr auto`: the chip that identifies the row, everything nameable,
   everything numeric. `--ctl-std` because these rows ARE tap targets in half
   their consumers, and `tabular-nums` because a score column sitting under a
   per-second clock must not jitter as its digits change. `margin:0` drops
   /shared's `margin-top:4px` — every list that holds these rows is a `.stack`
   with a real gap now, and the two stacked to 4+4. */
.rank{display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:var(--s1) var(--s2);min-height:var(--ctl-std);margin:0;padding:var(--s1) var(--s2);font-size:var(--t-body);font-variant-numeric:tabular-nums}
/* …and the 4 px back, for the lists that are NOT a flex column with a gap of
   their own. Six of the eight `.rank` call sites ARE (`.rk-list`, `.sun-list`,
   the playbook's `.scroll.stack.gap-1`, and the scouting reveal's `.stack.gap-2`
   until that sheet went, 2026-09-24),
   so the separation is the container's; the other three — My Bids' OPEN and
   LAST 24H lists, the Gazette's cut / free-agent lists and the final screen's
   Drives and Biggest-factors panels — drop their rows straight into a `<div>`
   or a `.pnl`, where `margin:0` above left consecutive rows touching and
   reading as one dark slab (measured 0 px at 390). `:not(.stack) >` is what
   keeps it from doubling the gap in the six that already have one. */
:not(.stack) > .rank + .rank{margin-top:var(--s1)}
.rank-pos{min-width:34px;font-size:var(--t-ui);color:var(--dim);text-align:center}
.rank-pos.is-p1{color:var(--gold)} .rank-pos.is-p2{color:#C8D2E4} .rank-pos.is-p3{color:#C88A4A}
/* The middle cell wraps rather than ellipsising the tags off the row: a
   display name is the leaderboard's whole point, and YOU/TIER are 2 chips. */
.rank-id{min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:2px var(--s2)}
.rank-name{min-width:0}
.rank-tags{flex:none}
.rank-num{display:flex;flex-direction:column;align-items:flex-end;justify-content:center;line-height:1.15}
/* The leaderboard's tie-break (A1, 2026-09-23): the run's TD count under its
   score, dim like the finish date it replaced — until the count separated the
   row from another on the same score, where it decided the order and reads
   gold (an equal score on an equal count is the week's draw: no gold). The
   tie itself is told by the equal scores; the colour only points at the
   figure that broke it. */
.rank-tds{letter-spacing:.04em}   /* the label role's .10em pulled "7 TD" apart into "7 T D" */
.rank-tds.is-tie{color:var(--gold)}
/* One tier palette for the leaderboard chip, the Front Office cards and the
   four reward tiles — three call sites that each carried their own `TC` map
   inline. `--tier` is a local variable, not a token: it exists only on the
   element that sets it. */
/* A11Y T3 office-live-09: `--tier` paints the whole card, heading AND body, so
   wood's #6B7C87 put the 14 px promotion sentence at 4.26:1 on `--panel2`
   (3.76 on `--panel`) — the probe read 4.41 — and the player's OWN tier was
   therefore the dimmest card of the four.
   The first lift, #8395A3, overshot. Measured on `22-cutday-office-tabp`, the
   only capture that shows all four rungs, against the (13,23,41) card ground:
   GOLD 11.3, SILVER 11.9, BRONZE 6.23, WOOD 5.92 — wood within 5 % of bronze
   and gold within 5 % of silver, so a four-rung ladder read as TWO PAIRS and
   the big CURRENT TIER WOOD read as a light near-silver rather than the
   bottom of the ladder. #798A98 is the same steel-wood hue, one notch back.
   The ground it is measured against is the one the app paints, and that is
   NOT `--panel`: both tier renderers (screens.js#officeScreen) mount
   `.pnl dark`, whose gradient runs #0E182B → #080F1C, and the leaderboard's
   `.tag.tr-*` puts two black washes on top of that again. Against
   `.pnl dark`'s LIGHTEST stop #0E182B — the worst case, since the heading
   sits at the top of the card — #798A98 reads 4.98 and bronze #C88A4A reads
   6.07: clear of 4.5, and a real step below bronze rather than 5 % under it.
   On the bare `.pnl` gradient (top stop #182742) it would read 4.19 — no
   renderer paints `--tier` TEXT there, only the border-left pairing three
   lines down, and if one ever does the token has to go back to about #8395A3
   (4.83 there). tests/polish.spec.js pins this on resolved pixels.
   No shared token moves. */
.tr-gold{--tier:var(--gold)} .tr-silver{--tier:#C8D2E4} .tr-bronze{--tier:#C88A4A} .tr-wood{--tier:#798A98}
.tr-gold,.tr-silver,.tr-bronze,.tr-wood{color:var(--tier)}
.pnl.tr-gold,.pnl.tr-silver,.pnl.tr-bronze,.pnl.tr-wood{--edge:var(--tier)}
/* The three screens below are one panel that fills `#screen` and scrolls its
   own list, plus a centred empty/error state with a real RETRY control. */
.rk-root,.sun-root,.of-root{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2)}
.rk-empty,.sun-empty,.of-empty{flex:1;align-items:center;justify-content:center;padding:var(--s5) 0;text-align:center}
.rk-list,.sun-list,.of-body{padding-right:2px}
/* The ranks screen's scope segment (DAY · WEEK · SEASON, D13) and the day strip (days 1-7):
   keycaps at the 44 px `.btn.sm` tier, the current one ringed gold like the auth segment and
   `aria-pressed` for the reader. The day strip shares its seven keys across the panel width
   (`flex:1`), so a phone at 320 still fits all seven without a horizontal scroll. A day after
   today is `disabled` — nothing to read there yet — and dims rather than vanishing, so the
   week keeps its shape. */
.rk-scope .btn.is-active,.rk-days .btn.is-active{box-shadow:inset 0 0 0 2px var(--gold);color:var(--gold)}
.rk-days{flex-wrap:nowrap}
.rk-days .btn{flex:1 1 0;min-width:0;padding:0}
.rk-days .btn:disabled{opacity:.4;cursor:default}
/* The podium title (RD58): gold on the tag's black wash — a trophy, not a tier, so it borrows
   the gold of the player's score rather than one of the four tier colours. */
.tag.is-title{color:var(--gold);box-shadow:inset 0 0 0 1px rgba(255,196,45,.5)}
/* The season's standing lines: the closed notice reads gold (the standing is final), the
   double week stays a hint. */
.rk-closed{color:var(--gold)}
#ranksErr:empty{display:none}
/* …and `.of-body` SCROLLS its children instead of crushing them. `.hint`
   carries an explicit `min-height:16px` (/shared), and an explicit min-height
   REPLACES a flex item's `auto` content minimum — so inside this overflowing
   flex column `#cutsLine` shrank to a 16 px box while its text still needed
   two or three lines, and ~24 px of live ink painted outside its own box and
   under the tier grid ("…bel… 24" in EN; in FR the whole of « jamais sous »
   and the number 24 itself, plus an orphan "2" adrift between the GOLD and
   SILVER cards). The box probe cannot see it: the 16 px box overlaps nothing,
   only the ink does. It is the one sentence that states Cut Day's own rule,
   on the screen the product is named after. */
.of-body>*{flex-shrink:0}

/* ---- Kaiten Market (§4.4) ---- */
/* TWO top-level children of `#screen`: the header block and the belt panel
   (screens.js#marketScreen explains why). */
.mk-hd{display:flex;flex-direction:column;gap:var(--s2);flex:none;min-width:0}
.mk-ctl{align-items:center}
/* Title left, live clock right — ONE header shape for the two hub screens
   that carry a clock (the market's next-card countdown and the Front
   Office's tick). The title's `flex:1 1 200px` is what makes the clock drop to its
   own line rather than squeezing the title: 200 + a 170 px `0D 13:58:21`
   does not fit a 326 px phone panel, 200 + a 100 px `30s` does. */
.hub-hd-top>.t-title{flex:1 1 200px;min-width:0}
.of-tick{flex:none}
/* The next-card clock. `flex:none` alone left it at flex-START once the header
   wrapped: `justify-content:space-between` does nothing for a lone item on line
   two, so in French — where `PROCHAINE CARTE DANS` will not share line one with
   a 200 px title — the block fell to the left of a 326 px panel AND stacked its
   label over its value, spending 60 px of a phone's header on two half-empty
   lines. `margin-inline-start:auto` re-anchors it right, and is a no-op while
   the line is unwrapped — so English, where the clock still shares line one
   with the title, renders exactly what it rendered before.
   MEASURED, and NOT done: also turning the block into a label-beside-value ROW
   would make the French case one line instead of two — but it widens the block
   past what can share line one with a `flex:1 1 200px` title, so English LOSES
   a line (header 220 -> 243, belt 480 -> 457) to save French 17 px. The
   wrapped case needs the row and the unwrapped case needs the stack, and CSS
   cannot tell them apart here without a container query; it is a spec question,
   not a declaration. `.of-tick` keeps the old rule — the Front Office's half of
   this is its own repair (docs/FOLLOWUPS.md). */
.of-tick .t-led{line-height:1}
/* The live wire (game.js#feed), reduced to the newest event. It appends and
   trims to four children; a column with `justify-content:flex-end` and one
   line of height overflows off the TOP, so the last child is the visible
   one — the same mechanism the jumbotron's own `.jfeed` used, minus the
   jumbotron. That only holds while a line keeps its own height: a flex item
   shrinks by default, and `overflow:hidden` takes its automatic minimum to 0,
   so from the second event on every line was squeezed into the one-line box
   (5–8 px each) and their glyphs were drawn over each other — the garbled
   LIVE row of the 2026-09-19 iPhone report, on every engine. `flex:none`. */
.mk-feed{display:flex;flex-direction:column;justify-content:flex-end;height:var(--t-ui);overflow:hidden;flex:none}
.mk-feed span{flex:none;font-size:var(--t-label);line-height:1.3;color:#D5DEEB;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;animation:popIn .3s both}
.mk-feed span b{font-family:var(--pix);font-weight:400;letter-spacing:.06em;margin-right:var(--s2)}
/* THE HOUSING. `.pnl`'s blue accent and its 14/16/18 padding framed the stage
   a SECOND time — a bevel inside a bevel — and cost the belt
   20 px of width it needed for the tile pitch: one dark casing, a steel edge
   instead of the blue one, and the deck flush inside it. */
.mk-pnl{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s2);container:mkpnl / inline-size;--edge:#22375A;background:linear-gradient(180deg,#101C31 0%,#0A1525 100%)}
/* With the housing capped (below), a tall window leaves the panel taller than
   the deck. Centre what is left rather than letting it all fall under the
   band: ~85 px of dead panel below the cards read bottom-heavy at 1280x740,
   and the same slack split in two reads as clearance. Scoped to a HORIZONTAL
   stage with `:has()` — a vertical belt has no cap and fills the panel, and
   centring it there would do nothing but risk the phone layout. */
/* THE BELT (continuous-flow spec §3, Q8/Q9) — one moving stage at every
   breakpoint now; the static portrait stack and the landscape-only walk are
   gone. `#belt` takes what is left of the panel, `#plates` is the track laid
   over it, and game.js#beltFrame places each `.plate.mp` with a transform
   computed from SERVER time (never a speed, never a wrap). It also writes
   `#belt[data-belt]` from the belt's OWN measured box: "h" when it is wider
   than tall (a phone or tablet in landscape, a desktop-sized window), "v"
   otherwise (both portraits, the tablet's main column). The plate's shape
   below keys off that attribute, so the stage decides, never a device name —
   and none of it needs a breakpoint.
   THE FLOOR is the length that clears all `--belt-n` rows at the 44 px tap
   floor — n − 1 plates plus n gaps (the fit rule under the row, below); below
   it `.mk-pnl` scrolls rather than crushing the stage. `--belt-n` is the card
   count game.js#beltFrame writes from the envelope (shared/belt.js#beltCards:
   windowSeconds / entryEverySeconds, 9 today); the 9 in `var()` only covers the
   frame before it does. The old floor was five 60 px rows, 300 px: on
   Safari's own portrait viewport (390×664 — its toolbars take the rest of the
   844 px screen) the belt got 326×316, WIDER than tall, so it turned
   horizontal on a portrait iPhone and nine 72 px tiles piled up at a 44 px
   pitch. Every Chromium project is taller (390×844), so none saw it. The
   phone-landscape breakpoint keeps its own floor.
   `container-type:size` lets a plate size itself against the stage (`cqh`/
   `cqw`, below): the plates are absolutely placed, so the stage's own box
   never depends on them. */
/* THE DECK. The stage stops being a flat navy rectangle with a
   bevel and becomes a lit machine: an overhead wash, a deck glow that bleeds up
   from the floor, and the housing's doubled bevel replaced by a real vignette.
   The RAILS and the drifting TIES are the two pseudo-elements below; the
   feathering is a mask on `#plates`. None of it touches a plate's transform —
   game.js#beltFrame is still the only writer of that. */
/* THE HOUSING HUGS THE BAND. `--mp-tw` is solved HERE, not on the plate, for
   one reason: the stage has to know how tall a card will be before it can stop
   stretching. Custom properties inherit, so the plate below still reads the
   same value it used to declare, computed against the same width — `.mk-pnl`'s
   content box is exactly `#belt`'s width, the panel's only padding being its
   own `--s2`.
   Without the cap the stage is `flex:1` and takes whatever the window leaves,
   while the card is capped at 3.6x its width: at 1180x820 that left 110 px of
   bare deck above AND below the band and the screen read empty again — the
   complaint this whole section exists to answer, moved to another viewport.
   Capped, the housing is the band plus one `--s4` of clearance each side at
   every width. Horizontal only: a vertical belt's rows already fill it. */
/* The FLOOR goes with it. `#belt`'s own min-height is the length that clears
   all `--belt-n` rows at the 44 px tap floor — 436 px at n=10 — and that is a
   VERTICAL belt's requirement: a horizontal deck needs one card, not ten rows.
   Left unscoped it beat the cap outright (min-height wins over max-height) and
   the housing stayed 436 whatever the cap said. It stays on the bare `#belt`
   selector, so it still applies while `data-belt` is UNSET — the first paint,
   before game.js#beltFrame has measured the box — which is what keeps a short
   portrait stage tall enough to be read as vertical in the first place
   (Safari's 390x664: a 326x316 stage turned horizontal and piled nine tiles
   at a 44 px pitch). Only a stage already resolved to "h" takes the lower one. */
#belt[data-belt="h"]{--mp-tw:clamp(var(--mp-wmin), calc((100cqw - var(--belt-n, 9) * var(--mp-gap)) / (var(--belt-n, 9) - 1)), var(--mp-w))}
/* THE CAP IS WIDE-STAGE ONLY, and the container query is the whole point of
   this block — not a breakpoint in disguise.
   `beltOrientation` reads the stage's OWN measured box (`W > H`), and the cap
   changes that box: left unguarded it LATCHES. Measured on a 360x780 phone
   that starts in landscape and rotates back to portrait — the path a real
   Galaxy S24 took: the stage is `h` from landscape, so the cap applies; at
   308 px wide `--mp-tw` clamps to its 64 px floor, so the cap is 64*3.6+32 =
   262 and the stage becomes 308x262 — still wider than tall. It can never
   return to `v`, and ten 64 px cards in a 308 px stage overlap by 27 px.
   A fresh portrait load was fine, which is exactly why this survived the
   six-profile sweep: no profile rotates.
   The guard is the PANEL's width, not the viewport's, so it tracks the thing
   the cap actually depends on. 700 px clears every phone in both orientations
   (portrait ~324, landscape ~688) and admits the tablet landscape (~1036) and
   the desktop console (~1136). Below it the base floor stands and the stage
   stays tall enough to be read as vertical — self-correcting, latch gone. */
@container mkpnl (min-width: 700px){
  #belt[data-belt="h"]{min-height:var(--mp-th);max-height:calc(var(--mp-tw) * 2.65 + 2 * var(--s4))}
}
/* The slack the cap leaves is split above and below the band rather than all
   of it falling under the cards (~85 px at 1280x740, which read bottom-heavy).
   OUTSIDE the query on purpose, and it has to be: an element cannot match a
   container query against its OWN container, so `.mk-pnl` inside
   `@container mkpnl` never matches. It is safe out here in a way the cap is
   not — it changes no box, so it cannot feed back into `beltOrientation`. */
.mk-pnl:has(#belt[data-belt="h"]){justify-content:center}
#belt{position:relative;flex:1;min-height:max(calc(var(--mp-h) * 5), calc(var(--ctl-sm) * (var(--belt-n, 9) - 1) + var(--s1) * var(--belt-n, 9)));overflow:hidden;container-type:size;background:radial-gradient(ellipse 78% 44% at 50% 106%,rgba(36,120,210,.20) 0%,transparent 68%),radial-gradient(ellipse 88% 50% at 50% -10%,rgba(135,180,240,.16) 0%,transparent 70%),linear-gradient(180deg,#0B1729 0%,#061120 44%,#03080F 100%);box-shadow:inset 0 0 70px 10px rgba(2,6,13,.85)}
/* The ties: the deck's own tread, drifting the way the cards ride
   (shared/belt.js#beltOffset — h runs right→left, v runs top→bottom) at roughly
   the belt's own speed. Pure furniture, behind every plate, and killed by the
   reduced-motion block in section 5. */
#belt::before{content:'';position:absolute;inset:0;pointer-events:none}
/* The rails and the glass, OVER the plates: two bright edges the band rides
   between, and a vignette that closes the stage at the entry and the exit. */
#belt::after{content:'';position:absolute;inset:0;pointer-events:none}
/* The empty board says so. `#plates` is written by game.js (innerHTML on
   entry, insertAdjacentHTML on each poll), so the message is a SIBLING of it
   and CSS decides: it disappears the moment a plate exists and comes back
   when the last one leaves, with nothing for either code path to remember. */
.mk-none{padding:var(--s5) var(--s3);color:var(--dim)}
#belt:has(#plates .plate) .mk-none{display:none}
#plates{position:absolute;inset:0}
/* ---- entry / exit feathering ----
   A card used to be guillotined by the stage's border at both ends. The mask is
   written in PIXELS, not percentages, so the fade is one half-tile at every
   stage width and never eats a whole row on a short portrait belt. */
#belt[data-belt="h"] #plates{-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 44px,#000 calc(100% - 44px),transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 44px,#000 calc(100% - 44px),transparent 100%)}
#belt[data-belt="v"] #plates{-webkit-mask-image:linear-gradient(180deg,transparent 0,#000 20px,#000 calc(100% - 20px),transparent 100%);mask-image:linear-gradient(180deg,transparent 0,#000 20px,#000 calc(100% - 20px),transparent 100%)}
/* ---- the rails and the tread, per orientation ---- */
#belt[data-belt="h"]::before{background-image:repeating-linear-gradient(108deg,rgba(158,200,255,.34) 0 2px,transparent 2px 13px),repeating-linear-gradient(108deg,rgba(158,200,255,.34) 0 2px,transparent 2px 13px),linear-gradient(180deg,rgba(120,175,240,.10) 0 16px,transparent 16px),linear-gradient(0deg,rgba(120,175,240,.10) 0 16px,transparent 16px);background-size:24px 16px,24px 16px,100% 100%,100% 100%;background-position:0 0,0 100%,0 0,0 0;background-repeat:repeat-x,repeat-x,no-repeat,no-repeat;animation:beltTreadH 1.22s linear infinite}
#belt[data-belt="v"]::before{background-image:repeating-linear-gradient(180deg,rgba(150,195,250,.26) 0 2px,transparent 2px 26px),repeating-linear-gradient(180deg,rgba(150,195,250,.26) 0 2px,transparent 2px 26px);background-size:9px 26px,9px 26px;background-position:0 0,100% 0;background-repeat:repeat-y;animation:beltTreadV 1.22s linear infinite}
@keyframes beltTreadH{to{background-position:-24px 0,-24px 100%,0 0,0 0}}
@keyframes beltTreadV{to{background-position:0 26px,100% 26px}}
#belt[data-belt="h"]::after{background:linear-gradient(90deg,rgba(3,8,15,.96) 0%,rgba(3,8,15,.55) 4%,transparent 11%,transparent 89%,rgba(3,8,15,.55) 96%,rgba(3,8,15,.96) 100%),radial-gradient(ellipse 9% 64% at 0% 50%,rgba(96,178,255,.20),transparent 72%),radial-gradient(ellipse 9% 64% at 100% 50%,rgba(96,178,255,.20),transparent 72%),linear-gradient(180deg,rgba(176,214,255,.62) 0 2px,rgba(6,14,26,.75) 2px 5px,transparent 5px),linear-gradient(0deg,rgba(176,214,255,.62) 0 2px,rgba(6,14,26,.75) 2px 5px,transparent 5px),radial-gradient(ellipse 62% 4px at 50% 100%,rgba(90,175,255,.60),transparent 72%)}
#belt[data-belt="v"]::after{background:linear-gradient(180deg,rgba(3,8,15,.88),transparent 6%,transparent 94%,rgba(3,8,15,.88)),linear-gradient(90deg,rgba(168,208,255,.45) 0 1px,transparent 1px),linear-gradient(270deg,rgba(168,208,255,.45) 0 1px,transparent 1px)}

/* ---- one plate ---- */
/* Two columns: the card face on the left spanning the three text rows, and
   name → price+clock → action stacked beside it, the action full width at
   the standard 48 tier. `.star-badge` and `.stamp` are absolutely positioned,
   so they leave the grid flow entirely. */
.plate{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--s2) var(--s3);align-items:center;padding:var(--s2);background:linear-gradient(180deg,#2A4166,#16243B);box-shadow:inset 0 0 0 2px rgba(255,255,255,.12);clip-path:polygon(0 0,calc(100% - 10px) 0,100% 10px,100% 100%,10px 100%,0 calc(100% - 10px));transition:opacity .45s,box-shadow .2s}
.plate-art{grid-column:1;grid-row:1/4;display:flex;align-items:center;justify-content:center}
/* `.mini{overflow:hidden}` clips its own PWR/CAP badges at `--mw:84`: the
   two of them need ~44 px each and get 35. Drop the WORD, keep the number
   and its colour — the fix legacy.css already carried for this exact card
   (`body.mob.port .plate .plate-art .mini .mst small{display:none}`, area
   `market`) and the one the match card face uses for the same reason. */
.plate .mini .mst small{display:none}
.plate-id{grid-column:2;min-width:0}
/* Two lines, never one ellipsised one: a listing IS the player's name. */
.plate-name{overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.plate-meta{color:var(--dim);white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.plate-price{grid-column:2;display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--s1) var(--s3);min-width:0}
.pricetag{color:var(--gold);white-space:nowrap}
.pricetag.hot{color:#FF9A6B}
.plate-cd{display:flex;align-items:baseline;gap:var(--s2);min-width:0}
.plate-clock{color:#FFB300}
.plate-cd b{font-weight:400;color:var(--dim)}
.plate>button{grid-column:2}
.plate.leading{box-shadow:inset 0 0 0 2px var(--green)}
.plate.outbid{box-shadow:inset 0 0 0 2px var(--red)}
.plate.new{animation:plateIn .6s cubic-bezier(.2,1.3,.3,1) both}
.plate.gone{opacity:0;pointer-events:none}
.star-badge{position:absolute;top:var(--s1);left:var(--s1);font-size:var(--t-body);line-height:1;color:var(--gold);text-shadow:0 0 6px rgba(255,196,45,.85);z-index:3;pointer-events:none}
/* A coach listing has no card face, so its plaque takes the `.mini` footprint
   exactly — otherwise a mixed belt/stack would step in and out on every row. */
.plate .cc{width:84px;height:116px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s1);padding:var(--s2);text-align:center;background:linear-gradient(165deg,#3A2A12,#1A1408);box-shadow:inset 0 0 0 2px var(--gold)}
/* game.js#retire's own overlay — the plate stays in place for 1.3 s under it
   before it fades, which is the only feedback a closed window ever gets. */
.stamp{position:absolute;inset:0;display:grid;place-items:center;gap:var(--s1);padding:var(--s3);background:rgba(3,8,18,.72);text-align:center;font:var(--t-ui)/1.3 var(--pix);letter-spacing:.08em;color:#fff;z-index:3}
.stamp b{display:block;font-size:var(--t-head);color:var(--gold)}
.stamp.taken b{color:var(--danger)}
/* v0.2 Task 6 — the Kaiten belt. A card past its `exitsAt` is off the belt
   but not over: it is only reachable from MY BIDS, and the plate says so by
   going flat and grey rather than disappearing mid-render. */
.plate.is-exited{opacity:.62}
.plate.is-exited:not(.leading):not(.outbid){box-shadow:inset 0 0 0 2px rgba(255,255,255,.08)}
.plate.is-exited .plate-clock{color:var(--dim)}
/* ---- the mini-plate (continuous-flow spec Q9) ----
   A `.plate` for the chrome (panel gradient, chamfers, the leading/outbid
   rings, `.new`/`.gone`, the retire stamp), laid over the track and moved by
   game.js alone. The WHOLE plate is its button (`.mp-hit`): a moving target
   gets every pixel as tap area, ≥ 44 in both axes in both shapes. Default
   shape = the vertical belt's ROW: face · name over position/PWR · money over
   state. */
/* The ROW HEIGHT is derived from the stage, because the pitch cannot be: a card
   enters every `entryEverySeconds` and rides `windowSeconds`, so two
   consecutive plates are always (belt length + plate size) / n apart, n =
   window / entry = `--belt-n`. A plate of size S clears the next one by a gap
   only while S ≤ (length − n·gap) / (n − 1). So the row is the `--mp-h` token
   when the belt is tall enough and shrinks to fit the pitch when it is not,
   never under the 44 px tap floor — which the belt's own floor (above)
   guarantees room for. */
.plate.mp{position:absolute;left:0;top:0;right:0;display:block;height:clamp(var(--ctl-sm), calc((100cqh - var(--belt-n, 9) * var(--s1)) / (var(--belt-n, 9) - 1)), var(--mp-h));padding:0;will-change:transform}
/* Two lines beside the face, each its own flex row, so each text competes only
   with its own neighbour: the NAME with the price (`.mp-top`), the position ·
   PWR · CAP line with the state word (`.mp-bot`). One shared grid column made
   the name give way to the widest state word and ellipsise at 360 px. The
   state words are kept short for the same reason — OUTBID ▸ / SURENCHÉRI ▸,
   not the sheet's OUTBID · RAISE — so position · PWR · CAP keeps its line
   beside the widest of them at 360 px in both languages. */
.mp-hit{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-rows:1fr 1fr;align-items:center;gap:2px var(--s2);width:100%;height:100%;padding:var(--s1) var(--s3) var(--s1) var(--s1);border:0;background:none;color:var(--cream);text-align:left;cursor:pointer;-webkit-appearance:none;appearance:none}
.mp-face{grid-row:1/3;position:relative;display:block;width:clamp(40px, 9cqw, 56px);height:100%;overflow:hidden;background:linear-gradient(180deg,#24395C,#101B2E);box-shadow:inset 0 3px 0 var(--pc)}
.mp-face::after{content:'';position:absolute;left:50%;bottom:-6%;width:86%;height:96%;transform:translateX(-50%);background:radial-gradient(circle at 50% 24%,rgba(255,255,255,.22) 0 19%,transparent 20%),radial-gradient(ellipse 52% 40% at 50% 92%,rgba(255,255,255,.2) 0 60%,transparent 61%)}
.mp-face:has(img)::after{content:none}
.mp-face img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top}
.mp-top,.mp-bot{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s2);min-width:0}
.mp-top{align-self:end}
.mp-bot{align-self:start}
.mp-name{flex:1 1 auto;min-width:0;font:700 var(--t-body)/var(--lh-ui) var(--ui);color:var(--cream);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Position · value (YARDS, or +MULT for a QB or a Line — spec §4.1) on one
   label line. ONE coloured token on this line, and it is the position
   (market-02): a blue YARDS figure beside a blue WR code, or a red MULT beside
   nothing red, was the same collision the old PWR/CAP hues made (#2F8BFF
   against #7FC4FF, 12 px apart). Value and unit split by TONE instead — the
   figure `--cream`, its unit word `--dim` — and the unit word names the chip,
   so colour never has to (WCAG 1.4.1). `font-weight:inherit` keeps the `<b>`
   a hook, not a second weight on `.t-label`'s own 700. */
.mp-meta{flex:1 1 auto;display:flex;align-items:baseline;gap:var(--s1);min-width:0}
.mp-meta .t-label{letter-spacing:.02em;white-space:nowrap}
.mp-meta .mp-pos{color:var(--pc)}
.mp-meta .mp-val{color:var(--dim)}
.mp-meta .mp-val b{color:var(--cream);font-weight:inherit}
.plate.mp .pricetag{flex:none;font:600 var(--t-ui)/1 var(--led)}
/* The money line's word (PRICE / FLOOR, master §3.4.8): the label face at the
   label floor, under the figure's own hue, so the number stays the one thing
   a glance reads. */
.mp-price-k{font:700 var(--t-label)/1 var(--ui);letter-spacing:.04em;opacity:.78}
/* The state word in the condensed LED face at the body size: SURENCHÉRI ·
   RELANCER has to fit an 80 px tile column in two lines, and a 12 px label
   face would need three. */
.mp-state{flex:none;padding:2px var(--s1) 0;background:rgba(255,196,45,.16);color:var(--gold);font:600 var(--t-body)/1.1 var(--led);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap}
.plate.mp.leading .mp-state{background:rgba(61,190,108,.2);color:var(--green)}
.plate.mp.outbid .mp-state{background:rgba(232,72,59,.24);color:var(--danger)}
.plate.mp.is-capped .mp-state{background:rgba(255,255,255,.08);color:var(--dim)}
.plate.mp .star-badge{top:2px;left:2px}
/* The bid sheet is the moment of commitment and the card face is its hero —
   and it was the one card face in the app that was CUT. At the base `--mw:84`
   the name plate had 74 px for a name that wants 109, so the sheet printed
   "Bernhard …" on the card while rendering "BERNHARD RAIMANN" in full in its
   own title 130 px above: the same screen showing a worse version of a string
   it already had right, at the instant the player is about to spend coins.
   The FA board's fix, ported verbatim (section 9a): 104 px of card and two
   12 px lines anchored from the BOTTOM, so the second line grows UPWARD clear
   of the stat chips instead of under them (`.mm` is absolutely positioned with
   no height; anchored from the top its second line lands beneath `.mst`).
   The badge line below STAYS. It was tempting to drop it at 104 — the audit
   offered it as a bonus — but measured at that width a two-digit pair is
   "48 PWR" 48 px and "16 CAP" 43 in an 88 px `.mst`, so the spans overflow
   their own row and each chip ends up exactly as wide as its text, which is
   precisely when `.mini .mst span`'s parallelogram starts eating the first
   and last glyph (the deck view's "PWF" / "|9 PWR", section 9a). The local
   fixture's PWR is single-digit, so the failure is invisible here and the
   numbers are named in full by the meta line beside the card either way. */
#bidOverlay .mini{width:104px;height:144px;--mw:104px}
#bidOverlay .mini .mph{height:40%}
#bidOverlay .mini .mm{top:auto;bottom:22%;font-size:12px;line-height:1.2;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
#bidOverlay .mini .mst small{display:none}
/* The sheet's live clock (`EXITS IN 1:27`, then `RESOLVES IN …`) at the body
   size, not the 12 px of the `.tag` chip it sits in: it is the one number the
   sheet is opened to watch, and as an element with an id it reaches the font
   gate as `b#bidCd` alone — outside the 12 px label allow-list, which reads
   classes. It was 12 px all along and only surfaced once the sheet's own
   mini badges stopped filling the probe's 40-entry `small` list first. The
   LED face is condensed, so the chip grows by ~4 px. */
#bidOverlay #bidCd{font-size:var(--t-body)}
/* …and the horizontal belt's TILE: one column `--mp-w` wide — face, the name on
   up to four lines, position · PWR with CAP wrapping under it, then (after the
   slack) the money and the state. The two flex rows dissolve
   (`display:contents`) so their four children take the tile's own rows. Same
   fit-the-pitch rule as the row, on `cqw`, down to `--mp-wmin`: 88 px at
   852×393 with nine cards, 78 with ten, 67 on Safari's 750×342 landscape.
   Its text stays at the 14 px body floor, so a narrower tile needs more lines
   for a long name: it grows TALLER by twice the width it lost (`--mp-tw`),
   never past the stage — 192 px at 88, 212 at 78, the stage's 214 at 67 — and
   never under `--mp-th`: Chromium resolves `cqh` to 0 when the stage's height
   comes from its min-height (a coach lane taking the free space in phone
   landscape), which without the `max()` collapsed the tile to 0 px. The price
   follows the width (`cqi` — the tile is its own inline-size container)
   between 14 and 16 px. */
/* ======================================================================
   THE CARRIER — what a card looks like on a horizontal deck
   On a horizontal deck the tile used to be a 205 px chip levitating in a
   612 px stage: `--mp-tw` hits its `--mp-w` ceiling at a wide stage, so the
   old `--mp-th + 2*(--mp-w - --mp-tw)` term added nothing and two thirds of
   the machine was void. The tile takes the stage's height, capped at 3.6x its
   own width. That ratio is the whole balance of this card and it was found by
   looking, not by arithmetic: the stage is 515 px tall and the card only 111
   wide, so letting the stage win outright gives a 1:4.35 strip whose `1fr`
   alcove swells into a 300 px floating head — filling the stage, and comic.
   3.6 puts the card at 398, the bust at a portrait crop, and leaves ~60 px of
   lit deck above and below the band, which reads as the machine the belt runs
   in. 3.0 was tried and rejected the same way: the head is right but 107 px of
   empty deck each side brings the original complaint back. The height the card
   does take goes to the PORTRAIT ALCOVE — a lit
   case with the player's bust standing in it and the position code ghosted
   behind, not a 56 px mugshot strip.
   game.js centres the tile on the cross axis ((H - offsetHeight) / 2), so a
   tile this tall also lands hard against both rails: the band IS the belt.
   ====================================================================== */
#belt[data-belt="h"] .plate.mp{right:auto;width:var(--mp-tw);height:clamp(var(--mp-th), calc(100cqh - 2 * var(--s4)), calc(var(--mp-tw) * 3.6));container:mpcard / inline-size;background:linear-gradient(180deg,#2B4772 0%,#1C3052 30%,#132239 72%,#0D1728 100%);box-shadow:inset 0 0 0 1px rgba(255,255,255,.18),inset 0 3px 0 rgba(255,255,255,.16),inset 0 -3px 0 rgba(0,0,0,.6);clip-path:polygon(0 0,calc(100% - 15px) 0,100% 15px,100% 100%,15px 100%,0 calc(100% - 15px));filter:drop-shadow(0 14px 18px rgba(2,6,14,.6))}
/* The carrier's rows. The slack lives in the FACE now, not in a blank band
   between the stats and the money — the alcove is the thing that should grow
   when the stage is tall. */
#belt[data-belt="h"] .mp-hit{grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto auto;grid-template-areas:"face" "price" "state";align-items:start;gap:var(--s1);padding:var(--s1)}
/* ---- THE CARD IS THE GAME'S CARD ----
   The horizontal deck stopped carrying its own anatomy. `screens.js#plate`
   emits `mini(card)` — the same component the locker, the draft board, Free
   Agents, the bid sheet and the ticker all render — and the tile keeps only
   the money line and the action under it. Thirteen rules went with it: the
   alcove, its light cone and position watermark, the name's own wrap, the
   meta grid, the position tab and the star/contested variants of the face.
   It sizes the way `.draft-kept` already sizes: one knob, `--mw`, fed from
   the tile's own width, with the aspect fixed. That fixed aspect is the
   structural win — the housing's height is DERIVED from the card rather than
   tuned to it, so the `3.6` magic number is gone and with it the whole class
   of bug where a cap on the stage fed back into `beltOrientation`.
   HONEST ABOUT WHAT IT DOES NOT DO: it removes no markup. `.mini`'s children
   are absolutely positioned inside a card box, so the name and stats cannot
   come OUT of it to sit beside a 46 px row — which is the belt's shape in
   portrait — and `plate()` emits ONE markup for both arrangements. So the row
   anatomy below stays in the DOM and each orientation hides the other's. The
   reason to do this is that the belt now shows the card the player already
   knows, not that it is less code. */
.mp-card{display:none}
/* The tile sizes to its CONTENT now. It used to be given a height (a clamp on
   the stage, capped at a ratio of its own width) because nothing inside it had
   a shape of its own and the alcove swallowed whatever was left. The card has
   a fixed aspect, so face + money + action IS the height — and a tile still
   forced taller than that left a 60 px hole between the stat badges and the
   price (measured at 780x360). */
#belt[data-belt="h"] .plate.mp{height:auto}
#belt[data-belt="h"] .mp-card{display:block;width:100%;height:auto;aspect-ratio:84/150;--mw:100cqi;grid-area:face;align-self:start}
#belt[data-belt="h"] .mp-face,#belt[data-belt="h"] .mp-name,#belt[data-belt="h"] .mp-meta{display:none}
/* The name WRAPS here and nowhere else. `.mini .mm` is one nowrap line with an
   ellipsis, which is right in a locker row (the card sits beside its own text)
   and fatal on a belt, where the card IS the listing: at 78 px every one of
   the ten names was cut. Two lines, and the card is 84:150 rather than 84:116
   to hold them — the photo gives the band back, because `.mph` ends at 64 % by
   default and a two-line surname landed ON the portrait. */
#belt[data-belt="h"] .mini .mph{height:38%}
#belt[data-belt="h"] .mini .mm{top:56%;white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.1;text-overflow:clip}
/* The stat badges drop their parallelogram. THAT clip is why the unit words
   are hidden on a plate in the first place — at a card's width it eats the
   first glyph ("|9 PWR", the deck view's documented defect) — so squaring it
   off is what lets PWR and CAP be words again instead of being dropped. */
#belt[data-belt="h"] .mini .mst span{clip-path:none}
#belt[data-belt="h"] .mini .mst small{display:inline}
#belt[data-belt="h"] .mini .mst{bottom:4%;height:12%}
/* `.star-badge` and `.mini .mp` shared the top-left corner, so a star listing
   read "L" instead of "OL". The jersey number goes with it: it is the one fact
   the belt never carried and there is no room to introduce it here. */
#belt[data-belt="h"] .mini .mn{display:none}
#belt[data-belt="h"] .mp-top,#belt[data-belt="h"] .mp-bot{display:contents}
/* `.mp-face:has(img)::after` is `content:none` at the row shape (section 9b
   above): on the carrier it comes back as the scrim over the bust. */
/* The name takes the tile's side padding back (the 78 px measurement under the
   row shape still rules), three lines on a carrier instead of four: the alcove
   is worth more than a fourth line of a surname nobody has. */
/* ---- the stat block ----
   Three stacked lines, not one wrapped label row: POSITION as a filled chip in
   its own colour (ink on `--pc` measures 5.6-9.8:1 across the five), then the
   two figures, each `cream` value + `dim` unit. Same rule as the row shape —
   `--pc` is the only hue on this block. */
/* ---- the money ----
   The one gold TEXT on the carrier, on a perforation line that separates the
   card from its stub, and big enough to be read across a moving deck. `.hot`
   (a contested listing) keeps its ember. */
#belt[data-belt="h"] .plate.mp .pricetag{grid-area:price;display:flex;align-items:center;justify-content:center;text-align:center;margin-inline:calc(-1 * var(--s1));padding:2px;border-top:1px dashed rgba(150,195,250,.30);background:repeating-linear-gradient(115deg,rgba(255,255,255,.03) 0 1px,transparent 1px 8px),linear-gradient(180deg,rgba(4,10,20,.62),rgba(4,10,20,.10));color:var(--gold2);font-size:clamp(var(--t-ui), 26cqi, var(--t-num))}
#belt[data-belt="h"] .plate.mp .pricetag.hot{color:#FFB089}
/* On the tile the money word is read out, not drawn: "PLANCHER $150" is wider
   than a 67 px card, a second line would take the card's portrait, and both
   words name the same figure (the price now — the floor while nobody bids).
   The sheet spells both numbers out in full. */
#belt[data-belt="h"] .mp-price-k{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* ---- the action ----
   BID was dim gold on a 16 % gold wash at the tile's foot — lighter than the
   price above it, which is exactly backwards. It is a filled control now, ink
   on gold, at the 44 px tier, and the three other states are filled too so the
   foot of the carrier always reads as the thing you press. */
#belt[data-belt="h"] .mp-state{grid-area:state;display:flex;align-items:center;justify-content:center;font-size:var(--t-body);padding:2px var(--s1) 0;white-space:normal;text-align:center;background:linear-gradient(180deg,var(--gold) 0%,#E39D00 100%);color:var(--ink);box-shadow:inset 0 -2px 0 rgba(0,0,0,.28)}
/* THE CARD'S OWN WIDTH DECIDES ITS CHROME, not the viewport's — `.plate.mp` is
   an inline-size container (`mpcard`), so this asks the only question that
   matters: is there room on THIS card for a control-tier action, whole unit
   words in the stat badges, and a plaque under the price?
   Measured at 78 px (a phone in landscape) none of the three fit: "11 CAP"
   came back as "11 C.", and a 48 px action plus a padded price took a quarter
   of a 208 px card. Above ~95 px all three fit whole. `.mp-state` is not
   itself a tap target and never was — `.mp-hit` is the whole plate, >= 44 in
   both axes at every width — so the tier drops here at no cost to reach. */@container mpcard (min-width: 95px){
  #belt[data-belt="h"] .mp-state{min-height:var(--ctl-std);font-size:var(--t-ui)}
  #belt[data-belt="h"] .plate.mp .pricetag{padding:var(--s2) 2px var(--s1)}
}
/* THE SHORT-STAGE CARD. Its own width again, the other way round — and this is
   the case a real player hit that no profile did: the daily-win banner.
   `#wonBanner` is 46 px of the panel the moment the cap is reached, which takes
   the phone-landscape stage 240 -> 208 and the card 208 -> 192; and the state
   word is `WON TODAY`, two words that wrap where `BID` did not, so the action
   strip goes 17 -> 33. Measured together the portrait fell to **34 px, 18 % of
   the card** — worse than the 27 px this section already fixed once, because
   the fix was measured on an account that had not won anything that day.
   Three changes buy it back without touching a fact or a type floor: the
   grid's own gaps and padding halve (+14), the card's clearance inside the
   stage drops from `--s4` to `--s1` so the short stage is actually used (+8 —
   at 208 px of stage the old term resolved to 176 and the `--mp-th` floor of
   192 was binding instead), and the price gives back the one step it can (+4).
   TRIED AND REVERTED: riding the position tab out of the text column onto the
   foot of the portrait (`.mp-pos{position:absolute;bottom:100%}`) would have
   been +15, the largest of the four. `bottom:100%` of `.mp-meta` is the bottom
   of the NAME, not of the alcove — the name sits directly above it — so the
   tab landed on the surname: "Cooper" with QB painted over "Rush", on every
   card. `.mp-pos` is a grandchild of the grid, so it cannot be placed in the
   face's own area either, and reparenting it is a renderer change for a case
   that is already legible at 31 %. */
@container mpcard (max-width: 94px){
  #belt[data-belt="h"] .mp-hit{gap:2px;padding:2px}
  /* …and the unit words go again where the badge cannot hold them whole. At
     78 px "11 CAP" came back as "11 C." even unclipped — the same guillotine
     the shared rule was written for; above ~95 px both words fit. */
  #belt[data-belt="h"] .mini .mst small{display:none}
  #belt[data-belt="h"] .plate.mp .pricetag{font-size:clamp(var(--t-body), 24cqi, var(--t-ui))}
}
#belt[data-belt="h"] .plate.mp.leading .mp-state{background:linear-gradient(180deg,var(--green) 0%,#25A04D 100%);color:var(--ink)}
#belt[data-belt="h"] .plate.mp.outbid .mp-state{background:linear-gradient(180deg,#FF6A5C 0%,#C22A1E 100%);color:#fff}
#belt[data-belt="h"] .plate.mp.is-capped .mp-state{background:linear-gradient(180deg,#42536F 0%,#2A374E 100%);color:#E2E9F4}
/* ---- differentiation ----
   A contested $140 star and a $10 filler used to differ by two words of colour.
   Two composable marks, both from data already on the plate: a STAR listing
   (its floor is ten times its neighbours') gets a gold carrier and a gold tab,
   and a CONTESTED one (`.pricetag.hot` = somebody has bid) gets an ember foot
   glow. A plate can be both, and the star's frame wins the edge. */
#belt[data-belt="h"] .plate.mp:has(.pricetag.hot){background:linear-gradient(180deg,#3A3352 0%,#2A2440 30%,#161E36 72%,#0D1728 100%);box-shadow:inset 0 0 0 1px rgba(255,154,107,.35),inset 0 2px 0 rgba(255,180,140,.35),inset 0 -2px 0 rgba(0,0,0,.55),inset 0 -70px 60px -60px rgba(255,120,60,.55)}
#belt[data-belt="h"] .plate.mp:has(.star-badge){background:linear-gradient(180deg,#4A3A16 0%,#33280F 28%,#18213A 70%,#0D1728 100%);box-shadow:inset 0 0 0 1px rgba(255,196,45,.55),inset 0 2px 0 var(--gold),inset 0 -2px 0 rgba(0,0,0,.55);filter:drop-shadow(0 12px 18px rgba(2,6,14,.6)) drop-shadow(0 0 10px rgba(255,196,45,.22))}
/* The star is a corner TAB on the carrier, not a floating glyph — and it keeps
   the 14 px body size it has everywhere else: the font gate reads it as prose
   (it is a `div.star-badge`, on no 12 px label tier), and a 12 px star failed
   `gate-mobile --strict` at land and tabl for exactly that reason. */
#belt[data-belt="h"] .plate.mp .star-badge{top:0;left:auto;right:0;display:flex;align-items:center;justify-content:center;width:var(--s5);height:var(--s5);background:linear-gradient(180deg,var(--gold),#E09A00);color:var(--ink);font-size:var(--t-body);text-shadow:none}
/* The leading / outbid rings still win the edge — they are the viewer's own
   state, which outranks what the listing is worth. */
#belt[data-belt="h"] .plate.mp.leading{box-shadow:inset 0 0 0 2px var(--green),inset 0 -2px 0 rgba(0,0,0,.55)}
#belt[data-belt="h"] .plate.mp.outbid{box-shadow:inset 0 0 0 2px var(--red),inset 0 -2px 0 rgba(0,0,0,.55)}
/* The weekly coach lane (API.md §3.7 D9): its own block under the belt,
   never on it, so a coach never takes one of the 8 player slots. Same
   `#plates` grid, so a coach tile is exactly a belt tile at every width. */
.coach-block{display:flex;flex-direction:column;gap:var(--s2);flex:none;min-width:0}
.coach-lane{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:var(--s2);align-content:start}
/* A MY BIDS row that is past its exit carries a second line under the row
   grid — the "you can still raise" line, which is the only place that fact
   is written once the card is off the belt. */
.mybid-row{display:flex;flex-direction:column}
.bid-note{color:var(--gold);padding:0 var(--s2) var(--s1)}
/* …and, when a raise or a withdraw driven FROM a My Bids row is refused,
   that row's own error line. `#bidErr` only exists inside the bid sheet, so
   a My Bids-side refusal had nowhere to land and failed silently. Empty
   until it has something to say, so it costs the resting sheet no height.
   Declared after `.bid-note` for the same reason that rule is: equal
   specificity, later wins, and these two never share an element. */
.bid-err{color:var(--danger);padding:0 var(--s2) var(--s1)}
.bid-err:empty{display:none}
/* The bid sheet's amount row: the input and its two quick-raise buttons sit
   on their baselines, not centred on the tallest of them (the input is 48
   with a label above it, the buttons 44). */
.bid-row{align-items:flex-end}
/* The proxy bid's two numbers (master §3.4.8): the current price is the one
   that moves, so it carries the money hue; the floor stays neutral. */
.bid-prices .bid-now{color:var(--gold2)}
.bid-min{color:var(--dim);flex:none;white-space:nowrap}
.bid-amt-k{white-space:nowrap}
.bid-foot{align-items:baseline}
/* The release picker (RD39, RF 5): at 53 cards a bid names the card a win
   releases. One 44 px row per deck card — position tag, name (and, on the
   last QB or Line, the reason it cannot go), the card's value — in a list
   that scrolls on its own so PLACE BID never leaves the sheet. The picked row
   carries the gold rail and tint: it is the price of the bid. */
.bid-rel-list{display:flex;flex-direction:column;gap:2px;max-height:min(40dvh,264px);overflow-y:auto;overscroll-behavior:contain;padding-right:2px}
.bid-rel-row{flex:none;display:grid;grid-template-columns:44px minmax(0,1fr) auto;align-items:center;gap:var(--s2);min-height:var(--ctl-sm);padding:var(--s1) var(--s2);border:0;background:rgba(255,255,255,.04);box-shadow:inset 3px 0 0 transparent;color:var(--cream);text-align:left;cursor:pointer;-webkit-appearance:none;appearance:none}
.bid-rel-row:hover:not(:disabled){background:rgba(255,255,255,.08)}
.bid-rel-row:focus-visible{outline:2px solid var(--gold);outline-offset:-2px}
.bid-rel-row.is-active{background:rgba(255,196,45,.14);box-shadow:inset 0 0 0 1px var(--gold)}
.bid-rel-row:disabled{cursor:not-allowed;opacity:.62}
.bid-rel-id{min-width:0}
.bid-rel-named{color:var(--dim)}

/* ---- the console: the market's header block ----
   Gold used to mean five different things on this screen at once — the clock,
   the MY BIDS count, the floor, BID and the star. It means exactly two now:
   MONEY (the floor, `--gold2`) and the ACTION (BID, ink on gold). The clock is
   a readout in its own LED well; the MY BIDS count is cream until it is full,
   where it goes red because that is a real refusal. (One rule: an identical
   `.bid-cap` block used to sit up in the bid-sheet section too, and this one
   — later in the file, same specificity — silently won. The dead one is
   gone.)
   THE CONTROL ROW IS ONE FAMILY. MY BIDS and the daily-win state are the same
   kind of thing — a label and the count that qualifies it — so they get the
   same chip: flat, tinted, the figure in cream inside it. MY BIDS stays a real
   `button` at the `--ctl-sm` tier, which is why the two are matched by giving
   the SPANS the button's height rather than by giving the button the span's:
   44 px is the tap floor and a chip cannot go under it. */
.bid-cap{margin-left:2px;background:#41577A;color:var(--cream)}
.bid-cap.is-full{background:var(--red);color:#fff}
/* The live wire gets a window rather than a bare line of text, and the rules
   note a quiet card: the two of them plus the console are what the 320 px side
   column had to say all along, it just had no shape. */
.mk-feed{padding:0 var(--s2);background:rgba(4,10,20,.6);box-shadow:inset 0 0 0 1px rgba(150,195,250,.10)}

/* ---- Sunday Live (§4.8) ---- */
/* `34px 1fr auto auto`, four cells for what used to be five fixed tracks
   (34 + 1fr + 3×90 = 304 px of columns before the name got any, on a 326 px
   panel). The two weekly PWR figures share the third cell. */
.sun-row{grid-template-columns:34px minmax(0,1fr) auto auto;animation:popIn .3s both}
.sun-pos{width:34px;height:24px}
.sun-id{min-width:0;display:flex;flex-direction:column;justify-content:center}
/* The name WRAPS (two lines, clamped) instead of ellipsising. Measured at
   390: the name cell is 121-123 px and every NFL name on a starter roster
   wants 126-161 — one ellipsised line loses 11-40 px of exactly the thing
   the row identifies. Two lines cost the LONG rows 5 px of height (53) and
   leave every short one at the 48 tier, the same trade `.lrow-name` made in
   the locker. `overflow-wrap:anywhere` covers the unbreakable surname. */
.sun-name{min-width:0;line-height:1.15;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* `.is-played` — this player's team has kicked off this week. The dot is the
   row's own live light; the delta cell reads DNP without it. Inline (not a
   flex item) so it rides inside the clamped name box. */
.sun-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--steel);margin-right:var(--s1);vertical-align:middle}
.sun-row.is-played .sun-dot{background:var(--red);box-shadow:0 0 8px var(--red)}
/* Bêta v0.1 (API.md §3.14): « WED: 5 → 6 », this boss week's frozen value and the projection
   for the next tick, beside the team; green when it rises, red when it falls (a comparison of
   the two wire numbers, nothing more). A `belt`/`bid` row carries its scope tag. */
.sun-meta{min-width:0;flex-wrap:wrap;align-items:center}
.sun-yards{font-variant-numeric:tabular-nums;white-space:nowrap}
.sun-yards.is-up{color:var(--green)}
.sun-yards.is-down{color:var(--red)}
.sun-scope{background:var(--gold);color:#1E1400}
.sun-stat{display:flex;flex-direction:column;align-items:flex-end;line-height:1.3;white-space:nowrap}
.sun-delta{min-width:var(--ctl-sm);text-align:right;font-variant-numeric:tabular-nums}
.sun-status{vertical-align:middle;background:var(--steel);color:#05080F}
/* A11Y T3 office-live-07: white on `--red` measured 3.53:1 at 12 px. `--ink`
   on the same red reads 5.30:1 — the badge language the app uses everywhere
   else (`.btn.gold`, `.btn.green`, `.box.gold`) — and `--red` is untouched.
   NB for whoever picks up the sibling: the finding cited the steel default
   above as the example to follow, but #05080F on `--steel` measures 2.54:1,
   so that one is a failure of its own. It is a different repair (lighten the
   ink on a dark fill, not darken it on a light one) and no T3 finding covers
   it, so it is left standing and reported rather than smuggled in here. */
.sun-status.is-live{background:var(--red);color:var(--ink);animation:blink 1.4s infinite}

/* ---- Cut Day at connection (P4 Task 11 — spec §4.4, RD14/RD18/RD31/RD44/RD45/RD48) ----
   The keep: a grid of the deck's card faces at the game's 70 × 100 (RD27 — `--cw/--ch`
   pinned HERE, on the wrap, so nothing the match writes at runtime can resize them), gold
   edge + KEEP badge on the kept ones, the Franchise card green-edged in its 9th place,
   every card leaving at this Cut Day dimmed so the release is legible before CONFIRM.
   `#screen` is the scrollport on a phone (section 6); the wrap is content-sized. */
body:has(.keep-wrap) #lights .turf,body:has(.keep-first-wrap) #lights .turf{opacity:.25}
.keep-wrap,.keep-first-wrap{--cw:70px;--ch:100px;flex:none;display:grid;gap:var(--s3);width:100%;max-width:1120px;margin:0 auto}
.keep-wrap>*{min-width:0}
.keep-wrap .hint{margin-top:0}
.keep-hd .pips{flex:1 1 auto;max-width:calc(var(--s5) * 8);margin-top:0}
/* CONFIRM spans the panel (its label never clips), LATER is a text action on its own line. */
.keep-hd .keep-confirm{min-height:var(--ctl-std)}
.keep-grid{display:grid;grid-template-columns:repeat(auto-fill,var(--cw));gap:var(--s3) var(--s2);justify-content:center;padding-top:var(--s2)}
.keep-card .face{transition:box-shadow .16s ease,filter .16s ease,opacity .16s ease}
.keep-card:not(.is-kept):not(.is-franchise) .face{filter:grayscale(.6) brightness(.62)}
.keep-card.is-kept .face{box-shadow:inset 0 0 0 3px var(--gold),0 0 18px rgba(255,196,45,.45),0 8px 16px rgba(0,0,0,.55)}
.keep-card.is-franchise .face{box-shadow:inset 0 0 0 3px var(--green),0 0 18px rgba(75,227,111,.45),0 8px 16px rgba(0,0,0,.55)}
.keep-card:focus-visible{outline:2px solid var(--cream);outline-offset:3px}
.keep-badge,.keep-abs{position:absolute;z-index:2;font-family:var(--pix);font-size:var(--t-label);line-height:1;padding:3px 5px;clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%);pointer-events:none}
.keep-badge{top:31%;right:-4px;background:var(--gold);color:#1E1400}
.keep-badge.is-fr{background:var(--green);color:#062012}
.keep-abs{left:-4px;top:18%;background:var(--red);color:#fff}
/* THE HUB'S ONE CARD FACE (cards.js#faceHTML in flow — finish review item 8): the keep grid,
   the draft and redraft boards and kept pile, the Kaiten bid sheet. The match mock's anatomy:
   the position tab with its team code, the photo, the surname in caps, the skewed value chip
   (YARDS blue, a QB's or a Line's +MULT red) and, on a card that tires, the four fatigue pips.
   `.card` is an absolutely-placed flying card in /shared (the match layer); a `.bface` is in
   flow, `--cw` × `--ch` by default and fluid where a grid sizes it. It is its own inline-size
   container, so the surname's size follows the card: the largest that fits its `--nl`
   characters (cards.js writes it), 14 px at most and never under the 12 px label floor. */
.card.bface{position:relative;left:auto;top:auto;display:block;width:var(--cw,70px);height:var(--ch,100px);padding:0;border:0;background:none;font:inherit;color:inherit;will-change:auto;container-type:inline-size;flex:none;pointer-events:auto}
/* The mock's own flow, not `/shared`'s absolute anatomy: tab, photo (it takes what is left),
   surname, foot — so the value chip can never ride up onto the surname, at any card height. */
.bface .face{display:flex;flex-direction:column}
.bface .c-tab{position:relative;flex:none;height:18px;display:flex;align-items:center;justify-content:space-between;gap:2px;padding:0 6%;clip-path:polygon(0 0,100% 0,100% 72%,0 100%)}
.bface .c-pos,.bface .c-team{position:static;font-size:var(--t-label);line-height:1}
.bface .c-pos{font-family:var(--pix)}
.bface .c-team{font-style:normal;font-weight:700;color:rgba(255,255,255,.85);letter-spacing:.02em}
.bface .c-photo{position:relative;flex:1 1 auto;min-height:0;top:auto;left:auto;right:auto;height:auto;margin:2px 6% 0}
/* A Line's photo slot is its team (owner, 2026-09-28): the trigram in the value face (Teko), in
   the LINE position gold (#D9B36C on the slot's navy, about 9:1), on a gold hairline. Sized from
   the card's width (it is the inline-size container), whole at the 70-px keep grid and hand. */
.bface .c-photo.is-line{display:grid;place-items:center;box-shadow:inset 0 0 0 1px rgba(217,179,108,.55)}
.bface .c-line-team{font:700 min(40px, 38cqi)/.8 var(--led);letter-spacing:.02em;padding-top:.12em;color:#D9B36C;text-shadow:0 1px 0 rgba(0,0,0,.5);white-space:nowrap}
.bface .c-traits{position:absolute;right:2px;bottom:2px;display:flex;gap:1px}
.bface .c-trait{display:grid;place-items:center;width:14px;height:14px;background:linear-gradient(180deg,#FFE08A,var(--gold));color:#1E1400;clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%)}
.bface .c-trait svg{width:9px;height:9px}
.bface .c-name{position:relative;flex:none;top:auto;left:auto;right:auto;padding:4px 5% 0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font:600 max(var(--t-label), min(14px, calc(90cqi / (var(--nl, 8) * .64))))/1.1 var(--ui);letter-spacing:-.02em;text-transform:uppercase;color:var(--cream)}
.bface .c-name .full{display:none}
.bface .c-name .short{display:inline}
.bface .c-foot{position:relative;flex:none;display:flex;align-items:flex-end;justify-content:space-between;gap:3px;padding:4px 5% 5px}
.bface .c-yards,.bface .c-mult{display:grid;place-items:center;min-width:30px;height:24px;padding:3px 5px 0;font-family:var(--led);font-weight:700;font-size:24px;line-height:.8;color:#fff;text-shadow:0 1px 0 rgba(0,0,0,.4);font-variant-numeric:tabular-nums;clip-path:polygon(5px 0,100% 0,calc(100% - 5px) 100%,0 100%);background:linear-gradient(180deg,#2C8FE8,#0F5AA8)}
.bface .c-mult{background:linear-gradient(180deg,#FF5E5E,#B81E1E)}
.bface .c-mult small{display:none}
.bface .c-pips{display:flex;gap:2px;margin:0 0 1px;flex:none}
.bface .c-pips i{flex:none;width:5px;height:9px;background:#18243C;box-shadow:inset 0 0 0 1px rgba(147,163,188,.75);clip-path:polygon(2px 0,100% 0,calc(100% - 2px) 100%,0 100%)}
.bface .c-pips i.on{background:var(--green);box-shadow:none}
/* The Franchise place: one chip per eligible card, plus NONE. */
.keep-fr-chip{min-height:var(--ctl-sm)}
.keep-fr-chip.is-active{background:linear-gradient(180deg,#5FE88A,#1B8F3A);color:#062012}
/* The three-line first screen (RD45): one centred panel, read top to bottom. */
.keep-first{max-width:520px;width:100%;margin:0 auto}
.keep-first-list{margin:0;padding-left:1.4em}
.keep-first-list li::marker{font-family:var(--pix);color:var(--gold)}
/* The report's three piles (`.gz-kept/.gz-released/.gz-redraft`): mini-cards in a wrapping
   grid, the Franchise tag under its card. */
.gz-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:var(--s2)}
.gz-card{display:grid;gap:var(--s1);justify-items:center;min-width:0}
.gz-card .mini{width:100%;height:auto;aspect-ratio:84/116;--mw:64px}
.gz-card .mini .mst small{display:none}
.gazette .gz-fr{background:var(--green);color:#062012}
.gazette .gz-season{background:#101512;color:var(--gold)}
/* A pending Cut Day on the Front Office (RD71). */
.of-pending{box-shadow:inset 0 0 0 2px var(--red)}
.of-reward.of-season b{color:var(--gold)}
/* A signature recipe's name is wider than a coins figure: the row wraps and the value keeps
   the right edge instead of pushing the label onto two broken lines. */
.of-reward{flex-wrap:wrap;row-gap:0}
.of-reward>span{white-space:nowrap}
.of-reward>b{margin-left:auto}

/* ---- Front Office (§4.7) ---- */
/* Both blocks are `auto-fit` grids: the day-0 `flex:1 1 220px` / `1 1 150px`
   rows put two cards on the first line and left the rest on a short second
   one at every width. `auto-fit` and not `auto-fill` because the item count
   is FIXED here (3 standings, 4 tiers) — auto-fill would leave the four tier
   cards at 130 px each beside three empty tracks in a 1004 px tablet column.
   Cards stretch, so a row of them shares one height. */
.of-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--s2);align-items:stretch}
.of-card{display:flex;flex-direction:column;gap:var(--s1);padding:var(--s3) var(--s4) var(--s3) var(--s3)}
.of-num{line-height:1}
/* The unread report's CTA pins to the bottom of its card whatever the two
   beside it need. */
.of-card .btn{margin-top:auto}
.of-tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:var(--s2);align-items:stretch}
.of-tier{display:flex;flex-direction:column;gap:var(--s1);padding:var(--s2) var(--s3) var(--s2) var(--s2);box-shadow:inset 0 0 0 1px rgba(255,255,255,.05)}
.of-tier.is-current{box-shadow:inset 0 0 0 2px var(--gold)}
.of-tier.is-next{box-shadow:inset 0 0 0 2px rgba(255,255,255,.35)}
/* What a tier pays (shared/tiers.js#tierRewards, 2026-09-24): coins, coach
   roll, signature plays — one label/value row each, the value right-aligned so
   the four cards read as one table. A reward the tier does not pay stays on
   its row in the dim ink: "nothing" is part of the ladder. */
.of-rewards{gap:2px}
.of-reward{gap:var(--s2);align-items:baseline}
.of-reward b{font-variant-numeric:tabular-nums;white-space:nowrap}
.of-reward.is-zero,.of-reward.is-zero b{color:var(--dim)}


/* ==== 10 match — the Balatro match screen (P4 Task 7; spec §5.2; the approved O-B4 mock,
   docs/mockups/2026-09-27-match-screen/, Final O-B4 and RD81) ====
   ONE markup (app/m/match.js#renderAll), two columns:
     .m-col-a   top bar · field strip · boss line · coach row · starters · the counter
     .m-col-b   pile · the 2×4 hand · the "only at their moment" row · the action row
   Portrait stacks them; every landscape block below sets them side by side at 390 px each
   (RD26: the portrait cut in two — every component the same pixel size, only vertical
   whitespace moves). Cards are 70 × 100 in both (RD27). Colour is the DNA's: gold = the score
   and the selection, blue = YARDS, red = MULT and the boss, purple = a ×MULT coach, orange =
   the starting QB. The column is 390 wide with an 8 px gutter, so every component measures
   the same 374 px of content on any phone. */
#screen.match{--cw:70px;--ch:100px;display:flex;flex-direction:column;gap:10px;padding:8px 0 10px;overflow:hidden}
.m-col{display:flex;flex-direction:column;gap:10px;min-height:0;width:min(390px,100%);margin-inline:auto;padding:0 var(--s2)}
.m-col-b{flex:1}

/* ---- the top bar: menu · down & distance over the spot · the clock · the score ---- */
.m-top{display:grid;grid-template-columns:44px minmax(0,1fr) auto auto;align-items:center;gap:var(--s2);height:52px;flex:none}
.m-top .m-menu{width:44px;height:44px}
.m-top .sit{display:flex;flex-direction:column;justify-content:center;min-width:0;line-height:1}
.m-top .sit-down{font:700 30px/.8 var(--led);color:#fff;letter-spacing:.01em;white-space:nowrap;text-transform:uppercase}
.m-top .sit-spot{margin-top:4px;letter-spacing:.08em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.m-top .clock{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 var(--s2);background:linear-gradient(180deg,#FFD866,#E8A80F);color:#1E1400;clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%);box-shadow:0 4px 12px rgba(255,196,45,.25)}
.m-top .clock .q{font:var(--t-label)/1 var(--pix)}
.m-top .clock .t{font:700 28px/.8 var(--led);padding-top:4px;letter-spacing:.02em;font-variant-numeric:tabular-nums}
.m-top .clock .tag{font:var(--t-label)/1 var(--pix);background:#1E1400;color:var(--gold);padding:4px 5px 3px}
/* The score reads as the score (Task L2: a lone gold "0" beside the clock read as a stray box): its
   label over the figure, right-aligned, the gold the DNA keeps for the player's score. */
.m-top .score{display:flex;flex-direction:column;align-items:flex-end;justify-content:center;gap:3px;min-width:48px;line-height:1}
.m-top .score .k{font:700 var(--t-label)/1 var(--ui);letter-spacing:.08em;color:var(--dim)}
.m-top .score .n{margin:0;padding-top:3px;font:700 32px/.8 var(--led);color:var(--gold);text-shadow:0 0 18px rgba(255,196,45,.35);font-variant-numeric:tabular-nums}

/* ---- the field strip: own end zone · 100 yards · the goal (a 40 px ruler, not a stage) ----
   Positions travel as the unitless `--x` (0–100 along the turf, match.js never writes a size). */
/* The strip draws 40 px (the mock) inside a 44 px hit box: 2 px of margin given back top and bottom,
   and the clip-path trims the same 2 px off the painted band (Task 17 walk: the tap gate read 40). */
.m-field{position:relative;display:grid;grid-template-columns:22px minmax(0,1fr) 30px;flex:none;height:44px;margin:-2px 0;width:100%;padding:0;border:0;background:none;color:inherit;cursor:pointer;clip-path:polygon(0 2px,100% 2px,100% calc(100% - 2px),8px calc(100% - 2px),0 calc(100% - 10px));box-shadow:0 6px 14px rgba(0,0,0,.45),inset 0 0 0 1px rgba(255,255,255,.08)}
.m-field .ez{background:repeating-linear-gradient(135deg,#123E24 0 5px,#0F3620 5px 10px)}
.m-field .ez.td{background:linear-gradient(180deg,#FFD866,#E09A10)}
.m-field .turf{position:relative;overflow:hidden;background:repeating-linear-gradient(90deg,#1E6B3A 0 10%,#175A30 10% 20%)}
.m-field .lines{position:absolute;inset:0;background:linear-gradient(90deg,rgba(255,255,255,.5) 1px,transparent 1px) 0 0/10% 100%,linear-gradient(90deg,rgba(255,255,255,.2) 1px,transparent 1px) 5% 0/10% 100%}
.m-field .hash{position:absolute;left:0;right:0;height:4px;background:linear-gradient(90deg,rgba(255,255,255,.35) 1px,transparent 1px) 0 0/2% 100%}
.m-field .hash.t{top:7px}.m-field .hash.b{bottom:7px}
.m-field .yd,.m-field .mark,.m-field .ball{left:calc(var(--x) * 1%)}
.m-field .yd{position:absolute;top:50%;transform:translate(-50%,-50%);font:var(--t-label)/1 var(--pix);color:var(--cream);text-shadow:0 2px 0 rgba(0,0,0,.45);pointer-events:none}
#screen .m-field .plate{position:absolute;left:6px;top:50%;z-index:1;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 var(--s2);transform:translateY(-50%);font:var(--t-label)/1 var(--pix);letter-spacing:.04em;color:var(--dim);white-space:nowrap;background:rgba(4,9,20,.9);clip-path:polygon(5px 0,100% 0,calc(100% - 5px) 100%,0 100%);box-shadow:inset 0 0 0 1px rgba(255,255,255,.14);transition:none}
.m-field .plate b{font:700 20px/.8 var(--led);color:#fff;padding-top:3px}
.m-field .plate .x{font:var(--t-label)/1 var(--pix);color:var(--gold);margin-left:2px}
.m-field .plate .x:empty{display:none}
/* The plate keeps clear of the ball: in the own half it sits at the turf's far end. */
#screen .m-field .plate.is-right{left:auto;right:6px}
.m-field .mark{position:absolute;top:0;bottom:0;width:3px;transform:translateX(-50%)}
.m-field .mark.los{background:var(--blue);box-shadow:0 0 8px rgba(30,155,255,.9)}
.m-field .mark.fd{background:var(--gold);box-shadow:0 0 8px rgba(255,196,45,.9)}
.m-field .ball{position:absolute;top:50%;z-index:2;width:20px;height:12px;border-radius:50%;transform:translate(-50%,-50%);background:radial-gradient(ellipse at 40% 35%,#B8663A,#6E3418 70%);box-shadow:0 2px 4px rgba(0,0,0,.6),inset 0 0 0 1px rgba(255,255,255,.25);transition:left .7s cubic-bezier(.16,1,.3,1)}
.m-field .ball::after{content:'';position:absolute;left:35%;right:35%;top:48%;height:1.5px;background:#fff;opacity:.8}

/* ---- the boss line: the shield, the rule NAMES (a one-rule week keeps its text), the modifier tag ---- */
.m-boss{display:grid;grid-template-columns:34px minmax(0,1fr);grid-template-rows:auto auto;column-gap:10px;row-gap:2px;align-items:center;flex:none;width:100%;height:56px;padding:4px 10px 4px 6px;border:0;text-align:left;color:inherit;cursor:pointer;background:linear-gradient(90deg,#2A1522,#150D1C 70%,#120A18);clip-path:polygon(0 0,calc(100% - 10px) 0,100% 10px,100% 100%,0 100%);box-shadow:inset 0 0 0 1px rgba(255,59,59,.35),inset 0 1px 0 rgba(255,255,255,.06)}
.m-boss .shield{grid-row:1 / span 2;width:34px;height:34px;display:grid;place-items:center;font:var(--t-label)/1 var(--pix);color:#fff;background:linear-gradient(180deg,#8E2222,#4A0F14);clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);box-shadow:inset 0 0 0 3px rgba(255,255,255,.22)}
.m-boss .rules{display:flex;align-items:baseline;gap:var(--s3);min-width:0;white-space:nowrap;overflow:hidden}
.m-boss .rule{display:flex;align-items:baseline;gap:var(--s2);min-width:0}
.m-boss .rule b{flex:none;font:400 13px/1.1 var(--pix);color:#FF8A80;letter-spacing:.03em}
.m-boss .rule span{min-width:0;overflow:hidden;text-overflow:ellipsis;font:600 var(--t-label)/1.1 var(--ui);color:#FFE6E2}
.m-boss .rules i{font-style:normal;color:rgba(255,230,226,.35)}
.m-boss .side{display:flex;align-items:center;gap:6px;min-width:0}
.m-boss .mod{font:var(--t-label)/1 var(--pix);letter-spacing:.06em;color:var(--ink);background:linear-gradient(180deg,#FFD866,#E8A80F);padding:3px 6px 2px;clip-path:polygon(3px 0,100% 0,calc(100% - 3px) 100%,0 100%);white-space:nowrap}

/* ---- the coach row: five equal tiles read left to right (RD50); a legendary ×MULT coach
   wears the purple edge; a coach that contributes rises and glows while a play resolves ---- */
.m-jokers{position:relative;z-index:2;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:var(--s1);height:60px;flex:none}
.m-jokers .joker{--ec:#5B7597;position:relative;display:grid;place-items:center;height:60px;min-width:0;padding:0 2px;border:0;border-radius:6px;text-align:center;color:#fff;cursor:pointer;background:linear-gradient(165deg,#1E3050 0%,#0F1A2E 60%,#0B1424 100%);box-shadow:inset 0 0 0 2px var(--ec),0 4px 10px rgba(0,0,0,.5);transition:transform .16s cubic-bezier(.2,.9,.3,1),box-shadow .16s}
.m-jokers .joker::before{content:'';position:absolute;inset:0;border-radius:6px;background:linear-gradient(115deg,rgba(255,255,255,.12) 0%,transparent 40%);pointer-events:none}
.m-jokers .joker .pix{font:12px/1.15 var(--pix);letter-spacing:-.02em;text-shadow:0 1px 0 rgba(0,0,0,.5);white-space:nowrap}
.m-jokers .joker.is-long .pix{font-size:11px;letter-spacing:-.05em}
.m-jokers .joker.is-rare{--ec:var(--purple);background:linear-gradient(165deg,#2C1B42 0%,#170E28 60%,#0E0818 100%)}
.m-jokers .joker.is-rare::after{content:'';position:absolute;inset:2px;border-radius:4px;pointer-events:none;background:linear-gradient(118deg,transparent 28%,rgba(179,107,232,.38) 42%,rgba(30,155,255,.28) 52%,rgba(75,227,111,.18) 60%,transparent 74%)}
.m-jokers .joker.is-lit{transform:translateY(-4px);background:linear-gradient(165deg,#4A2C6E 0%,#2A1748 60%,#170E28 100%);box-shadow:inset 0 0 0 2px #D9A9FF,0 0 0 1px rgba(179,107,232,.6),0 0 24px rgba(179,107,232,.8),0 6px 14px rgba(0,0,0,.5)}
.m-jokers .joker .float{position:absolute;left:50%;bottom:-10px;z-index:5;display:none;align-items:center;height:24px;padding:3px 6px 0;transform:translateX(-50%);font:700 24px/.8 var(--led);color:#fff;white-space:nowrap;background:linear-gradient(180deg,#C98BF5,#8A3FD1);clip-path:polygon(5px 0,100% 0,calc(100% - 5px) 100%,0 100%);box-shadow:0 0 16px rgba(179,107,232,.85)}
.m-jokers .joker .float:not(:empty){display:inline-flex}
.m-jokers .joker.empty{--ec:rgba(147,163,188,.28);cursor:default;background:rgba(11,20,36,.55);box-shadow:inset 0 0 0 1.5px var(--ec)}
.m-jokers .joker.empty::before{display:none}
.m-jokers .joker.empty .pix{font:700 var(--t-label)/1 var(--ui);letter-spacing:.1em;color:rgba(147,163,188,.7);text-shadow:none}

/* ---- the starters: our side's fixtures under our coaches (the owner's correction), two chips
   spanning the coach row — QB orange, LINE gold — each a 44 px tall button around a 32 px chip ---- */
.m-starters{position:relative;z-index:1;display:flex;gap:var(--s2);height:36px;margin-top:-2px;flex:none}
.m-starters .starter{--ec:var(--orange);flex:1;min-width:0;display:flex;align-items:flex-end;height:44px;margin:-6px 0 -2px;padding:10px 0 2px;border:0;background:none;color:inherit;cursor:pointer}
.m-starters .starter.is-line{--ec:var(--gold)}
.m-starters .starter span{display:inline-flex;align-items:center;justify-content:center;gap:7px;width:100%;height:32px;padding:2px 10px 0;border-radius:6px;font:13px/1 var(--pix);letter-spacing:.03em;text-transform:uppercase;color:var(--ec);white-space:nowrap;overflow:hidden;background:linear-gradient(165deg,#1E3050 0%,#0F1A2E 60%,#0B1424 100%);box-shadow:inset 0 0 0 2px var(--ec),0 4px 10px rgba(0,0,0,.5);text-shadow:0 1px 0 rgba(0,0,0,.5)}
.m-starters .starter em{font-style:normal;color:#fff;min-width:0;overflow:hidden;text-overflow:ellipsis}

/* ---- the counter: THE screen. Blue YARDS × red MULT, dim at rest, alive once a selection
   prices; the gain in gold; one odds line (D4). Its floor pays the kicks row once, in every
   state, so nothing above it or below it ever jumps. ---- */
.m-counter{position:relative;flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:160px}
.m-counter>*{flex-shrink:0}
.m-counter .played:empty{display:none}
.m-counter .recipe{height:18px;font:16px/1 var(--pix);color:var(--gold);letter-spacing:.03em;white-space:nowrap;text-shadow:0 0 12px rgba(255,196,45,.35)}
.m-counter .recipe small{margin-left:var(--s2);font:700 var(--t-label)/1 var(--ui);letter-spacing:.06em;color:var(--dim)}
.m-counter .pair{display:grid;grid-template-columns:96px 24px 96px;align-items:center;justify-items:center;padding:0;border:0;background:none;color:inherit;cursor:pointer}
.m-counter .pair[disabled]{cursor:default}
.m-counter .box{position:relative;display:grid;place-items:center;flex:none;width:96px;min-width:0;height:44px;padding:0;font:inherit;clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%);box-shadow:inset 0 0 0 1px rgba(255,255,255,.16),inset 0 1px 0 rgba(255,255,255,.25);transition:background .2s}
.m-counter .box.yards{background:linear-gradient(180deg,#2C8FE8,#0F5AA8)}
.m-counter .box.mult{background:linear-gradient(180deg,#F04E4E,#B31C1C)}
.m-counter .box .n{margin:0;padding:0 0 8px;font:700 32px/.8 var(--led);color:#fff;text-shadow:0 1px 0 rgba(0,0,0,.35);font-variant-numeric:tabular-nums}
.m-counter .box .k{position:absolute;left:0;right:0;bottom:3px;text-align:center;font:var(--t-label)/1 var(--pix);letter-spacing:.06em;color:#fff} /* the 12 px label floor (Task 17 walk: it was 10 px) */
.m-counter .times{font:16px/1 var(--pix);color:var(--cream)}
.m-counter[data-state="rest"] .box.yards{background:linear-gradient(180deg,#10294A,#0B1E38);box-shadow:inset 0 0 0 1px rgba(30,155,255,.35)}
.m-counter[data-state="rest"] .box.mult{background:linear-gradient(180deg,#3A1522,#24101A);box-shadow:inset 0 0 0 1px rgba(255,59,59,.35)}
.m-counter[data-state="rest"] .box.yards .k{color:#7CC5FF}
.m-counter[data-state="rest"] .box.mult .k{color:#FF8A80}
.m-counter[data-state="rest"] .times{color:var(--dim)}
.m-counter .gain{display:flex;align-items:baseline;gap:6px;font:var(--t-label)/1 var(--pix);letter-spacing:.06em;color:var(--gold);text-shadow:0 0 14px rgba(255,196,45,.35)}
.m-counter .gain b{font:700 32px/.8 var(--led);letter-spacing:0;font-variant-numeric:tabular-nums}
.m-counter .gain .tag{padding:3px 6px;font-size:var(--t-label);color:var(--ink);background:var(--gold)}
.m-counter .gain .tag.is-td{background:var(--green)}
/* The hint line reads over the field art where the landscape counter meets it (2:1 bare): a dark shadow under each glyph. */
.m-counter .hint{margin:0;min-height:0;text-align:center;font:600 var(--t-body)/1.3 var(--ui);letter-spacing:.02em;color:var(--dim);text-shadow:0 1px 3px rgba(4,9,20,.95)}
.m-counter[data-state="refused"] .hint{color:var(--danger)}
/* The odds line reads as 12 px of type (Task L4: three figures and their deltas fit the 313 px landscape column) but is the only way into its sheet: a 44 px hit box (the tap floor),
   handed back by negative margins so the counter's layout does not move. The box grows upward, over the
   gain and the hint (nothing to tap there): below it, a landscape counter has no room to spare. */
.m-counter .odds{position:relative;z-index:1;display:inline-flex;align-items:center;box-sizing:border-box;min-height:44px;margin:-26px 0 -5px;padding:26px 0 5px;border:0;background:none;cursor:pointer;font:700 var(--t-label)/1 var(--ui);letter-spacing:.02em;text-transform:uppercase;white-space:nowrap}
.m-counter .gain+.hint:empty+.odds,.m-counter .gain+.odds{margin-top:calc(4px - 26px)} /* Teko's figures sit below their line box: room for the gain's 32 px digits */
.m-counter .odds b{color:var(--green)}
.m-counter .odds b.big{color:var(--gold)}
.m-counter .odds i{font-style:normal;color:var(--dim);margin:0 4px}
/* Task L4: FAIL in the boss's red; each figure carries what the last card change moved (an arrow and
   the points: green when better, red when worse, never the colour alone); a chevron says the line opens. */
.m-counter .odds b.fail{color:var(--danger)}
.m-counter :is(.odds,.ret-odds) .d,.m-sheet .cs-val .d{margin-left:2px;font:700 var(--t-label)/1 var(--ui);letter-spacing:0}
.m-counter :is(.odds,.ret-odds) .d.is-good,.m-sheet .d.is-good{color:var(--green)}
.m-counter :is(.odds,.ret-odds) .d.is-bad,.m-sheet .d.is-bad{color:var(--red)}
.m-counter .odds .more{width:6px;height:6px;margin:0 2px 0 7px;border:solid var(--dim);border-width:0 2px 2px 0;transform:rotate(-45deg)}
.m-counter .odds:disabled .more{display:none}
.m-counter .gain:empty,.m-counter .hint:empty,.m-counter .odds:empty,.m-counter .recipe:empty{display:none}
/* D5: throw mode reads as its own mode — the empty pair leaves, the instruction takes its place in red display type. */
.m-counter[data-mode="throw"] .pair{display:none}
/* The kick return prices nothing on the phone (the preview has no return): no empty YARDS × MULT
   pair, the instruction is the counter (Task L2); its gain line still counts the return's yards. */
.m-counter[data-mode="return"] .pair{display:none}
.m-counter[data-mode="return"][data-state="rest"] .hint{font:18px/1.15 var(--pix);letter-spacing:.04em;text-transform:uppercase;color:var(--cream);text-wrap:balance}
/* R1 (owner 2026-09-28): the selected returner's odds under the instruction — the likely start and
   its usual range (field position: the YARDS blue), then the TD (gold) and FUMBLE (the FAIL red)
   chances. Two lines, the odds line's type, on a readout plate (the event chips' ground): in
   landscape the counter's foot sits on the turf, and 12 px labels do not read on the yard lines.
   Each figure carries what the last change of returner moved (the odds line's ▲/▼, below). */
.m-counter .ret-odds{display:flex;flex-direction:column;align-items:center;gap:6px;box-sizing:border-box;max-width:100%;padding:6px 10px 5px;font:700 var(--t-label)/1 var(--ui);letter-spacing:.03em;text-transform:uppercase;color:var(--dim);white-space:nowrap;background:rgba(11,20,36,.86);box-shadow:inset 0 0 0 1px rgba(255,255,255,.12);clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%)}
.m-counter .ret-odds p{display:flex;align-items:baseline;margin:0}
.m-counter .ret-odds b{font-size:var(--t-body);letter-spacing:0;font-variant-numeric:tabular-nums}
.m-counter .ret-odds b.yl{color:#7CC5FF}
.m-counter .ret-odds b.td{color:var(--gold)}
.m-counter .ret-odds b.fum{color:var(--danger)}
.m-counter .ret-odds i{font-style:normal;margin:0 6px}
.m-counter[data-mode="throw"] .hint{font:20px/1.1 var(--pix);letter-spacing:.04em;text-transform:uppercase;color:var(--red);text-shadow:0 0 12px rgba(255,59,59,.35)}

/* ---- the resolution (P4 Task 8; spec §5.2 D8): events → the cards → the MULT build → the jokers →
   the result, on the screen as it stood. The odds line turns into the events as drawn (each chip
   flips in turn, the one that hit filled); a banner slams over the counter; the played cards dock
   above the pair at 0.6, pop their YARDS and sink into the YARDS box; the MULT lines land where
   the hint was, RD53's QB STACK at 1.5× in the starting QB's orange. Reduced motion (section 0's
   block) lands every step at once; the fade to the final frame is opacity only. ---- */
.m-counter .played{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;justify-content:center;height:60px;pointer-events:none}
.m-counter .played .card{flex:none;width:var(--cw);height:var(--ch);margin:-20px -14px;transform:scale(.6)}
.m-hand .card.is-played{opacity:0;pointer-events:none;transition:opacity .18s ease-out}
.c-yards.is-struck{text-decoration:line-through;text-decoration-color:var(--red);text-decoration-thickness:2px}
.m-counter[data-state="resolving"] .odds{flex-wrap:wrap;justify-content:center;gap:4px;max-width:100%;white-space:normal;cursor:default}
.m-counter .odds .ev{display:inline-flex;align-items:center;height:22px;padding:2px 6px 0;font:700 var(--t-label)/1 var(--ui);letter-spacing:.03em;white-space:nowrap;color:var(--dim);background:rgba(11,20,36,.85);box-shadow:inset 0 0 0 1px rgba(255,255,255,.14);clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%);animation:m-ev-flip .26s cubic-bezier(.2,.9,.3,1.2) both;animation-delay:calc(var(--i) * 120ms)}
.m-counter .odds .ev.is-hit{color:#fff;background:linear-gradient(180deg,#F04E4E,#B31C1C);box-shadow:0 0 12px rgba(255,59,59,.55)}
.m-counter .odds .ev.is-hit.is-big{color:var(--ink);background:linear-gradient(180deg,#FFD866,#E8A80F);box-shadow:0 0 12px rgba(255,196,45,.55)}
.m-counter .odds .ev.is-instant{animation:none}
.m-counter .odds.is-settled .ev.is-miss{display:none}
@keyframes m-ev-flip{from{transform:perspective(200px) rotateX(90deg);opacity:0}to{transform:none;opacity:1}}
.m-counter .mlines{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:4px 6px}
.m-counter .mline{display:inline-flex;align-items:baseline;gap:4px;padding:3px 6px 2px;font:700 var(--t-label)/1 var(--ui);letter-spacing:.03em;color:var(--cream);white-space:nowrap;background:rgba(179,28,28,.22);box-shadow:inset 0 0 0 1px rgba(255,59,59,.4)}
.m-counter .mline b{font:700 16px/.8 var(--led);color:#FF8A80;font-variant-numeric:tabular-nums}
.m-counter .mline.is-malus{background:rgba(11,20,36,.85);box-shadow:inset 0 0 0 1px rgba(147,163,188,.4)}
.m-counter .mline.is-malus b{color:var(--dim)}
.m-counter .mline.is-drive{background:rgba(255,196,45,.14);box-shadow:inset 0 0 0 1px rgba(255,196,45,.45)}
.m-counter .mline.is-drive b{color:var(--gold)}
.m-counter .mline.is-qb-stack{font-size:18px;color:var(--orange);background:rgba(255,138,30,.16);box-shadow:inset 0 0 0 1.5px var(--orange),0 0 14px rgba(255,138,30,.4)}
.m-counter .mline.is-qb-stack b{font-size:24px;color:var(--orange)}
.m-counter .m-banner{position:absolute;left:50%;top:50%;z-index:4;display:flex;flex-direction:column;align-items:center;gap:5px;padding:10px 20px 8px;transform:translate(-50%,-50%);white-space:nowrap;pointer-events:none;clip-path:polygon(10px 0,100% 0,calc(100% - 10px) 100%,0 100%);box-shadow:0 10px 24px rgba(0,0,0,.55)}
.m-counter .m-banner b{font:28px/1 var(--pix);letter-spacing:.05em;text-shadow:0 2px 0 rgba(0,0,0,.35)}
.m-counter .m-banner small{font:700 var(--t-body)/1 var(--ui);letter-spacing:.04em}
.m-counter .m-banner.is-bad{color:#fff;background:linear-gradient(180deg,#F04E4E,#9E1515)}
.m-counter .m-banner.is-gold{color:var(--ink);background:linear-gradient(180deg,#FFD866,#E09A10)}
.m-counter .m-banner.is-td{color:var(--ink);background:linear-gradient(180deg,#7CF29A,#2FAE52)}
#screen.match.is-fading .m-counter{animation:m-fade .3s ease-out}
@keyframes m-fade{from{opacity:.35}to{opacity:1}}

/* ---- the pile: a count, a tap to its sheet. It reads as an 18 px line but its hit box is 44 px (the tap
   floor), handed back by negative margins — 16 above, 10 below, so the box stops at the hand's gap. ---- */
.m-pile{position:relative;z-index:1;display:flex;align-items:center;justify-content:flex-end;gap:5px;align-self:flex-end;height:44px;margin:-16px 0 -10px;padding:6px 2px 0;min-width:44px;border:0;background:none;color:var(--dim);cursor:pointer;flex:none}
.m-pile b{font:700 20px/.8 var(--led);color:#fff;padding-top:3px}

/* ---- the hand: 2 × 4 at 70 × 100 (RD27). Selected = the gold edge and a 6 px rise; RD57's
   same-team mates = the gold team tag and a faint gold edge; a throw's pick = the red edge;
   a selected card outside the recipe (RD77) greys; a tired card's pips go red; an INJURY
   REPORT card's YARDS is struck ---- */
.m-hand{display:grid;grid-template-columns:repeat(4,var(--cw));grid-auto-rows:var(--ch);column-gap:12px;row-gap:16px;justify-content:center;align-content:center;flex:1;min-height:calc(var(--ch) * 2 + 16px)}
.m-hand .card .c-name{letter-spacing:-.04em}
.m-hand .card.bface{cursor:pointer;transition:transform .16s cubic-bezier(.2,.9,.3,1)}
.m-hand .card.bface:hover{transform:translateY(-2px)}
.m-hand .card .face{transition:box-shadow .16s,filter .16s,opacity .16s}
.m-hand .card.is-sel,.m-hand .card.is-sel:hover{transform:translateY(-6px)} /* a hovered (or last-tapped, sticky touch :hover) selected card sits with the others */
.m-hand .card.is-sel .face{box-shadow:inset 0 0 0 2px var(--gold),0 0 0 1px rgba(255,196,45,.4),0 10px 18px rgba(0,0,0,.6),0 0 16px rgba(255,196,45,.35)}
.m-hand .card.is-mate:not(.is-sel) .face{box-shadow:inset 0 0 0 1.5px rgba(255,196,45,.3),0 6px 12px rgba(0,0,0,.55)}
.m-hand .card.is-mate:not(.is-sel) .c-team{color:var(--gold)}
.m-hand[data-mode="throw"] .card.is-sel .face{box-shadow:inset 0 0 0 3px var(--red),0 0 18px rgba(255,59,59,.55)}
.m-hand .card.is-tired .c-pips i{box-shadow:inset 0 0 0 1px var(--red)}
.m-hand .card.is-injured .c-yards{text-decoration:line-through;text-decoration-color:var(--red);text-decoration-thickness:2px}
.m-hand[data-mode="scramble"] .card{opacity:.45;cursor:default}

/* ---- the "only at their moment" row (RD54, RD53, RD81): on screen in every state, on the action
   row's own four columns — SNEAK (TUSH PUSH on two TE) above PLAY, SCRAMBLE and PUNT above
   TIMEOUT, FG across the two icon columns; all four at once share four equal columns ---- */
.m-kicks{display:grid;grid-template-columns:44px 44px minmax(0,1fr) minmax(0,1.6fr);gap:var(--s2);height:44px;flex:none}
.m-kicks.is-full{grid-template-columns:repeat(4,minmax(0,1fr))}
.m-kicks.is-full .btn{padding:0 var(--s1);font-size:13px;letter-spacing:0}
.m-kicks .btn{min-height:44px;height:44px;padding:0 var(--s2);font-size:var(--t-body)}
.m-kicks .fg{grid-column:1 / 3}
.m-kicks .punt,.m-kicks .scramble{grid-column:3}
.m-kicks .sneak{grid-column:4}
.m-kicks.is-full .fg{grid-column:1}.m-kicks.is-full .punt{grid-column:2}.m-kicks.is-full .scramble{grid-column:3}
.m-kicks .scramble.is-armed{box-shadow:inset 0 0 0 2px var(--orange)}
.m-kicks+.m-actions{margin-top:-4px}
/* The kick return uses none of the row (Task L2): it leaves, and the offer sits on the actions. */
.m-kicks[data-mode="return"]{display:none}
.m-kicks[data-mode="return"]+.m-actions{margin-top:0}
.m-hand[data-mode="return"]{align-content:end}
.m-actions[data-mode="return"] .btn.pri{grid-column:3 / -1}

/* ---- the action row: RECIPES · AUDIBLES (always there, RD28) · TIMEOUT · PLAY ---- */
.m-actions{display:grid;grid-template-columns:44px 44px minmax(0,1fr) minmax(0,1.6fr);gap:var(--s2);height:58px;flex:none}
.m-actions .btn{height:58px;min-height:58px;padding:0 var(--s2)}
.m-actions .btn.icon{width:44px;min-width:44px}
.m-actions .btn.grey:not(.icon),.m-kicks .btn.grey{background:linear-gradient(180deg,#26385A,#142238);box-shadow:inset 0 0 0 1px rgba(255,255,255,.14)}
.m-actions .m-timeout{gap:6px;padding:0 var(--s1);font-size:var(--t-body);letter-spacing:.02em}
.m-actions .m-timeout b{font:700 26px/.8 var(--led);padding-top:4px;color:var(--gold)}
.m-actions .btn.is-disabled{opacity:.45;filter:saturate(.35)}
.m-actions .btn.pri{font-size:24px}
.m-actions .m-emoji{font-size:22px;line-height:1;filter:grayscale(.15)}
.m-actions .m-aud.is-used .m-emoji{opacity:.4}
.m-actions .m-aud.is-armed{box-shadow:inset 0 0 0 2px var(--green),0 0 14px rgba(75,227,111,.45)}

/* ---- a short portrait phone (final review ux-01): the stack above needs 832 px, and a phone
   browser's chrome leaves 660–750 (Safari's visible 390×664). Below 832 px the whitespace gives
   first — the gaps follow the height down to 2 px — then the three HUD bands step to their 44/44/48
   floor (still the tap floor), the hand's rows close to 8 px, the pile leaves the flow for the
   counter's bottom-right corner, and the counter takes what is left, down to the 116 px its selected
   state needs (recipe · pair · gain · odds). The cards stay 70 × 100 (RD27) and nothing scrolls. ---- */
@media (orientation: portrait) and (max-height: 831px){
  #screen.match{--mg:clamp(2px,calc((100dvh - 640px) / 16),10px);gap:var(--mg);padding:4px 0}
  #screen.match .m-col{gap:var(--mg)}
  #screen.match .m-top{height:44px}
  #screen.match .m-boss{height:44px;padding-block:3px}
  #screen.match .m-jokers,#screen.match .m-jokers .joker{height:48px}
  #screen.match .m-counter{height:clamp(116px,calc(100dvh - 526px - 8 * var(--mg)),160px);min-height:0;gap:4px}
  #screen.match .m-counter .hint{padding-inline:48px}
  /* A refusal wraps (two or three lines at 390 px): on these heights the gain line gives it its row —
     the pair still shows YARDS × MULT (fix round 1). So does a note on the play (READ's swap, RD77's
     cards outside the recipe): the counter has no fifth row to spare here. */
  #screen.match .m-counter:has(.hint.is-refusal,.hint>.note) .gain{display:none}
  /* The odds line's 44 px hit box grows upward only (over the gain), so the hand's first row starts
     right under it and a risen, selected card is never under the odds line's box. */
  #screen.match .m-counter .odds{margin:-30px 0 -1px;padding:30px 0 1px}
  #screen.match .m-counter .gain+.odds,#screen.match .m-counter .gain+.hint:empty+.odds{margin-top:-26px}
  #screen.match .m-col-b{position:relative}
  #screen.match .m-pile{position:absolute;right:var(--s2);top:calc(-44px - var(--mg));margin:0}
  /* R1: the kick return's odds take the counter's foot, and its four returners leave the hand's top
     row empty — the pile waits there, where a tall portrait keeps it, rather than over the odds. */
  #screen.match .m-col-b:has(> .m-hand[data-mode="return"]) .m-pile{top:0}
  #screen.match .m-hand{row-gap:8px;min-height:calc(var(--ch) * 2 + 8px)}
}
/* Under ~720 px the counter is at its 116 px floor: while a play resolves, the MULT build lines give
   their row to the event that hit (the landscape rule, section 10 of the landscape block) — the MULT
   box still shows the total. */
@media (orientation: portrait) and (max-height: 719px){
  #screen.match .m-counter[data-state="resolving"] .hint:has(> .mlines){display:none}
}

/* ---- the match's sheets: one row vocabulary (label left, figure right) ---- */
.m-sheet .cs-row{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s3);padding:var(--s1) 0;border-bottom:1px solid rgba(255,255,255,.06);font:600 var(--t-body)/1.3 var(--ui)}
.m-sheet .cs-label{min-width:0}
.m-sheet .cs-label small{color:var(--dim);font-size:var(--t-label);margin-left:var(--s1)}
.m-sheet .cs-val{flex:none;font:700 20px/1 var(--led);font-variant-numeric:tabular-nums}
.counter-sheet .cs-yards .cs-val{color:#7CC5FF}
.counter-sheet .cs-mult .cs-val{color:#FF8A80}
.m-sheet .cs-row.is-total .cs-label,.m-sheet .cs-row.is-gain .cs-label{font-family:var(--pix);text-transform:uppercase}
.m-sheet .cs-row.is-gain .cs-val{color:var(--gold)}
.m-sheet .cs-row.is-out,.m-sheet .cs-row.is-off{color:var(--dim)}
.m-sheet .cs-row.is-lit .cs-val{color:#D9A9FF}
.m-sheet .cs-row.is-malus .cs-label{color:var(--danger)}
.m-sheet .cs-row.is-big .cs-label{color:var(--gold)}
.m-sheet .cs-row.is-ok .cs-label{color:var(--green)}
.m-sheet .cs-row.is-fail{margin-top:var(--s1)}
.m-sheet .cs-row.is-fail .cs-label{color:var(--danger)}
.m-sheet .cs-row.is-ev{padding-left:var(--s3)}
.boss-sheet .bs-bar{display:grid;grid-template-columns:minmax(0,120px) minmax(0,1fr);align-items:center;gap:var(--s2)}
.boss-sheet .bs-bar .meter{margin:0}
.boss-sheet .bs-bar.is-spec .t-label{color:#FF8A80}
.boss-sheet .bs-bar.is-spec .meter i{background:linear-gradient(90deg,#FF6B60,var(--red))}
.boss-sheet .bs-rule,.boss-sheet .bs-mod{display:flex;flex-direction:column;gap:2px}
.boss-sheet .bs-rule b{color:#FF8A80;font-weight:400}
.m-sheet .tag.is-rare{color:#D9A9FF;box-shadow:inset 0 0 0 1px rgba(179,107,232,.6)}
/* The YARDS/MULT legend the draft board and the keep screen carry (screens.js#draftScreen,
   #keepScreen) — it lived in this section with the v0.2 match and stays for them. */
#pwrCapLegend{flex:none;flex-wrap:nowrap;gap:var(--s2);padding:0 var(--s1);white-space:nowrap}
/* The swatch pair is a CLASS now, not this id's private markup: the draft
   board's header grew the same legend (onboarding-05), where the words were
   dropped from the 84 px card for a measured reason and the player was left
   with two unnamed figures on the first screen they ever see. Two call sites,
   one definition — a legend that disagreed with the chips would be worse than
   no legend. `.sw-legend` is the draft's own wrapper; `#pwrCapLegend` keeps
   its own because it lives in the match's `.zone-side` column. */
.sw-legend{display:flex;align-items:center;gap:var(--s2);white-space:nowrap}
.sw-pwr,.sw-cap{display:inline-flex;align-items:center;gap:5px}
/* A KEY IS NOT A CONTROL, AND IT DEPICTS WHAT IT NAMES. A filled light SQUARE
   beside "PWR" and an outlined one beside "CAP", each followed by an uppercase
   word and sitting directly under two real tappable boxes, is the shape of a
   filter toggle — a checkbox pair — not the shape of a legend. It was also
   inaccurate: `/shared/design-system.css:154` gives `.c-pwr`/`.c-cap` a
   `clip-path:polygon(5px 0,100% 0,calc(100% - 5px) 100%,0 100%)` and these
   swatches had none, so the legend printed a shape the product never draws.
   The same slant, at 14 x 10 rather than 10 x 10: the 5 px offset is a third
   of the width at 14 and legible at 10 px tall, where at 10 x 10 it would be
   half the box and read as a wedge. A parallelogram is not a control anywhere
   in this product, which is the whole point. */
.sw-pwr::before,.sw-cap::before{content:'';flex:none;width:14px;height:10px;
  clip-path:polygon(5px 0,100% 0,calc(100% - 5px) 100%,0 100%)}
/* The swatches follow the chips they name (match-core-09). Their old blue and
   green were the stat pills' own fills, so the legend was ASSERTING "blue =
   PWR, green = CAP" three rows above a grid of blue-striped WRs and
   green-striped RBs — it made the collision worse by naming one of the two
   meanings as if it were the only one. Now it names the neutral pair, in the
   plates' own stops — CAP included, which is why the fill moved here with
   `.c-cap`'s: the swatch is clipped now too, so the inset ring's verticals are
   eaten on the swatch exactly as they are on the plate (see section 1), and a
   swatch drawn in the old fill would be re-stating a boundary the chip no
   longer relies on. */
.sw-pwr::before{background:linear-gradient(180deg,#5B7597,#31456A)}
.sw-cap::before{background:linear-gradient(180deg,#26384F,#0F1A2E);box-shadow:inset 0 0 0 1px rgba(255,255,255,.22)}

/* ==== 11 final — the post-match screen (spec §4.8, COVERAGE.md §4) ====
   Six panels in ONE grid, re-laid by the three breakpoints below rather than by
   `flex-wrap` + six inline `min-width`s (the day-0 shape: two wrapping rows of
   `flex:1;min-width:230px`, which on a 390 phone gave one column anyway and on a
   landscape phone put the third row under the ticker — audit L17). The panels
   are `.pnl` at the section-4 padding, so the screen has ONE panel padding
   instead of COVERAGE §4.3 defect 7's three.
   `#screen` scrolls it (section 6): the day-0 root was a `.scroll`, i.e. a second
   scroller nested inside the one the shell already provides. */
/* The base template is DECLARED, not left to auto-placement. `grid-area:hero` on a
   grid with no matching named area does NOT fall back to auto flow: CSS Grid §8.3
   says an unmatched <custom-ident> line name is assumed on every implicit line, so
   all six panels resolved onto the SAME implicit line and stacked on top of each
   other in a viewport-tall implicit track (measured — only the last panel painted). */
/* `align-items:start` so a panel is its own content height: the default `stretch`
   made the hero (four short lines) as tall as the Drives panel beside it in the
   multi-column breakpoints — measured 560 px of empty panel at 852×393. */
.fin{flex:none;display:grid;gap:var(--s3);align-content:start;align-items:start;
  grid-template-columns:minmax(0,1fr);
  grid-template-areas:"hero" "quarters" "coins" "levels" "share" "ranked"}
.fin>.pnl{min-width:0}
.fin-hero{grid-area:hero;text-align:center;padding:var(--s4)}
/* The hero's two labels are the only `.t-label`s on the app that WRAP (a boss
   name plus the ranked/practice verdict is 60+ characters), so they take the UI
   line-height rather than the tight one a one-line chip wants. */
.fin-hero .t-label{line-height:var(--lh-ui)}
/* 44, not the day-0 `clamp(56px,9vw,96px)`: `--t-led-lg` is the top of the scale
   (spec §4.1) and the final score was the one number on mobile bigger than the
   scale allows (COVERAGE §4.3 defect 8). The glow is `.led.big`'s, kept. */
.fin-score{margin-top:var(--s1);color:#FFB300;text-shadow:0 0 14px rgba(255,179,0,.55);letter-spacing:.02em}
.fin-pity{margin-top:var(--s2);background:var(--gold);color:#1E1400}
.fin-quarters{grid-area:quarters} .fin-levels{grid-area:levels}
.fin-row{min-height:var(--ctl-std);margin-top:var(--s1);padding:0 var(--s2);background:rgba(0,0,0,.28);clip-path:polygon(6px 0,100% 0,calc(100% - 6px) 100%,0 100%)}
.fin-row>*{min-width:0}
.fin-coins{grid-area:coins;align-items:center}
/* The match by quarter (Task L5): one 48 px button per quarter — Q, its YARDS (blue, the YARDS
   colour), its touchdowns (gold once there is one), the chevron that says it opens. While the
   replay loads the same four rows hold dashes, so nothing moves when it lands. */
.fin-tds{margin-top:var(--s1);color:var(--gold2)}
.fin-q{display:grid;grid-template-columns:34px minmax(0,1fr) auto 16px;align-items:center;gap:var(--s2);width:100%;border:0;color:var(--cream);font:600 var(--t-body)/1 var(--ui);text-align:left;cursor:pointer;transition:background-color .15s}
.fin-q:hover{background:rgba(30,155,255,.14)}
.fin-q:active{background:rgba(30,155,255,.22)}
.fin-q:focus-visible{clip-path:none;outline:2px solid var(--blue);outline-offset:-2px}
.fin-q.is-loading{cursor:default;color:var(--dim)}
.fin-q-yd,.fin-q-td{white-space:nowrap;color:var(--dim)}
.fin-q-yd b,.fin-q-td b{font:700 var(--t-head)/1 var(--led);font-variant-numeric:tabular-nums;color:var(--cream)}
.fin-q-yd b{color:#7CC5FF}
.fin-q-td.is-td b{color:var(--gold)}
.fin-q svg{color:var(--dim)}
.fin-q-err{margin:var(--s2) 0}
/* A quarter's plays (the sheet): situation over call · result, the yards on the right —
   blue a gain, red a loss, gold a touchdown (with the gold rail). */
.fin-plays{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
.fin-play{display:flex;align-items:center;gap:var(--s3);padding:var(--s2) 0;border-bottom:1px solid rgba(255,255,255,.06)}
.fin-play .t-body{line-height:var(--lh-ui)}
.fin-play-yd{flex:none;font:700 var(--t-head)/1 var(--led);font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--dim)}
.fin-play.is-gain .fin-play-yd{color:#7CC5FF}
.fin-play.is-loss .fin-play-yd{color:var(--danger)}
.fin-q-yd.is-loss b{color:var(--danger)}
.fin-play.is-td{box-shadow:inset 3px 0 0 var(--gold);padding-left:var(--s2)}
.fin-play.is-td .fin-play-yd{color:var(--gold)}
/* RD59: one row per recipe that gained a use, `{recipe} {from} → {to}`; gold when a level was
   gained. The no-bump line is plain body copy in the same panel, never an empty block. */
.fin-levels .fin-level{align-items:center}
.fin-levels p{margin:var(--s1) 0 0}
/* Share replay — the panel's teal rail and the replay card it frames. */
.fin-share{grid-area:share;--edge:#7EE0D2}
.fin-share-lbl{color:#7EE0D2}
.fin-card{margin-top:var(--s2);padding:var(--s3) var(--s4);background:linear-gradient(135deg,#0b1320,#1d2840);box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);clip-path:polygon(8px 0,100% 0,calc(100% - 8px) 100%,0 100%)}
.fin-card-brand{font:var(--t-label)/1 var(--pix);letter-spacing:.2em;color:#7EE0D2}
.fin-card-score{margin-top:var(--s1);font:var(--t-num)/1 var(--pix);color:#FFD166}
/* Prose, so the 14 px body role — the two lines were 12 and 11 px inline
   (COVERAGE §4.3 defect 4's "Money moment" is the second one). */
.fin-card-line{margin-top:var(--s1);font-size:var(--t-body);line-height:var(--lh-body);color:#CBD5E1}
/* Off-screen, never `display:none`: game.js#share-copy calls `.select()` on it
   and tests/match.spec.js reads `.inputValue()`, and both need a real, laid-out
   field. `position:fixed` so a focused off-screen box cannot scroll an ancestor,
   and an id selector (1,0,0) so it beats section 4's `input[type]` tier. */
#shareUrl{position:fixed;left:-9999px;top:0;width:1px;height:1px;min-height:0;padding:0;opacity:0;pointer-events:none}
.fin-ranked{grid-area:ranked}
.fin-ranked .hint{margin-top:var(--s2)}
/* ==== 12 phone landscape (A12/A14): rail 72 left = 6 tabs (back + ⚙ are in the
   header); header 44 in the content column; ticker in the column ==== */
@media (orientation: landscape) and (max-height: 500px){
  body{--rail:72px;--hdr:44px}
  #app{grid-template-columns:auto auto 1fr;grid-template-rows:auto 1fr auto;grid-template-areas:"nav hd hd" "nav side main" "nav tick tick"}
  #app.noside{grid-template-areas:"main" "tick";grid-template-columns:1fr;grid-template-rows:1fr auto}
  .tabbar{width:var(--rail);grid-template-columns:1fr;grid-template-rows:repeat(6,44px);gap:3px;align-content:start;padding:var(--s2) 0;min-height:0;box-shadow:1px 0 0 rgba(255,255,255,.08)}
  .tab{min-height:44px;font-size:var(--t-label);line-height:1;word-break:normal}
  .hub-hd{gap:var(--s2);padding:0 var(--s2)}
  .hub-hd .hub-stats .t-head{font-size:var(--t-ui)}
  /* HOW TO PLAY runs ONE column here. The pane is flex-shrunk by the modal's
     max-height to ~140 px, which is exactly one row of the two-column
     `auto-fit` grid, so nothing from row 2 ever peeked — and the fade at the
     bottom of section 5 landed on FINISHED content instead: step 2's box is
     taller than step 1's, so the mask dimmed step 2's last live line and its
     blue rail while step 1 sat crisp on the same baseline. Two side-by-side
     panels at different opacities read as a disabled pane, not as "there is
     more". One column puts steps 1..7 on a vertical run, the clip lands
     mid-step, and the mask means the same thing it means in portrait. */
  .howto-steps{grid-template-columns:1fr}
  #screen>.pnl{overflow:visible}                     /* #screen scrolls instead — see section 6 */
  #screen{padding:var(--s2)}
  /* THE MARKET HEADER (§4.4) collapses to ONE 44 px line so the stage gets
     the height: at 852×393 `#screen` is 732×305. The belt itself is no longer
     this breakpoint's business — it is the same moving stage at every size
     (section 9b), and a stage this wide measures itself horizontal. */
  .mk-hd{flex-direction:row;align-items:center;gap:var(--s3)}
  .mk-hd .hub-hd-top{flex:1;min-width:0;flex-wrap:nowrap}
  .mk-hd .t-title{font-size:var(--t-head)}
  .mk-hd-note,.mk-feed{display:none}
  .mk-ctl{flex:none}
  .mk-pnl{padding:var(--s2);gap:var(--s1)}
  /* The stage never gets shorter than the tile it moves (plus a gap above and
     below), and `.mk-pnl` stops shrinking below its own content: with the
     coach lane in flow, `#screen` scrolls to the lane instead of the belt
     silently absorbing the loss — the stage is the one with the tap targets
     in it. `flex-grow` stays 1: with no lane (the belt's normal state) the
     panel still fills and `#belt` with it. The coach lane's tiles are the
     base section 9b tiles here too: none of the belt's rules reach them. */
  .mk-pnl{flex:1 0 auto}
  #belt{min-height:calc(var(--mp-th) + 2 * var(--s2))}
  /* The deck's gutter goes back to 4 px here, and only here. At 852x393 the
     stage is 744 px for ten cards, so the pitch is already spent: a 12 px
     gutter solves the card to 69 px, under the 78 the name column needs before
     a ten-glyph surname breaks mid-word (docs/FOLLOWUPS.md). Cards that nearly
     touch is the lesser defect of the two on the one profile that is a
     regression guard rather than a layout to improve. */
  body{--mp-gap:4px}
  /* Auth stays ONE column at 852x393 — the panel is 420 at most and the header
     block shrinks to a 32 px logo, so the form is ~368 tall inside a 393 px
     viewport and `#screen` scrolls the remainder rather than a second column
     being invented for it (spec §4.3). */
  .auth-wrap .logo.auth-logo span{font-size:32px}
  .auth-wrap{gap:var(--s3)}
  .auth-panel{margin-top:var(--s1)}
  .pick-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
  /* Title at 852×393 — one column does not fit (measured: 493 px of content in
     349 px of `#screen`). Two columns of equal standing: the header block on the
     left, the five destinations plus HOW TO PLAY on the right, the user row and
     the boss strip spanning both. Each column is its own flex container,
     so each centres its own contents against the taller of the two rather than
     the two stacks being forced onto shared grid rows. */
  .title-wrap{display:grid;align-content:center;min-height:100%;padding:0;column-gap:var(--s4);row-gap:var(--s2);
    grid-template-columns:minmax(0,320px) minmax(0,1fr);
    grid-template-areas:"user user" "hero menu" "strip strip"}
  .title-hero,.menu{justify-content:center;margin:0}
  .title-strip{margin:0}
  .title-hero>.btn.pri{min-height:var(--ctl-std);font-size:var(--t-ui)}
  .menu-grid .btn{min-height:var(--ctl-sm)}
  .title-tile{min-height:48px}
  /* --- 10. match (P4 Task 7, RD26): the portrait cut into two 390 px columns side by side ---
     Every component keeps its portrait size (the cards 70 × 100, RD27); only the vertical
     whitespace gives, and the counter is the one element that shrinks. The pile sits at the
     right column's top with the hand under it; the kicks row takes the slack, so nothing moves
     when a 4th down arrives (the mock's fix round 3). */
  /* Two columns of AT MOST 390 px: below 812 px (a 667-800 px landscape phone) they shrink with
     the gap (32 px down to 8 px) instead of clipping both edges; the 2×4 hand (316 px) still fits
     a 667 px phone's column. */
  #screen.match{display:grid;grid-template-columns:repeat(2,minmax(0,390px));grid-template-rows:minmax(0,1fr);justify-content:center;align-items:stretch;column-gap:clamp(var(--s2),calc(100vw - 780px),var(--s6));padding:1px 0}
  #screen.match .m-col{width:auto;min-width:0;height:100%;margin:0;gap:var(--s1)}
  #screen.match .m-top{height:44px}
  #screen.match .m-starters{height:40px;margin-top:0}
  #screen.match .m-starters .starter{margin:-2px 0}
  #screen.match .m-counter{flex:1;min-height:0;gap:var(--s1)}
  /* 124 px of counter cannot hold the recipe, the pair, the gain, two rows of MULT build lines and
     the event that hit: the lines pushed the event under the viewport (Task 17 finish pass). The
     MULT box still shows the total; the event is what this moment needs. */
  #screen.match .m-counter[data-state="resolving"] .hint:has(> .mlines){display:none}
  #screen.match .m-col-b{gap:clamp(var(--s1),calc(100dvh - 381px),var(--s3));padding-top:16px} /* the pile's 44 px hit box reaches 16 px above it: keep it inside the viewport (the kicks row takes the slack); under a 393 px height (a 375 px phone) the right column's gaps give, down to 4 px, so PLAY stays on screen */
  #screen.match .m-hand{flex:none;min-height:0}
  #screen.match .m-kicks{margin-top:auto}
  /* The kick return (Task L2): four returners on one row, not the hand's 2 × 4 — they take the
     column's width, 70 × 100 at the least and 96 wide at the most, and sit on RETURN; the surname
     sizes itself from the wider card (`.bface .c-name`, 90cqi). */
  #screen.match .m-hand[data-mode="return"]{flex:1;grid-template-columns:repeat(4,minmax(var(--cw),96px));grid-auto-rows:auto;align-content:end;padding-bottom:var(--s2)}
  #screen.match .m-hand[data-mode="return"] .card{width:100%;height:auto;aspect-ratio:7 / 10}
  #screen.match .m-counter[data-mode="return"] .hint{white-space:normal}
  /* Final review ux-02: the counter never grows past its column — at 667×375 the odds line fell under
     the viewport and the recipe rode up onto the starters. It clips instead of pushing, its hint keeps
     one line (a two-line hint pushed the odds to y 406 at 844×390), and under a 390 px height the boss
     line and the coach row step to their 44 / 48 floor (the tap floor), the column's gaps with them,
     so the counter keeps the ~116 px its selected state needs. */
  #screen.match .m-col-a{gap:clamp(2px,calc((100dvh - 334px) / 10),var(--s1))}
  #screen.match .m-counter{overflow:hidden}
  #screen.match .m-counter .hint{max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  /* …but a refusal is read whole (fix round 1): only the build and rule notes keep one line. */
  #screen.match .m-counter .hint.is-refusal{white-space:normal;overflow:visible;text-overflow:clip}
  #screen.match .m-counter:has(.hint.is-refusal,.hint>.note) .gain{display:none} /* the room it wraps into, and a play note's row (READ, RD77's leftovers); the pair still shows YARDS × MULT */
  #screen.match .m-counter .odds{margin:-30px 0 -1px;padding:30px 0 1px} /* its 44 px hit box grows upward only: the counter clips at its foot */
  #screen.match .m-counter .gain+.odds,#screen.match .m-counter .gain+.hint:empty+.odds{margin-top:-26px}
  #screen.match .m-boss{height:clamp(44px,calc(100dvh - 334px),56px);padding-block:3px}
  #screen.match .m-jokers,#screen.match .m-jokers .joker{height:clamp(48px,calc(100dvh - 330px),60px)}
  /* ux-03: TIMEOUT keeps room for its label and its count on a 667 px phone (it read "IMEOUT"); the kicks
     row shares the action row's columns. */
  #screen.match :is(.m-actions,.m-kicks:not(.is-full)){grid-template-columns:44px 44px minmax(100px,1fr) minmax(0,1.6fr)}
  /* Under an 800 px width the column holds 298–339 px of content: the two rows' gaps close to 4 px so
     PLAY keeps its width beside TIMEOUT's floor, and on a 667 px phone PLAY's label steps down a size
     so "PLAY · 2" reads whole. */
  /* TIMEOUT's 100 px floor is TIMEOUT's own: in the throw and scramble modes the secondary is CANCEL,
     which keeps just its label's width, and CONFIRM takes the room back (it was clipped at 667×375,
     fix round 1). */
  #screen.match .m-actions:not(:has(.m-timeout)){grid-template-columns:44px 44px minmax(max-content,1fr) minmax(0,1.6fr)}
  @media (max-width: 800px){
    #screen.match :is(.m-actions,.m-kicks){column-gap:var(--s1)}
  }
  @media (max-width: 700px){
    #screen.match .m-actions .btn.pri{font-size:20px}
  }
  /* Under 361 px (Safari's visible 750×342): the right column needs 360 px at its landscape sizes, so
     the hand's rows close to 8 px, the action row steps to 48 px and the gaps go to 2 px; the starters
     take their portrait band. PLAY stays whole on screen (final review ux-02). */
  @media (max-height: 360px){
    #screen.match .m-col-b{gap:2px}
    #screen.match .m-hand{row-gap:8px}
    #screen.match .m-actions,#screen.match .m-actions .btn{height:48px;min-height:48px}
    #screen.match .m-starters{height:36px;margin-top:-2px}
    #screen.match .m-starters .starter{margin:-6px 0 -2px}
  }
  /* --- 11. final (T9) --- */
  /* TWO columns inside the content column: the hero and the coins stacked beside the quarters
     (P4 Task 9 — the quarters span both rows, so no hole opens under the short hero), then the
     level bumps and the ranked CTA beside the share card. A 393 px
     viewport gives `#screen` ~305 px, so the day-0 single stack put its last
     row under the ticker (audit L17) — two columns halve the stack instead. */
  .fin{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:var(--s2);
    grid-template-areas:"hero quarters" "coins quarters" "levels share" "ranked share"}
  .fin-hero{padding:var(--s3)}
  .fin-score{font-size:var(--t-led)}
  /* --- 7b. draft --- */
  /* 852×393 is wide and short: stacked, THE BOARD alone is taller than the
     305 px `#screen` and the kept pile never shares the fold with it. Two
     columns instead — the header spans both, the board takes the left, the
     kept strip the right, and the two auto-place in source order. The card
     column shrinks with the height, not the width: 10 cards at 72 still land
     four to a row in half the width. */
  .draft-wrap{--dcw:72px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start;gap:var(--s2)}
  .draft-wrap.draft-intro{grid-template-columns:minmax(0,1fr)}
  .draft-hd{grid-column:1 / -1}
  .draft-hd .t-led{font-size:var(--t-head)}
  /* The keep (Task 11): the portrait cut into two columns (RD26) — header and Franchise
     place on the left, the card grid on the right, the cards at the same 70 × 100. */
  .keep-wrap{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start;gap:var(--s2)}
  .keep-sec{grid-column:2;grid-row:1 / span 2}

  /* --- 5c. the three sheets this height actually breaks --- */
  /* A sheet is clamped to `min(86dvh, 100dvh − 56px)` = 337 px here, and the
     three below are the ones whose content runs past it. They are the sheet
     CHROME's problem, not their renderers': a renderer contributes markup,
     never geometry (section 5).

     MATCH MENU — five `.btn.wide` stacked full-bleed need ~372 px, so QUIT TO
     MENU landed at y=392 of a 393 px viewport: one pixel of a 48 px control,
     and it is the only way out of the deepest state in the product. On an
     816 px sheet each of those bars was four words centred in 1900 device
     pixels — the layout was overrunning the axis it does not have while
     wasting the one it does. Two columns then, the same shape `.menu-grid`
     (section 3) already gives the document elsewhere, with the header and the
     red QUIT spanning both: three rows of 48 ≈ 156 px inside the 337.
     `#overlay .modal` is (1,1,0), so these have to be id-on-id to win it —
     and `::before` (the 4 px grab handle) becomes a GRID ITEM the moment the
     sheet stops being flex, where `flex:0 0 4px` means nothing and
     `margin:0 auto` would centre it inside the left column: it needs the span
     and a real height. */
  #overlay #matchMenu{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-content:start}
  #overlay #matchMenu::before{grid-column:1 / -1;height:4px}
  #overlay #matchMenu>.sheet-hd,#overlay #matchMenu>.btn.red{grid-column:1 / -1}

  /* BID SHEET — the form goes BESIDE the card, not under it. Keeping the
     phone-portrait single column put the bid input row at y=381 of a 393 px
     viewport and PLACE BID, the `#bidErr` reserve line and the settle hint
     all below the fold, on the one surface that is OPERATE mode with a
     40-second clock: the player opens the sheet, sees a filled-in amount and
     two quick-raise buttons, and there is no button to commit. Meanwhile
     600 px of the sheet's own width sat empty beside the card.
     Pinning the commit block to the foot of the scroll was the other way and
     was tried first: it works, and it is what the finding proposed, but it
     parks an opaque plate over the card face — the hero of this sheet, and
     the subject of the fix directly above — and the player still has to
     scroll to see what they are bidding on. Two columns spend the width that
     was already there, and then nothing needs pinning at all, because the
     whole sheet fits: ~310 px of content in the 337 the sheet has.
     `min-content`, not `auto`, on the left track: `.bid-card` turns into a
     COLUMN here, so its meta line ("OL · LAC · 4 PWR · cap 1") drops under the
     card and — being the wider of the two — became what sized the track. An
     `auto` track takes its max-content: MEASURED at 852×393, the track was
     166.8 px for a 104 px card, i.e. 62.8 px spent on a string that reprints
     the card's own PWR/CAP chips and the right column's CAP tag — the same two
     numbers a third time — hanging below the card aligned to nothing.
     `min-content` sizes the track to the card itself (the mini is `flex:none`
     at a fixed width, so it IS the column's own minimum): 104 px, with the
     meta wrapping to two 13.2 px lines inside it, and the form column goes
     633.2 -> 696. The sheet still needs no scroll (322 of 322). Which is what
     this whole two-column block exists to feed. */
  #overlay #bidOverlay .bid-scroll{display:grid;grid-template-columns:min-content minmax(0,1fr);column-gap:var(--s4);align-content:start}
  #overlay #bidOverlay .bid-scroll>.bid-card{align-self:start;flex-direction:column;align-items:flex-start}
  #overlay #bidOverlay .bid-scroll>.bid-body{min-width:0}

  /* DECK VIEW — the sheet spent 85 % of its height naming itself and offering
     filters and 15 % on its content: measured, `.scroll` was 51 px for a
     138 px card, so no scroll position ever showed a whole card row. That is
     not "a scrollable list that scrolls", it is a screen that cannot do its
     one job. Gating the sack bar (screens.js#lockerBody) returns ~76 px and
     this returns the other ~21: the summary line and the nine-chip control
     row stop WRAPPING into two rows and share one, with the summary free to
     shrink — it wraps inside its own column, so nothing is clipped to buy
     this.
     The nine chips are the row's FIXED half and the summary is its elastic
     one: sharing the squeeze proportionally just moved the wrap from the row
     to the chips (measured: 65 px became 92). The summary wraps to two 13 px
     lines inside 254 px, which is still shorter than one 44 px control row,
     so the line costs nothing and no string is cut to pay for it. The header
     drops to the 44 px its own ✕ needs — the 48 was the standard tier, not a
     measurement — and the scroll's top margin goes with it. */
  #deckView>.row.between{flex-wrap:nowrap}
  #deckView>.row.between>.row{flex:none}
  #deckView>.row.between>.lbl{flex:1 1 auto;min-width:0}
  #deckView>.sheet-hd{min-height:var(--ctl-sm)}
  #deckView>.scroll{margin-top:0}
}
/* ==== 13 tablet portrait: capped column, full-bleed bars ==== */
@media (orientation: portrait) and (min-width: 600px){
  body{--col:min(600px, calc(100% - 2*var(--gut)))}
  #screen{padding:var(--s3) calc((100% - var(--col)) / 2)}
  .hub-hd,.tabbar,#ticker{padding-left:calc((100% - var(--col)) / 2);padding-right:calc((100% - var(--col)) / 2)}
  .auth-panel,.auth-howto{max-width:480px}
  /* 296 px tiles want the 56 px height the primary CTA has here, or a 600 px
     column reads as six thin slots under a slab (spec §4.4's tablet numbers).
     The vertical centring is section 8's, on every breakpoint. */
  .menu-grid .btn{min-height:var(--ctl-pri)}
  /* The division grid stays TWO columns here, unlike both landscape sections:
     `--col` is capped at 600, so four columns give a 144 px tile and an 84 px
     name box — "Buccaneers" then breaks mid-word and lands 2 px off its own
     edge (measured). Two columns give a 292 px tile, every name on one line. */
  /* --- 10. match (P4 Task 7): the portrait stack at its phone size, centred in the tall column. */
  #screen.match{justify-content:center}
  #screen.match .m-col-b{flex:none}
  /* --- 11. final (T9) --- ONE column in the 600 px capped column.
     match-aux-08: two columns inside a `--col:min(600px,…)` gave each panel
     294 px, and `.lrow-grid` spends 34 + 44 + 44 px plus gaps on fixed
     tracks, so the label track was left 112 px. Measured in
     en/checks/probe-tabp.json (61-final): `.fin-fac > span.t-body.ellipsis`
     clipped at have:112 three times — against have:176 on a 390 px iPhone and
     have:146 on a 360 px phone. The largest portrait device in the corpus was
     giving the label 64 px LESS than the smallest, which is the inverse of
     what more screen is for: fr/tablet-shots/61-final-tabp.png wraps all four
     drive rows to "14 YDS · pas de" / "TD" and cuts every factor label to
     "BROKEN…" / "Screen …", while en/portrait-shots/61-final-iphone.png puts
     the same rows on one line each.
     Stacking inside the existing column, rather than letting `.fin` out of it:
     `--col` is not only `#screen`'s — the two lines below it pad `.hub-hd`,
     `.tabbar` and `#ticker` to the same column, so a full-bleed final screen
     would sit visibly out of register with the header, the tab bar and the
     ticker on the one screen a session ends on. One column gives each panel
     ~568 px, better than the 354 the iPhone gives, and it closes the tabp
     half of the paired-row misalignment for free since nothing is paired by
     row any more.
     NO RULE IS NEEDED HERE. The stack this block wanted is what the base
     `.fin` at section 11 already declares, byte for byte — one
     `minmax(0,1fr)` track and the same six single-cell areas — and the only
     other `.fin` grid rules in the sheet are in the three LANDSCAPE blocks,
     which this portrait query can never match at the same time. The override
     that used to sit here was therefore re-stating the default at a cost of
     one more place to keep in step. The comment stays because the REASONING
     is not the default's: it is why tablet portrait must not go two-up and
     must not leave `--col`, and a future two-column experiment has to answer
     it before adding a rule back. */
  /* --- 7b. draft --- a 600 px column holds five 96 px cards per row. */
  .draft-wrap{--dcw:96px;gap:var(--s4)}
}
/* ==== 14 tablet landscape (A14): rail 96 = 6 tabs; back + coins/cap + ⚙ stay
   in the content header ==== */
@media (orientation: landscape) and (min-height: 501px){
  body{--rail:96px}
  #app{grid-template-columns:auto auto 1fr;grid-template-rows:auto 1fr auto;grid-template-areas:"nav hd hd" "nav side main" "nav tick tick";padding:calc(var(--sat) + var(--s3)) calc(var(--sar) + var(--s3)) calc(var(--sab) + var(--s3)) calc(var(--sal) + var(--s3))}
  #app.noside{grid-template-areas:"main" "tick";grid-template-columns:1fr;grid-template-rows:1fr auto}
  /* THE STAGE (A20). `#app` is `position:fixed;inset:0`, so `left` and `right`
     are both 0 and the box is over-constrained the moment `width` stops being
     auto: with `margin-inline:auto` the equal-margins rule resolves it by
     CENTRING the box (CSS 2.1 §10.3.7), which is exactly the wanted result and
     needs no extra wrapper element. The ambient scene keeps the rest of the
     screen: `#bg`, `#crt`, `#cards`, `#top`, `#fx` and `#overlay` are fixed
     SIBLINGS of `#app`, not part of its grid, so they still span the viewport.
     This lives in section 14 rather than in the A20 breakpoint on purpose: the
     caps are inert at 1280x800 and below, so the 1180x820 tablet this block was
     written for renders byte-for-byte what it did before, and the arrangement a
     wide screen gets is the tablet's — one client, one layout, no desktop-only
     branch (D19). game.js#setCW clamps its landscape terms to the same box so a
     card is sized against the stage, not the screen.
     The stage is capped in HEIGHT too (controller ruling on c6d250d, amending
     A20) — but that half lives in SECTION 15, not here, and the split is
     load-bearing: 1180x820 is TALLER than the 800 cap, so writing `max-height`
     in this block took 20 px off the tablet (measured: `#casc` 358 -> 338,
     `.actions` top 728 -> 718). The width cap is inert at 1180 and stays here,
     where it also covers a touch tablet wider than 1280; the height cap belongs
     to the pointer-fine breakpoint, which the tablet never matches. */
  #app{max-width:1280px;margin-inline:auto}
  .tabbar{width:var(--rail);grid-template-columns:1fr;grid-template-rows:repeat(6,56px);gap:6px;align-content:start;min-height:0;box-shadow:1px 0 0 rgba(255,255,255,.08)}
  .tab{min-height:56px}
  #screen>.pnl{overflow:visible}                     /* #screen scrolls instead — see section 6 */
  #screen{padding:var(--s3)}
  /* The 320 + main split (§4.2, LANDSCAPE-TABLET-AUDIT §11.3). `#screen`
     itself is the splitter so any hub screen can opt in by emitting a second
     top-level child and having game.js set `.has-side` (the market, below; the
     deck screen left it in P4 Task 12 — one panel whose groups flow into
     columns). Areas rather than source order.
     `minmax(0,1fr)` on the single row is what lets both columns bound their
     own scroll instead of the row growing to the taller one's content. */
  #screen.has-side{display:grid;grid-template-columns:320px 1fr;grid-template-rows:minmax(0,1fr);grid-template-areas:"side main";gap:var(--s3)}
  /* The market is the second `.has-side` screen: its header block takes the
     rail and the plates take the 716 px column (§4.4 — measured 704 here,
     the audit's number minus `#screen`'s own padding), where section 9b's
     `auto-fill` grid lands on two per row. `#screen>.pnl{overflow:visible}`
     above hands the scroll to `#screen`, which a two-column grid row cannot
     do, so the panel takes its own back. */
  #screen.has-side>.mk-hd{grid-area:side;min-height:0;overflow-y:auto;overscroll-behavior:contain}
  #screen.has-side>.mk-pnl{grid-area:main;min-height:0;overflow-y:auto;overflow-x:hidden}
  /* THE DECK TAKES THE ROOM.
     The market was the second `.has-side` screen, and the 320 px rail cost the
     belt a quarter of its length: at 1280 the stage was 772 px for ten cards,
     i.e. an 81 px tile — a domino, whatever height it is given. A conveyor is a
     LENGTH; give it the whole column (1140 px, a 122 px card) and put the rail's
     four parts on one console bar above it. Nothing is dropped and no string is
     added — title, the clock, the rules note, MY BIDS and the live wire are the
     same elements `.mk-hd` already held, laid out in a row instead of a
     three-quarters-empty column. */
  #screen.has-side:has(>.mk-pnl){grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr);grid-template-areas:"side" "main"}
  #screen.has-side>.mk-hd{grid-area:side;min-height:0;overflow:visible;display:grid;grid-template-columns:auto minmax(220px,1fr) auto minmax(240px,1.3fr);align-items:center;gap:var(--s3);padding:var(--s2) var(--s3);background:linear-gradient(180deg,#101C31,#0A1525);box-shadow:inset 0 0 0 1px rgba(150,195,250,.12),inset 0 -2px 0 rgba(90,150,220,.28)}
  #screen.has-side>.mk-hd .hub-hd-top{display:contents}
  #screen.has-side>.mk-hd .t-title{flex:none;font-size:var(--t-head);white-space:nowrap}
  /* The rules note becomes a plaque only HERE. On a phone the same padding
   narrows the text box by 12 px, which is a whole extra line in French
   (measured: `.mk-hd-note` 61 -> 81, the belt 455 -> 436 and the plate row
   down to the 44 px tap floor) — the console has the width to spend and the
   phone does not. */
#screen.has-side>.mk-hd .mk-hd-note{margin:0;padding:var(--s2) var(--s3);background:rgba(8,17,31,.55);box-shadow:inset 0 0 0 1px #2A4166}
  #screen.has-side>.mk-hd .mk-ctl{flex:none}
  /* …and the live wire stops being ONE clipped line: game.js appends and trims
     to four events, and the console has the room to show all four — the whole
     of what the screen keeps. No new string anywhere on this bar; every part
     of it is an element `.mk-hd` already rendered, laid out in a row instead
     of a three-quarters-empty 320 px rail. */
  #screen.has-side>.mk-hd .mk-feed{height:calc(var(--t-label) * 4.4);justify-content:flex-start;padding:var(--s2)}
  .auth-panel,.auth-howto{max-width:480px}
  .pick-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
  /* Title: the same two centred columns as the phone landscape, at full sizes
     (PLAY 56, tiles 56 — i.e. no size override at all), on capped tracks so a
     1180 px viewport does not stretch a button across the whole stage. */
  .title-wrap{display:grid;align-content:center;justify-content:center;min-height:100%;padding:0;column-gap:var(--s4);row-gap:var(--s2);
    grid-template-columns:minmax(0,360px) minmax(0,520px);
    grid-template-areas:"user user" "hero menu" "strip strip"}
  .title-hero,.menu{justify-content:center;margin:0}
  .title-strip{margin:0}
  .menu-grid .btn{min-height:var(--ctl-pri)}
  /* --- 10. match (P4 Task 7, RD26 + C-35): the same two 390 px columns as a phone in landscape,
     centred on both axes — every component at its portrait size, the counter at its floor. */
  #screen.match{display:grid;grid-template-columns:repeat(2,minmax(0,390px));justify-content:center;align-content:center;align-items:start;column-gap:clamp(var(--s2),calc(100vw - 780px),var(--s6));padding:var(--s3) 0}
  #screen.match .m-col{width:auto;min-width:0;margin:0}
  #screen.match .m-hand{flex:none}
  /* --- 11. final (T9) --- three columns: 1180 px of content column holds the
     hero across the top and the five panels on two rows below it.
     The first two columns carry FLOORS. Section 11's five-digit drive tracks
     cost the drive cell 36 px, and an equal third of a 1024×768 tablet left it
     99 px (135 before), 78 at 960×600: three-line rows, and the French chain
     drawn past its cell (review of fix/final-screen-numbers). 338 is the 120 px
     the drive cell needs for two lines — its widest unbreakable lines,
     measured, are « 14815 YDS · 4 TD » at 113 and « · chaîne 16 ×1.40 » at
     108 — plus the row's fixed 218 (panel 4 + 12 + 16, row 2 × 8, tracks
     34 + 52 + 72, three 4 px gaps). The factors keep 296, the equal third they
     had at 960×600, the narrowest tablet in landscape (Nexus 7; Fire HD 8 is
     962×601): under that 114 px label a coach line such as « Tempo Builder
     (D2: 1.10²) » takes a third line and clamps. So the coin ledger pays, 254
     px at 960, still one line a row. From 1086 px wide an equal third passes
     both floors and the three `1fr` share as before: 1133 and 1180×820, the
     audited size, lay out exactly as they did.
     The `min(…, 40% / 33%)` caps are for narrow DESKTOP windows: redirect.js
     sends anything under 900 px wide to /m/, and one 501 px tall or more is
     this breakpoint. Together the floors never pass 73 % of the grid, so the
     columns always fit and nothing scrolls sideways; the caps bind only under
     ~945 px, where every column was already too narrow for its content. */
  .fin{grid-template-columns:minmax(min(338px,40%),1fr) minmax(min(296px,33%),1fr) minmax(0,1fr);
    grid-template-areas:"hero hero hero" "quarters levels coins" "share ranked ranked"}
  /* --- 7b. draft --- same two-column split as the phone in landscape, for the
     same reason (a wide, short content box), at the tablet's card column. */
  .draft-wrap{--dcw:96px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start;gap:var(--s4)}
  .draft-wrap.draft-intro{grid-template-columns:minmax(0,1fr)}
  .draft-hd{grid-column:1 / -1}
  .keep-wrap{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:start;gap:var(--s4)}
  .keep-sec{grid-column:2;grid-row:1 / span 2}
}

/* ==== 15 large screen (A20) — the FOURTH and last breakpoint, the one A20 adds
   on top of A12's three: `(min-width: 1024px) and (pointer: fine)`, narrowed to
   landscape. Everything that is not pointer-specific was placed elsewhere — the
   width half of the stage in section 14 (inert at <= 1280), the focus ring
   unscoped in section 4 — so what is left here is what a mouse-driven 1280+
   screen needs and a touch one must not get. `pointer: fine` also stands in for
   `hover: hover`: A20 allows ONE extra query, not two, and a fine pointer at
   1024 px and up is a mouse.
   `and (orientation: landscape)` is NOT decoration. Without it the height cap
   below fires on a pointer-fine PORTRAIT window — a rotated monitor at
   1250x2000 — where section 14 does not apply, so the stage came out 1250 wide
   and 800 tall: a letterboxed band across the middle of the screen (measured
   `[0, 600, 1250, 800]`). The whole block is landscape-only rather than the one
   declaration, because A12 allows no second query to split them; the cost is
   that such a window keeps full-width sheets and no hover extras, which is
   exactly what it has on `main`. Portrait on a large screen is outside A20's
   measured set and is left unchanged rather than half-changed. ==== */
@media (min-width: 1024px) and (pointer: fine) and (orientation: landscape){
  /* Sheets: 720 and centred, still anchored to the bottom edge — a sheet stays
     a sheet, it just stops being a 2000 px band. `#overlay` already centres on
     the cross axis (section 5), so the cap alone does the centring. The how-to
     grid is `repeat(auto-fit,minmax(280px,1fr))`: inside 720 minus 40 of sheet
     padding it resolves to TWO columns (three would need 872), where the
     uncapped sheet gave it seven at 2000. No `.howto-steps` override is needed
     and none is written. */
  #overlay .modal{max-width:720px;max-height:min(86dvh,744px)}
  /* `#overlay` is a viewport-fixed layer (section 6) and its sheets are
     bottom-aligned, so once the stage is centred vertically a sheet would rise
     from the VIEWPORT floor — up to 225 px of ambient scene below the stage at
     2000x1250, with the sheet's foot floating in it. Pad the layer by exactly
     the stage's own bottom offset, `(viewport - stage) / 2`, so the sheet lands
     on the stage's bottom edge at every height; `max()` keeps it at 0 for a
     viewport shorter than the cap. Same arithmetic as the `margin:auto` above,
     written out because a fixed layer has no box to inherit it from. */
  #overlay{padding-bottom:max(0px,calc((100dvh - 800px) / 2))}
  /* The other half of the stage: 800 tall, centred on the block axis. Section
     14 carries the width cap (inert at the 1180 tablet); this one CANNOT go
     there, because 820 > 800 and the tablet would lose 20 px of column. Why a
     height cap at all: the landscape arrangement was authored for 393-820 px of
     it, and an uncapped 1080 stage stretched it — `.m-play{flex:1;
     justify-content:center}` pushed the play zone 244 px below `#mTop` (104 at
     800, 119 on the tablet) and `#casc` grew 316 -> 596, floating the trace
     mid-column instead of over the action bar. With the cap, the match geometry
     at 1920 and at 2000 is the 1280x800 geometry translated to the centre —
     asserted box-for-box in tests/large-screen.spec.js.
     `margin-block:auto` centres it for the same reason `margin-inline:auto`
     does horizontally: `#app` is `inset:0`, so top/height/bottom are
     over-constrained and auto margins resolve equal (CSS 2.1 §10.6.4). */
  #app{max-height:800px;margin-block:auto}
  /* --- 10. match (P4 Task 7, RD26 + C-35): the same two 390 px columns as a phone in landscape,
     centred on both axes — every component at its portrait size, the counter at its floor. */
  #screen.match{display:grid;grid-template-columns:repeat(2,minmax(0,390px));justify-content:center;align-content:center;align-items:start;column-gap:clamp(var(--s2),calc(100vw - 780px),var(--s6));padding:var(--s3) 0}
  #screen.match .m-col{width:auto;min-width:0;margin:0}
  #screen.match .m-hand{flex:none}
  /* Hover parity. /shared already lifts `.btn` (brightness + a 1 px rise),
     `.lrow` and `.mini`; these are the controls that had nothing, and each
     borrows the affordance of the family it belongs to rather than inventing a
     look. `.card` is deliberately absent: cards.js#initTilt gives it a 3D tilt
     on pointermove, a stronger hover response than any of these. */
  .tab:hover{color:var(--cream)}
  button.title-tile:hover,button.sheet-card:hover{filter:brightness(1.12);transform:translateY(-1px)}
  .plate:hover{box-shadow:inset 0 0 0 2px rgba(255,255,255,.3)}
  /* The carrier's own hover. It lives HERE and not beside the carrier in
     section 9b for the reason this whole block exists: a touch client has no
     hover to leave, and on iOS a tap leaves `:hover` stuck on the last card
     touched — on a belt, that is a card that has since moved on. Colour and
     shadow only: the plate's transform belongs to game.js#beltFrame, so a
     hover can never nudge a moving target out from under the cursor. */
  #belt[data-belt="h"] .plate.mp:has(.mp-hit:hover:not([disabled])){filter:drop-shadow(0 14px 18px rgba(2,6,14,.6)) drop-shadow(0 0 14px rgba(120,190,255,.30))}
  #belt[data-belt="h"] .mp-hit:hover:not([disabled]) .mp-state{background:linear-gradient(180deg,#FFDC77 0%,var(--gold) 100%)}
  #belt[data-belt="h"] .plate.mp.leading .mp-hit:hover:not([disabled]) .mp-state{background:linear-gradient(180deg,#63E08C 0%,var(--green) 100%)}
}

/* ==== 15 finish fix wave (P4 hub screens, impeccable finish review 2026-09-28) ====
   What the review asked of every hub screen, in one place; each block names its item. */

/* Item 7 — drawn icons, one stroke (screens.js#icon*): sized by their control, never a glyph.
   A chevron follows its label; a tag's icon leads its text. */
.btn svg,.tag svg,.star-badge svg{flex:none;display:block}
.btn:not(.icon) svg{margin-left:var(--s1)}
.tag svg{display:inline-block;vertical-align:-2px;margin-right:3px}
/* The TEXT ACTION: a step aside (LATER, WITHDRAW) that must never weigh what the primary
   beside it weighs — no plate, no bevel, an underlined label, still a 44 px target. */
.btn.link{min-height:var(--ctl-sm);padding:0 var(--s2);background:none;box-shadow:none;text-shadow:none;clip-path:none;filter:none;color:var(--dim);font:700 var(--t-body)/1 var(--ui);letter-spacing:.06em;text-transform:uppercase;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:4px;align-self:center}
.btn.link:hover{color:var(--cream)}
.btn.link.danger{color:#FF8A80}
.btn.link.danger:hover{color:#FFB3AD}
.btn.link[disabled]{background:none;box-shadow:none;opacity:.5}
/* A disabled ICON control keeps its plate, dimmed — not the empty ghost of `/shared`'s
   `.btn[disabled]` (the coach row's first ◂ and last ▸). */
.btn.icon.grey[disabled]{background:linear-gradient(180deg,#42536F 0%,#2A374E 100%);box-shadow:none;color:var(--cream);opacity:.35}

/* Item 8 — the draft's PWR tier is the card's FRAME: gold for the top tier, silver for the
   middle, the face's own steel for the rest (the aria-label says it in words). */
.draft-card[data-tier="high"] .bface .face{box-shadow:inset 0 0 0 2px var(--gold),0 0 14px rgba(255,196,45,.35),0 8px 16px rgba(0,0,0,.55)}
.draft-card[data-tier="mid"] .bface .face{box-shadow:inset 0 0 0 2px #C8D2E4,0 8px 16px rgba(0,0,0,.55)}
.draft-card:focus-visible{outline:2px solid var(--cream);outline-offset:3px}
.draft-later,.keep-later{margin-top:calc(-1 * var(--s1))}
/* Item 8 — the title's five screens use the tab bar's words; the odd fifth spans its row. */
.menu-grid>.btn:last-child:nth-child(odd){grid-column:1 / -1}
/* Item 4 — under the minimum the alert carries the way back (the Kaiten). */
.deck-needs .deck-to-market{align-self:flex-start}
.release-drop{margin:0}

/* Item 3 — the bid sheet: a scrolling body over a pinned footer. The sheet itself stops
   scrolling; `.bid-scroll` does, and PLACE BID / RAISE MAX sit in `.sheet-foot` under it
   whatever the release picker holds. */
#overlay #bidOverlay{overflow:hidden}
#bidOverlay .bid-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:var(--s3)}
#bidOverlay .sheet-foot{flex:none;padding-top:var(--s2);box-shadow:0 -1px 0 rgba(255,255,255,.08)}
#bidOverlay .sheet-foot .btn.gold{min-height:var(--ctl-std)}
#bidOverlay .bid-card{--cw:70px;--ch:100px;gap:var(--s3);align-items:flex-end}
.bid-max-hd{align-items:center}
.bid-help{margin:0}
.bid-row input{flex:1 1 96px;min-width:0}
/* The release picker shows about three and a half rows — the half row says "more below" —
   with the bottom edge fading out; the list scrolls on its own. */
.bid-rel-list{max-height:calc(var(--ctl-sm) * 3.5 + var(--s2));padding-bottom:var(--s4);-webkit-mask-image:linear-gradient(180deg,#000 calc(100% - var(--s5)),transparent);mask-image:linear-gradient(180deg,#000 calc(100% - var(--s5)),transparent)}
.bid-release>p{margin:0}

/* Item 5 — My Bids: the current price is a tag inside the row, beside the status. */
.mybid-row .bid-now{color:var(--gold2)}

/* Item 5 — the week board: the days line is short (« 5/5 j ») and sits under the score, so
   the name keeps the row's width. Item T15 — the seven day keys are 44 px wide at least and
   wrap to two rows (4 + 3) where seven do not fit. */
.rank-num>.t-label{white-space:nowrap}
.rk-root{container-type:inline-size}
.rk-days{display:grid;grid-template-columns:repeat(7,minmax(var(--ctl-sm),1fr));gap:var(--s1)}
.rk-days .btn{min-width:var(--ctl-sm);width:100%;padding:0}
@container (max-width: 340px){.rk-days{grid-template-columns:repeat(4,minmax(var(--ctl-sm),1fr))}}
.rk-closed{margin:0}

/* Item 6 — a coach the coins cannot upgrade says by how much. */
.coach-short{margin-top:calc(-1 * var(--s1))}

/* Item 2 — Sunday Live: YARDS (or a QB's / a Line's +MULT) now → next is the row's figure,
   the unit under it; the delta is next − now. */
.sun-stat b{font-variant-numeric:tabular-nums}
.sun-status .sun-dot{background:currentColor;margin-right:var(--s1)}
.sun-chips .tag{display:inline-flex;align-items:center}
