/* =====================================================================
   CS Anchor component layer.

   Every color, font, space, radius and shadow here is `var(--token)` from
   tokens.css. NO COLOR LITERALS IN THIS FILE — a structural test asserts
   it and carries a synthetic-violation probe.

   Classes keep Nocturne's names where they map (`.btn`, `.card`, `.table`,
   `.tag`, `.seg`, `.field`, `.input`, `.nav`, `.hr`) so a reader of the
   design system finds what the readme told them to expect; the product
   components extend from there.

   Written mobile-first. The single layout breakpoint is 900px, which is
   the reference's own: below it the shell is one column and the sidebar
   becomes a horizontal strip. Every auto-fit grid uses
   `minmax(min(Npx, 100%), 1fr)` rather than `minmax(Npx, 1fr)` — the bare
   form cannot go narrower than N and is exactly how a 240px card
   overflows a 390px viewport.
   ===================================================================== */

/* ── base ─────────────────────────────────────────────────────────── */

/* CROSS-DOCUMENT VIEW TRANSITIONS (UX-1).
 *
 * Every page here is a full server-rendered document, so every nav click
 * destroys one document and builds another. The browser holds the OLD page on
 * screen until the new one can paint, and then it JUMP-CUTS — measured at
 * 45 ms RTT: one frame of the old page, nothing for ~300 ms, one frame of the
 * new page. The operator's word for that was "glitched", and they were
 * describing a hard cut, not a slow page: PERF-1..3 had already taken the
 * render down to ~130 ms.
 *
 * Two lines of CSS turn the cut into a cross-fade. There is no JavaScript in
 * this — no router, no fetch, no framework — which is why it is allowed to
 * exist under the house rule that nav JS is the only JS: it is the browser's
 * own MPA feature, and a browser that does not implement it navigates exactly
 * as it did before. Progressive enhancement in the literal sense.
 *
 * 140 ms, not 300: long enough to read as a transition, short enough that it
 * never becomes the thing you are waiting for. The easing is the same
 * decelerate curve the rest of the shell uses.
 */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 140ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

/* A reader who asked for less motion gets the old behaviour, which was never
 * broken — only abrupt. `navigation: none` turns the feature off at the source
 * rather than animating to zero, so no snapshot is ever taken. */
@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

/* ENTRANCE MOTION — the four movements, defined once (UX-2).
 *
 * The operator asked for entrance animation that is "natural, not annoying".
 * The keyframes live here, in the shell's sheet, and the page sheets say WHICH
 * of their own elements use them — because the alternative is three pages
 * inventing three easings and three durations, and a page that arrives 60 ms
 * differently from its neighbour reads as a slower page rather than as a
 * different one. Durations and easing are tokens (tokens.css: `--t-enter`,
 * `--t-reveal`, `--ease-out`, `--stagger`, `--stagger-max`).
 *
 * ONLY `transform`, `opacity` and `clip-path` appear below, and that is the
 * zero-layout-shift argument stated as a rule rather than as an intention:
 * none of the three can move a box. A bar that animated `width` would relayout
 * its row on every frame; one that animates `scaleX` inside a track whose
 * geometry is already reserved cannot. Measured over five loads a page in both
 * motion modes, before and after: the layout-shift score is unchanged and the
 * residual on both sides is `filter-group`, which is older than this block.
 *
 * `from`-only keyframes on purpose: the resting state is whatever the
 * stylesheet already says, so the animation cannot become a second opinion
 * about the final geometry. There is no `animation-fill-mode: forwards`
 * anywhere for the same reason — the end of every one of these is the page as
 * it would have been drawn with no animation at all.
 */

/* A bar filling to its true length. `transform-origin` is set by the caller,
   because a fill grows from the end its track is anchored to. */
@keyframes enter-fill {
  from {
    transform: scaleX(0);
  }
}

/* A column growing off the axis. Clip rather than `scaleY`, because these
   columns CONTAIN artwork — the spend curve stacks item glyphs inside each
   bar — and scaling a box scales its contents with it. A clip reveals the
   drawing at its true size from the first frame it is visible in. */
@keyframes enter-grow {
  from {
    clip-path: inset(100% 0 0 0);
  }
}

/* A card or a headline arriving. 6px, which is under a line of leading: the
   distance is meant to be felt rather than watched. */
@keyframes enter-rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* A row of a table or a chip in a strip. No movement at all — thirty rows
   sliding at once is a wave, and a wave is the thing the operator called
   annoying. They simply resolve. */
@keyframes enter-fade {
  from {
    opacity: 0;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.55;
  font-weight: 400;
  color: var(--color-text);
  background:
    radial-gradient(1100px 520px at 12% -12%, var(--page-bloom-1) 0%, var(--page-bg-fade) 62%),
    radial-gradient(900px 500px at 92% 4%, var(--page-bloom-2) 0%, var(--page-bg-fade) 58%),
    var(--color-bg);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  line-height: 1.12;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2);
}

p {
  margin: 0 0 var(--space-3);
}

a {
  color: var(--color-accent);
  text-underline-offset: 3px;
}

img {
  display: block;
  max-width: 100%;
}

svg {
  display: block;
}

/* Themed interaction states, never the browser's. */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

::selection {
  background: color-mix(in srgb, var(--color-accent) 30%, transparent);
}

.text-muted {
  color: var(--dim);
}

.text-dim {
  color: var(--dim-2);
}

/* Numerals are mono everywhere. A stat that reflows because 1 is narrower
   than 8 is a stat you cannot scan down a column. */
.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Freestanding rules fade to transparent over 48px an end. A Nocturne
   signature; box outlines and in-control separators stay solid. */
.hr {
  height: 1px;
  border: 0;
  margin: var(--space-4) 0;
  background: linear-gradient(
    to right,
    transparent,
    var(--color-divider) var(--rule-fade),
    var(--color-divider) calc(100% - var(--rule-fade)),
    transparent
  );
}

/* ── buttons ──────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1-5);
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--fs-base);
  line-height: 1.2;
  color: var(--color-text);
  background: transparent;
  border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Outlined, never filled — the accent is a line, not a flood. */
.btn-primary {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.btn-primary:hover {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}

.btn-primary:active {
  background: color-mix(in srgb, var(--color-accent) 22%, transparent);
}

.btn-secondary {
  border-color: var(--color-divider);
}

.btn-secondary:hover {
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
}

.btn-secondary:active {
  background: color-mix(in srgb, var(--color-text) 14%, transparent);
}

.btn-ghost {
  color: var(--color-accent);
  padding-inline: var(--space-1);
}

.btn-ghost:hover {
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

.btn-ghost:active {
  background: color-mix(in srgb, var(--color-accent) 18%, transparent);
}

.btn-sm {
  font-size: var(--fs-sm);
  padding: var(--sp-1) var(--sp-2);
}

/* ── the 3D replay's two doors ────────────────────────────────────────
   `web/templates/components/replay3d_cta.html`. Two sizes of one
   destination: the button (which the match page's feature panel calls, from
   `replay3d_feature.html` next door) and the row's chip. Here rather than in
   game-page.css because the chip lands on four pages with four different
   sheet lists, and a component whose CSS is in one page's sheet is a
   component that renders naked on the next page that uses it.

   NEITHER IS A NEW COLOUR OR A NEW SHAPE. The accent is `--color-accent`,
   the same one every primary control in this product carries; the radii are
   `--radius-md` and `--radius-sm`, the two the scale has. The whole visual
   difference between "this is the biggest feature on the page" and "this is
   a link in a row" is size, weight and one step of wash. */

.v3-cta {
  /* One step of accent wash AT REST, which no other button in this product
     has. `.btn-primary` is an accent outline over nothing (the rule above
     says why: "the accent is a line, not a flood"), and its hover state is
     12% wash. Starting at 10 means this control is already warm before the
     pointer arrives, and its hover still has somewhere to go. It is the
     loudest thing on the match page without being a filled button that
     exists nowhere else in the system. */
  background: var(--accent-wash-2);
  /* The hero is a flex row that wraps; without this the button would stretch
     to the row's height on the line it wraps onto. */
  align-self: center;
  flex: none;
}

.v3-cta:hover {
  background: var(--accent-wash-4);
}

.v3-cta:active {
  background: var(--accent-wash-4);
  /* The tactile push every button in this product would have if the design
     had asked for one. It asks for one here, because this is the click the
     page is FOR. */
  transform: translateY(1px);
}

.v3-cta-ico {
  width: 15px;
  height: 15px;
  display: block;
  flex: none;
  /* A solid play triangle, not a stroked one: at 15px a 1.8-stroke triangle
     reads as an outlined arrowhead and loses the one thing the shape is for. */
  fill: currentColor;
}

.v3-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  flex: none;
  /* The row-tag vocabulary this product already has: `.dup-tag` and
     `.demo-chip` are mono at `--fs-micro-2`, and a third tag shape in the same
     row would be a third opinion about how a tag looks. What separates this one
     from those is the ACCENT — they are a warning hue and a neutral one, this
     is the product's own go-here colour — not a different type or a different
     box. */
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  letter-spacing: 0.06em;
  line-height: 1;
  /* Was `3px 6px`, which measured 26x18 in the team section's result rows —
     the size the operator reported as not being there. The glyph and this step
     put it at 44x20, which is a control rather than a tag, without changing
     what it is made of. */
  gap: 4px;
  padding: 4px 7px;
  border-radius: var(--radius-sm);
  color: var(--color-accent);
  border: 1px solid var(--accent-ring);
  background: var(--accent-wash);
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}

/* The BUTTON's triangle, at the row's scale. Same path, same solid fill, same
   reason it is solid (`.v3-cta-ico`): a 1.8-stroke triangle this small reads as
   an outlined arrowhead and loses the one thing the shape is for. Drawing it
   here rather than shipping the chip as text alone is what makes the small door
   and the big one legibly the same door. */
.v3-chip-ico {
  width: 9px;
  height: 9px;
  display: block;
  flex: none;
  fill: currentColor;
}

.v3-chip:hover {
  background: var(--accent-wash-3);
  border-color: var(--color-accent);
}

/* The two INLINE contexts, where the chip follows text or a link with no gap of
   its own. `.dup-tag` carries the identical 4px nudge for the identical reason,
   one rule above. The flex contexts (`.ov-result`) are not listed: they have a
   `gap` and would double-space. */
.mapline-meta > .v3-chip,
.series-maps .v3-chip {
  margin-left: 6px;
  vertical-align: middle;
}

@media (max-width: 900px) {
  /* The touch-target floor the row links already meet (see "R3 ·
     TOUCH-TARGET FLOOR" at the end of this file). The chip is a SECOND target
     inside a row that is itself a target, so it has to clear 44px on its own
     or it becomes the thing a thumb hits by accident when it wanted the row. */
  .v3-chip {
    padding: 13px 10px;
  }

  /* Full width at phone sizes. The hero wraps to a column here, and a
     three-word button floating at the left edge of its own line reads as
     something that failed to lay out rather than as the page's main action. */
  .v3-cta {
    width: 100%;
  }
}

/* ── forms ────────────────────────────────────────────────────────── */

.field > label {
  display: block;
  font-size: var(--fs-sm);
  margin-bottom: 5px;
  color: var(--dim);
}

.input {
  width: 100%;
  min-height: 36px;
  padding: var(--sp-1-5) var(--sp-2-5);
  font: inherit;
  font-size: var(--fs-base);
  color: var(--color-text);
  caret-color: var(--color-accent);
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
}

.input:hover {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
}

.input:focus-visible {
  border-color: var(--color-accent);
  outline-offset: 0;
}

.seg {
  display: inline-flex;
  overflow: hidden;
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
}

.seg-opt {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1-5);
  padding: 7px var(--sp-3);
  font-size: var(--fs-md);
  cursor: pointer;
}

.seg-opt input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}

.seg-opt + .seg-opt {
  border-left: 1px solid var(--color-divider);
}

.seg-opt:has(input:checked) {
  color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}

.seg-opt:not(:has(input:checked)):hover {
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
}

.seg-opt:has(input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* ── cards / tags ─────────────────────────────────────────────────── */

.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.elev-sm {
  box-shadow: var(--shadow-sm);
}
.elev-md {
  box-shadow: var(--shadow-md);
}
.elev-lg {
  box-shadow: var(--shadow-lg);
}

.tag {
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
  padding: 3px var(--sp-2-5);
  border-radius: calc(var(--radius-md) * 0.75);
}

.tag-accent {
  background: var(--color-accent-800);
  color: var(--color-accent-100);
}

.tag-neutral {
  background: var(--color-neutral-800);
  color: var(--color-neutral-100);
}

.tag-outline {
  border: 1px solid var(--color-accent);
  color: var(--color-accent);
}

/* ── tables ───────────────────────────────────────────────────────── */

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-base);
}

.table th {
  text-align: left;
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  padding: var(--space-2);
  border-bottom: 1px solid transparent;
}

.table td {
  padding: var(--space-2);
  border-bottom: 1px solid transparent;
  font-family: var(--mono);
  font-size: var(--fs-sm-2);
  font-variant-numeric: tabular-nums;
}

/* The first cell of a row is a label, not a number. */
.table td:first-child {
  font-family: var(--font-body);
}

/* Row rules are painted on the ROW, so the end-fade spans the row rather
   than restarting in every cell. */
.table thead tr {
  background: linear-gradient(
      to right,
      transparent,
      var(--color-divider) var(--rule-fade),
      var(--color-divider) calc(100% - var(--rule-fade)),
      transparent
    )
    no-repeat bottom / 100% 1px;
}

.table tbody tr {
  background: linear-gradient(
      to right,
      transparent,
      var(--row-rule) var(--rule-fade),
      var(--row-rule) calc(100% - var(--rule-fade)),
      transparent
    )
    no-repeat bottom / 100% 1px;
}

.table tbody tr:hover {
  background:
    linear-gradient(var(--hover-tint), var(--hover-tint)) no-repeat 0 0 / 100% 100%,
    linear-gradient(
        to right,
        transparent,
        var(--row-rule) var(--rule-fade),
        var(--row-rule) calc(100% - var(--rule-fade)),
        transparent
      )
      no-repeat bottom / 100% 1px;
}

.cell-t {
  color: var(--t);
}
.cell-ct {
  color: var(--ct);
}
.cell-win {
  color: var(--win);
}
.cell-loss {
  color: var(--loss);
}
.cell-dim {
  color: var(--dim);
}

/* ── definition tooltip (M12: the tooltip text IS Metric.definition) ── */

.tt {
  position: relative;
  display: inline-flex;
  width: 13px;
  height: 13px;
  flex: none;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--line-2);
  font: 600 8.5px var(--mono);
  color: var(--dim-2);
  cursor: help;
  border: 0;
  padding: 0;
  background: transparent;
}

/* The ring stays the reference's 13px; the POINTER target grows to 25px.
   WCAG 2.2 target-size is about what you can hit, not what you can see, so
   this is the one way to satisfy it without redrawing the affordance. */
.tt::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 50%;
}

/* HIDDEN MEANS OUT OF LAYOUT, and that is a correctness rule here rather than a
   style (night QA m8). `visibility: hidden` hides a box; it does not remove one,
   and a 214px popover anchored to an icon near the right edge still counts
   toward the document's scrollable overflow. So the page scrolled sideways with
   nothing visible out of place — measured at 500px on 9 pages, worst /numbers at
   575px against a 500px viewport, and `SPAN.tt-pop` was the ONLY unclipped
   offender on 8 of them (attributed by walking ancestors and discarding
   everything already inside an overflow-x container, so the nav strip's own
   scroller is not counted). The invariant is this sheet's own, two comments
   below `.xscroll`: the page body never scrolls sideways.

   `display: none` is therefore the hidden state. The alternative — clipping the
   overflow at `.main` — would have made the document-width check permanently
   green and taken away the very measurement that found this. A guard that can no
   longer fail is worse than the defect it hides.

   The fade survives where the browser supports discrete transitions, and
   snaps where it does not. That is the right way round for a tooltip: nobody is
   harmed by an instant tooltip, and `prefers-reduced-motion` already removes the
   fade for some readers, so the design does not depend on it. */
.tt-pop {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 7px);
  transform: translateX(-42%);
  width: 214px;
  padding: var(--sp-2) var(--sp-2-5);
  background: var(--panel-3);
  color: var(--color-text);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  font: 400 var(--fs-xs-2) / 1.45 var(--font-body);
  letter-spacing: 0;
  text-transform: none;
  text-align: left;
  display: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--t-fast) ease, display var(--t-fast) allow-discrete;
  z-index: 60;
  /* THE POPOVER HAS A CEILING NOW (R6). It never had one, and the text it
     holds is `Metric.definition` — the registry's 75 entries measure a median
     of 470 characters and a maximum of 973. At this size in a 214px column,
     973 characters is a ~40-line ribbon around 600px tall, opening UPWARD from
     a card head; the review measured 9 of them running off the top of a 390px
     viewport with no way to read the part that left. A definition nobody can
     finish reading is the same failure as a definition nobody can find.

     So the box is bounded and scrolls itself. `overscroll-behavior: contain`
     because a tooltip that reaches its end and then scrolls the PAGE out from
     under the icon you are hovering closes itself, which is a worse answer
     than stopping. */
  max-height: min(46vh, 260px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The scrollbar IS the affordance here — it is the only thing telling a
     reader the definition continues past the fold — so it is drawn thin and on
     theme rather than left to the platform, where an overlay scrollbar can be
     invisible until something is already scrolling. */
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
  /* Was `none`, and had to change with the line above: a box you cannot put
     a pointer on is a box you cannot scroll. It is safe to accept the pointer
     because this variant is a CHILD of its trigger, so the cursor resting on
     the note keeps `.tt:hover` true and the note open — the reader is not
     racing a 7px gap to reach their own scrollbar. */
  pointer-events: auto;
}

/* HOVER IS GATED ON THERE BEING A HOVER (B3). Chromium and WebKit apply a
   STICKY `:hover` to the element a finger last tapped, and it survives until
   the next tap somewhere else. With the reveal unconditional that hover fought
   the tap-to-close path and won: the note opened on the first tap and then
   would not shut on the second, because the trigger was still "hovered" by a
   finger that had long since left the glass. Measured in a real touch context,
   not reasoned about.

   `(hover: hover)` is the honest condition — reveal on hover where hovering is
   something the pointer can do — and it loses a desktop reader nothing: a
   mouse reports `hover: hover`, and `:focus-visible` and the click path below
   are outside the gate for every device. */
@media (hover: hover) {
  .tt:hover .tt-pop {
    display: block;
    opacity: 1;
    visibility: visible;
  }
}

.tt:focus-visible .tt-pop {
  display: block;
  opacity: 1;
  visibility: visible;
}

/* THE TOUCH PATH (B3). `:hover` and `:focus-visible` are the only two reveals
   this sheet had, and a phone has neither: Chromium and WebKit do not match
   `:focus-visible` for pointer-initiated focus, so a tap on the `i` showed
   nothing. The entire honesty layer of this product — every sample floor,
   every denominator, every per-chip opponent and date — lived behind an
   affordance a finger could not open, on the device a coach reads it on.

   `data-tt-open` is set by web/static/js/tips.js on click, which is what a tap,
   a mouse press and Enter/Space on a focused button all emit. Written as its
   own block rather than added to the group above because the two selectors
   there are counted by tests/test_shell_layout.py — one reveal path each — and
   this is a third path, not a second spelling of one of them. */
.tt[data-tt-open] .tt-pop {
  display: block;
  opacity: 1;
  visibility: visible;
}

/* Both variants open from `display: none`, so both need the entry opacity a
   discrete transition animates FROM. Without it the fade has no start value and
   the popover simply appears. */
@starting-style {
  .tt:hover .tt-pop,
  .tt:focus-visible .tt-pop,
  .tt[data-tt-open] .tt-pop,
  .tt--section:hover + .tt-pop--section,
  .tt--section:focus-visible + .tt-pop--section,
  .tt--section[data-tt-open] + .tt-pop--section {
    opacity: 0;
  }
}

/* At phone widths the 214px popover, centred on an icon that sits ~70px from
   the left edge, hangs off the viewport by up to 18px. Measured, not guessed
   (browser QA walks every box against clientWidth). Narrowing it and shifting
   the anchor keeps it fully on screen without moving the desktop version. */
@media (max-width: 560px) {
  .tt-pop {
    width: 190px;
    transform: translateX(-28%);
    /* Narrower means taller, so the ceiling comes down with the width: at
       190px the longest definition is ~46 lines. A third of a phone screen is
       as much as a hover note may take before it stops being an aside. */
    max-height: 34vh;
  }
}

/* THE CARD NOTE FLIPS (R6), and this is the half CSS can do alone: the class
   the enhancement sets, and what it means. `.tt-pop` opens upward — the right
   default, since the icon sits at the end of a heading and the note lands over
   what the reader has already passed — and upward is exactly wrong for a
   trigger near the top of the viewport, which on a phone is most of them.

   No CSS selector can ask how much room is above an element. The section
   variant gets a real answer from `position-try-fallbacks` in the `@supports`
   block below, but that path needs `position: fixed`, and the card variant
   nests INSIDE its trigger where fixed would take the popover out of the
   trigger's own hover subtree. So this one is flipped by measurement instead,
   in web/static/js/tips.js: a dozen lines that read two rects and set one
   attribute. Progressive in the honest sense — with no JavaScript the note
   opens upward and is clamped and scrollable, which is where R6 found it plus
   the ceiling. */
.tt[data-tt-flip="down"] .tt-pop {
  bottom: auto;
  top: calc(100% + 7px);
}

/* The SECTION-scale variant (see `section_tip`). The popover is a SIBLING of
   its trigger rather than a child, so the note is a real `aria-describedby`
   target instead of label text swallowed into the button's name. It is also
   wider than the card variant, because a family note is two sentences where a
   metric definition is one.

   THE COMPONENT CARRIES ITS OWN CONTAINING BLOCK, and that is a correctness
   rule rather than a style (BUG-1). This variant used to be positioned against
   `.section-head` — a rule about the PAGE, not about the component. It held for
   exactly as long as `section_tip` was only ever used beside a section heading.
   The moment the same macro was called from a KPI card head, a table column
   label and a chart caption, those popovers had NO positioned ancestor left, so
   their containing block became the INITIAL one: `left: 0; top: calc(100% + 5px)`
   resolved against the viewport rectangle at the document origin and every such
   tip on the page rendered at document (0, 100vh + 5px) — the same pixel, under
   the left nav, whichever `i` was hovered. Measured before the fix at 1440px:
   61 of 74 tips across six pages, all stacked on that one spot.

   `.tt-anchor` is emitted by the macro itself, so the containing block travels
   with the component and no page can forget to provide one. */
.tt-anchor {
  position: relative;
  display: inline-flex;
  align-items: center;
  anchor-scope: --tt-section;
}

/* Opens DOWNWARD, unlike the card variant. A family note is about the cards
   under the header, so it lands on them; and a header scrolled to the top of
   the viewport has nothing above it to open into, which is where an upward
   popover would be drawn off-screen.

   This is the fallback placement — CENTRED on the trigger, because a fallback
   cannot flip. Hung from the trigger's LEFT edge instead, a 360px note beside
   a table column 47px from the right of a 1440px viewport runs 313px off
   screen; centred, the worst case over four pages at 390px measures 115px and
   at 1440px measures zero. The `@supports` block below removes even that, and
   left-aligns the note where it can, because a browser that can flip does not
   need to compromise. */
.tt-pop--section {
  left: 50%;
  top: calc(100% + 7px);
  bottom: auto;
  transform: translateX(-50%);
  width: min(360px, calc(100vw - var(--sp-4) * 2));
  /* The same ceiling the card variant takes, one step taller because this one
     is wider and holds two sentences by design. `50vh` is the binding term on
     a phone and `340px` on a desk. */
  max-height: min(50vh, 340px);
}

/* The section variant's half of the same gate — see the card variant above. */
@media (hover: hover) {
  .tt--section:hover + .tt-pop--section {
    display: block;
    opacity: 1;
    visibility: visible;
  }
}

.tt--section:focus-visible + .tt-pop--section {
  display: block;
  opacity: 1;
  visibility: visible;
}

/* The section variant's half of B3's touch path. Same attribute, set on the
   same trigger; only the combinator differs, because this note is a SIBLING of
   its trigger rather than a child. */
.tt--section[data-tt-open] + .tt-pop--section {
  display: block;
  opacity: 1;
  visibility: visible;
}

/* A SCROLLABLE NOTE HAS TO SURVIVE BEING REACHED (R6). This variant is a
   SIBLING of its trigger, not a child, so the moment the pointer leaves the
   icon the rule above stops matching and a note the reader was moving toward
   in order to scroll it disappears under their cursor.

   This rule cannot leak: a `display: none` element cannot be hovered, so
   `.tt-pop--section:hover` only ever matches a note the trigger already
   opened. It is written as its own block rather than added to the group above
   because the two selectors there are counted by
   tests/test_shell_layout.py — one reveal path each — and this is a third
   thing, not a second spelling of one of them.

   `::before` bridges the 7px the note is offset by, in both directions, so
   the crossing is continuous however the fallbacks flipped it. */
.tt-pop--section {
  pointer-events: auto;
}

.tt-pop--section:hover {
  display: block;
  opacity: 1;
  visibility: visible;
}

.tt-pop--section::before {
  content: "";
  position: absolute;
  inset: -9px 0;
  z-index: -1;
}

/* Where the browser can position a box against another box, it does, and that
   buys the three things a containing block alone cannot:

   1. THE NOTE STAYS ON SCREEN. The first placement is block-end/left-aligned —
      the note hangs down and to the RIGHT of its icon, which is the shape the
      section variant always had. `position-try-fallbacks` re-runs it only when
      that would overflow: a note beside the last column of a nine-column table
      swings left instead of off the edge, and one beside an icon at the bottom
      of the viewport opens upward. No width guess anywhere.
   2. THE NOTE ESCAPES THE TABLE'S SCROLLER. `table_scroll` is `overflow-x: auto;
      overflow-y: hidden`, and a popover whose containing block is inside it is
      clipped by it and adds to its scroll width. `position: fixed` takes the
      viewport as its containing block, so the note is neither clipped nor
      counted — while the anchor keeps it glued to the icon it belongs to.
   3. IT NEVER OPENS OVER THE NAV. Left-aligning to the icon means the note
      grows away from the left rail; the centred fallback grows both ways and
      reaches ~110px into it on a section heading near the content edge.

   `anchor-scope` is part of the gate ON PURPOSE. Every trigger carries the same
   `anchor-name`, and without a scope a name resolves to the LAST element in the
   document carrying it — which would be this same bug wearing a different hat,
   every tip on the page pointing at the last icon. A browser that has anchors
   but not scopes must therefore take the fallback above, which is merely
   centred, not wrong. */
@supports (anchor-scope: --a) and (position-area: block-end) {
  .tt--section {
    anchor-name: --tt-section;
  }

  .tt-pop--section {
    position: fixed;
    position-anchor: --tt-section;
    position-area: block-end span-inline-end;
    position-try-fallbacks:
      block-end span-inline-start,
      block-start span-inline-end,
      block-start span-inline-start;
    inset: auto;
    transform: none;
    margin: 7px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tt-pop {
    transition: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   PRODUCT COMPONENTS
   ═══════════════════════════════════════════════════════════════════ */

/* ── nav_shell ────────────────────────────────────────────────────── */

/* The track list is the load-bearing line, not decoration (night QA m7). Below
   900px this is two rows — the strip, then the page — and WITHOUT an explicit
   list both are implicit `auto` tracks. `align-content` resolves to `stretch`,
   which grows every auto track EQUALLY to satisfy `min-height: 100dvh`, so on a
   page shorter than the viewport the STRIP's row took half the leftover height
   and `align-items: start` parked the 60px strip at the top of it. The result
   was a blank field between the nav and the content: measured in the browser at
   500px on 7 of 22 pages, worst /me at 211px of nothing.

   Naming the rows fixes it at the grid rather than at the items: the strip
   takes what it needs and the PAGE absorbs the rest, which is also why this is
   `1fr` and not `align-content: start` — the main column keeps reaching the
   bottom of the viewport, so anything that paints against it still does. */
.shell {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr;
  min-height: 100dvh;
  align-items: start;
}

/* Below 900px the rail becomes a strip. Only the NAV scrolls — putting
   `overflow-x` on the whole sidebar made the account chip (which is
   `margin-left: auto`) land on top of the last nav link, and dragged the
   brand out of view with it. Caught in the browser at 390px. */
.side {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--panel-2), var(--color-bg));
  min-width: 0;
}

/* ── the sub-900px strip's width budget (task 7, operator-ruled) ──────────
   Below 900px the rail is a horizontal strip sharing one line with the mode
   toggle, the nav and the account chip. Measured before this block, at a
   390px viewport: brand 115 + toggle 102 + chip 63 + gaps took 300 of the
   390, and `.side-nav` — which holds up to twenty destinations — was left
   with 53px against a nav entry that is 85–103px wide. PR #1 made that strip
   scroll to the active entry, which is everything scrolling can do; it cannot
   make a 53px window show a 103px entry.

   So the two elements that are the same on every page give their width back,
   and only below 900px. The ruling is the operator's: the brand collapses to
   its mark and the chip to its avatar. Both are RECOGNISABLE at that size —
   an anchor and an initialled disc — which is why these two and not the mode
   toggle, whose two words are the control.

   Every rule in this block is inside `@media (max-width: 899.98px)` further
   down; nothing here changes the desktop rail, and the acceptance test proves
   that by comparing rendered geometry at 900px and 1440px rather than
   trusting it. */
.side-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  margin-right: var(--sp-2-5);
}

.side-brand svg {
  color: var(--color-accent);
}

.side-brand-name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 14.5px;
  letter-spacing: 0.02em;
  line-height: 1.05;
}

.side-brand-sub {
  display: none;
  font-size: var(--fs-micro-2);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim-2);
}

.side-cap {
  display: none;
  padding: var(--sp-3) var(--sp-2-5) 5px;
  font-size: var(--fs-micro);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--dim-2);
}

.side-nav {
  display: flex;
  flex-direction: row;
  gap: var(--sp-1);
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: thin;
}

.side-nav::-webkit-scrollbar {
  height: 4px;
}

.side-nav::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--radius-md);
}

.side-nav::-webkit-scrollbar-track {
  background: transparent;
}

.side-nav::-webkit-scrollbar-button {
  display: none;
  width: 0;
  height: 0;
}

.navbtn {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  flex: none;
  padding: var(--sp-2) var(--sp-2-5);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--dim);
  font: 500 var(--fs-md) var(--font-body);
  cursor: pointer;
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--t-fast) ease;
}

/* The icon takes the link's colour, so it lights with the label on hover and
   on the current page rather than sitting at a fixed weight beside it. */
.navbtn svg {
  flex: none;
  color: currentColor;
}

.navbtn:hover {
  background: var(--hover-tint);
  color: var(--color-text);
}

/* The active mark is an inset accent bar plus a fading wash — a line and
   a glow, which is how this system is allowed to spend the accent. */
/* In the horizontal strip the mark reads as an underline; in the vertical
   rail it becomes the reference's inset left bar (see the 900px block). A
   left bar on a horizontal tab points at nothing. */
.navbtn[aria-current="page"] {
  background: var(--accent-wash-3);
  color: var(--color-text);
  box-shadow: inset 0 -2px 0 var(--color-accent);
}

/* The SECTION mark (R7b). An off-rail page — Manage leagues is the only one —
   lights the entry it sits under, and it must not be mistaken for being ON
   that entry's page: the bar is the same accent and the wash is not there, so
   the eye reads "this branch of the product" rather than "this page". ARIA
   makes the same distinction with `true` vs `page`, which is why the selector
   is the attribute VALUE and not a class. */
.navbtn[aria-current="true"] {
  color: var(--color-text);
  box-shadow: inset 0 -2px 0 var(--accent-ring);
}

/* The widget slot. The reference dresses the bottom of its rail with a real
   panel rather than leaving the space blank, and the blank space is exactly
   what made this sidebar read as unfinished. What goes in it is the surface's
   own business (the library puts its upload budget here); the shell only owns
   the slot. Hidden in the horizontal strip, where there is no space to give
   it — the reference hides its own for the same reason. */
.side-int {
  display: none;
}

.side-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  margin-left: auto;
  padding: var(--sp-2);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Settings affordance on the account chip — the reference's own. Drawn at the
   nav icons' stroke weight so the sidebar has ONE icon voice. */
.side-cog {
  margin-left: auto;
  flex: none;
  color: var(--dim-2);
}

.side-foot:hover .side-cog {
  color: var(--color-text);
}

.side-avatar {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 50%;
  background: var(--panel-3);
  box-shadow: inset 0 0 0 1px var(--line-2);
  display: grid;
  place-items: center;
  font: 600 var(--fs-label) var(--mono);
  color: var(--color-accent-300);
}

.side-who {
  display: none;
  line-height: 1.15;
  min-width: 0;
}

.side-who-name {
  display: block;
  font-size: var(--fs-sm);
}

.side-who-plan {
  display: block;
  font-size: var(--fs-label);
  color: var(--dim);
}

.main {
  padding: var(--sp-4) var(--sp-3-5) 60px;
  max-width: var(--main-max);
  min-width: 0;
}

/* The compaction itself. `max-width: 899.98px` rather than `899px`: the
   desktop block below opens at exactly 900, and a 1px gap between the two
   would leave a viewport with neither treatment. */
@media (max-width: 899.98px) {
  /* VISUALLY hidden, not removed. The mark beside these words is
     `aria-hidden="true"`, so `display: none` would leave a screen reader on a
     phone with no product name at all — the strip would announce a link to
     nowhere. These are `.visually-hidden`'s own declarations, applied by media
     query rather than by a class the template would have to swap. */
  .side-brand-words {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* With no words to sit beside, the mark's trailing margin is spacing around
     nothing. The strip's own `gap` still separates it from the toggle. */
  .side-brand {
    gap: 0;
    margin-right: 0;
  }

  /* The cog is `aria-hidden` decoration on a control the whole chip already
     is, so this one really can go. The avatar stays: it is the chip. */
  .side-cog {
    display: none;
  }

  /* The chip loses the padding it was carrying for a two-part row. */
  .side-foot {
    gap: 0;
    padding: var(--sp-1);
  }

  /* Two smaller levers, and they are needed because the brand and the chip
     alone land FIVE PIXELS short of the goal. Measured at 390px after those
     two: the strip is 186px and its two widest leading entries (Overview 105 +
     gap + Matches 98) need 207. Both of these are space AROUND things rather
     than the things themselves, which is the same trade the brand and chip
     made:

       - the entries' own horizontal padding, 10px -> 6px, takes 8px off each;
       - the strip's outer padding, 12px -> 8px, hands 8px to the nav.

     Together: entries 97 + 4 + 90 = 191 against a 194px strip. It fits, and it
     fits by 3px — thin enough to say out loud rather than leave for somebody to
     rediscover. The 25px pointer target is unaffected: `.navbtn` is a
     full-height row here and both reductions are horizontal. */
  .navbtn {
    padding-left: var(--sp-1-5);
    padding-right: var(--sp-1-5);
  }

  .side {
    padding-left: var(--sp-2);
    padding-right: var(--sp-2);
  }

  /* The WORKING toggle's compaction (operator-ruled after solo-for-all made
     it real everywhere and its inert-hide stopped matching): the padding
     tightens — a control at roughly half the width. The " hub" tail this rule
     used to hide is gone from the markup entirely (R3 renamed the control to
     "Team", because a page called Team hub exists elsewhere and this button
     does not go to it), so there is nothing left to hide at any width. */
  .side-mode .modebtn {
    padding-left: var(--sp-1-5);
    padding-right: var(--sp-1-5);
  }
}

/* `max-width: 899.98px` rather than `899px`: the desktop block below opens at
   exactly 900, and a 1px gap between the two would leave a viewport with
   neither treatment. (`cloud/mobile-nav-compact` opens a block with this same
   query — two blocks with one query is valid CSS and both apply, so a merge
   that keeps both is correct.) */
@media (max-width: 899.98px) {
  /* THE INERT TOGGLE GOES (task 8, operator-ruled follow-up to task 7's flag).
     102px — 26% of a 390px viewport — was going to a control that, in every
     real session today, is two spans: the current mode, and the other one
     disabled with a sentence saying switching arrives with account controls. A
     disabled control is worth its space on a desk and is not worth a quarter of
     a phone.

     The rule is BEHAVIOURAL, not environmental. `data-switchable` is set from
     the same `modes` the template's own branch turns on, so this hides exactly
     the branch that renders spans — never the dev override's working links,
     which stay at every width because the override is the only way to reach
     both products by hand during this build. And it needs no undoing: when
     switching ships, `modes` is truthy, the attribute flips to "1", and the
     selector stops matching. */
  .side-mode[data-switchable="0"] {
    display: none;
  }

  /* ...and the workspace keeps a NAME. This is the half the ruling's rationale
     assumed was already there: it cites the `.side-cap` line beneath the
     toggle, but `.side-cap` is itself `display: none` below 900px — so hiding
     the toggle would have left the mode stated nowhere a screen reader can
     reach it. The toggle's own "Team hub"/"Solo" text was the only statement.

     Visually hidden rather than shown costs exactly zero pixels (it was not
     drawn here before either) and puts the sentence back in the accessibility
     tree. The nav's SECTION captions share the `.side-cap` class and are
     deliberately NOT included: a heading over a horizontal scroller is in the
     way, which is why they were hidden in the first place. */
  .side-cap-workspace {
    display: block;
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

@media (min-width: 900px) {
  /* One row, two columns — so the row list goes back to a single track. It is
     `auto` rather than `1fr` because `align-content: stretch` already grows a
     lone auto track to the 100dvh minimum, which is the behaviour this layout
     has always had; naming it keeps the two-row list above from leaving a
     phantom second track in a layout that has no second row. */
  .shell {
    grid-template-columns: var(--side-width) 1fr;
    grid-template-rows: auto;
  }

  .side {
    position: sticky;
    top: 0;
    height: 100dvh;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-05);
    overflow-y: auto;
    padding: var(--sp-4-5) var(--sp-3);
    border-bottom: 0;
    border-right: 1px solid var(--line);
  }

  .side-brand {
    padding: 0 var(--sp-2) var(--sp-4);
    margin: 0 0 var(--sp-2-5);
    border-bottom: 1px solid var(--line);
  }

  .side-brand-sub,
  .side-cap,
  .side-who {
    display: block;
  }

  .side-nav {
    flex-direction: column;
    gap: var(--sp-05);
    flex: 0 1 auto;
    overflow-x: visible;
  }

  .navbtn {
    width: 100%;
  }

  .navbtn[aria-current="page"] {
    background: linear-gradient(90deg, var(--accent-wash-4), transparent);
    box-shadow: inset 2px 0 0 var(--color-accent);
  }

  /* Same split as the strip above: the left bar turns, the wash does not. */
  .navbtn[aria-current="true"] {
    background: transparent;
    box-shadow: inset 2px 0 0 var(--accent-ring);
  }

  .side-int {
    display: block;
    margin: auto 0 0;
    padding: var(--sp-2-5);
    border-radius: var(--radius-md);
    background: var(--panel-2);
    box-shadow: inset 0 0 0 1px var(--line);
  }

  /* When the slot is filled it, not the account chip, takes the free space —
     otherwise both would claim `margin-top: auto` and the panel would be
     pinned to the nav with the gap below it. */
  .side-int ~ .side-foot {
    margin-top: var(--sp-2-5);
  }

  .side-foot {
    margin: auto 0 0;
    padding: var(--sp-2-5);
    border-top: 1px solid var(--line);
    border-radius: 0;
    width: 100%;
  }

  .main {
    padding: var(--sp-6) var(--sp-8) 80px;
  }
}

/* ── page header ──────────────────────────────────────────────────── */

.page-head {
  margin-bottom: var(--sp-5);
}

/* The reference's kicker. Nocturne's `.card-kicker` in a page role. */
.page-kicker {
  font-size: var(--fs-label-2);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 7px;
}

.page-title {
  font-size: var(--fs-h1);
  margin: 0;
  letter-spacing: -0.025em;
}

.page-sub {
  font-size: var(--fs-sm-2);
  color: var(--dim);
  margin-top: 5px;
  text-wrap: pretty;
}

/* Title plus its one action. The action is a link, so it wraps under the
   title rather than shrinking it when the two do not fit. */
.page-head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
}

.sg-body,
.page-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  min-width: 0;
}

.sg-layout,
.page-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

/* The rail is sticky, so a layout that starts under a filter bar needs the
   gap here rather than a margin on the bar (which the rail would ignore). */
.page-layout-top {
  margin-top: var(--sp-5);
}

@media (min-width: 1100px) {
  .sg-layout,
  .page-layout {
    grid-template-columns: minmax(0, 1fr) var(--rail-width);
  }

  /* W2-γ. A page that puts NOTHING in the second track still paid for it: the
     grid reserved `--rail-width` plus the gap, so the header (outside the
     layout, full measure) ended 198px to the right of the filter bar, the card
     grid and every module under it. Three right edges on one page reads as a
     layout that broke rather than as a column that is empty. The modifier is
     opt-in per page and not `:has(> :only-child)` on purpose: which pages carry
     a rail is a product decision, and a structural selector would have moved
     any page that happens to render its rail away — a filtered state, a shared
     view — without anybody choosing that. */
  .page-layout-solo {
    grid-template-columns: minmax(0, 1fr);
  }
}

.sg-section,
.page-section {
  scroll-margin-top: var(--sp-5);
  min-width: 0;
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1-5) var(--sp-2-5);
  margin-bottom: var(--sp-2-5);
}

.section-title {
  font-size: var(--fs-body);
  margin: 0;
}

.section-note {
  font-size: var(--fs-xs-2);
  color: var(--dim);
}

/* Still used where a paragraph is genuinely the right shape: the definition
   revealed inside a disclosure, where the reader asked for the words. It is
   NOT the shape for a family header — see `.tt--section`. */
.section-lede {
  font-size: var(--fs-sm);
  color: var(--dim);
  margin: 0 0 var(--sp-3);
  max-width: 68ch;
}

/* ── stat_rail ────────────────────────────────────────────────────── */

.rail {
  display: none;
}

@media (min-width: 1100px) {
  .rail {
    position: sticky;
    top: var(--sp-5);
    display: flex;
    flex-direction: column;
    gap: var(--sp-05);
    padding-left: var(--sp-05);
    min-width: 0;
  }
}

.rail-cap {
  font-size: var(--fs-micro-2);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim-2);
  padding: 0 0 var(--sp-2) var(--sp-2-5);
}

.railbtn {
  text-align: left;
  padding: var(--sp-1-5) var(--sp-2-5);
  border: 0;
  border-left: 2px solid var(--line);
  background: transparent;
  color: var(--dim);
  font: 500 var(--fs-xs-2) / 1.3 var(--font-body);
  cursor: pointer;
  text-decoration: none;
  transition: color var(--t-fast) ease;
}

.railbtn:hover {
  color: var(--color-text);
}

.railbtn[aria-current="true"] {
  color: var(--color-accent);
  border-left-color: var(--color-accent);
  background: linear-gradient(90deg, var(--accent-wash-2), transparent);
}

/* ── filter_bar ───────────────────────────────────────────────────── */

.filters {
  padding: var(--sp-3) var(--sp-3-5);
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

.filters-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2-5) var(--sp-3-5);
  align-items: flex-end;
  min-width: 0;
}

.filter-group {
  flex: 0 1 auto;
  max-width: 100%;
  min-width: 0;
}

/* The control's NAME, in the interface's own voice. It was uppercase micro-
   type with 0.1em tracking, which is the reference's treatment for a STAT
   LABEL inside a card — borrowed onto a form control it read as terminal
   chrome (the operator's word: wireframe). The reference labels its own
   filter groups in plain sentence-case body type; so does this. */
.filter-group-label {
  font-size: var(--fs-sm);
  color: var(--dim);
  margin-bottom: 5px;
}

/* A SEGMENTED CONTROL, not a row of boxes. The options sit in one recessed
   track (the reference's own `.side-mode` treatment) so the group reads as a
   single control with one thing chosen, rather than as N independent
   rectangles that happen to be adjacent. */
.filter-opts {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 3px;
  max-width: 100%;
  border-radius: var(--radius-md);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* Mono here is the reference's own call and it is the right one: every value
   in this control is a CORPUS TOKEN (a map key, a buy tier, a side), not UI
   copy. `text-decoration: none` is the fix that matters — these are anchors
   (every filter state is a URL) and without it the whole bar rendered as a
   list of underlined links. */
.chipbtn {
  padding: 5px 9px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--dim);
  font: 500 var(--fs-xs) / 1.25 var(--mono);
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition:
    color var(--t-fast) ease,
    background-color var(--t-fast) ease;
}

.chipbtn:hover {
  background: var(--hover-tint-2);
  color: var(--color-text);
}

/* The selected segment is a FILLED tint with a ring and full-strength text —
   the reference's active-mode treatment. Accent-coloured text alone (what
   this was) reads as "a link you have visited", not as "this is on". */
.chipbtn[aria-pressed="true"] {
  color: var(--color-text);
  box-shadow: inset 0 0 0 1px var(--accent-ring);
  background: var(--accent-wash-3);
}

.chipbtn-t[aria-pressed="true"] {
  color: var(--t);
  box-shadow: inset 0 0 0 1px var(--t-ring);
  background: var(--t-wash);
}

.chipbtn-ct[aria-pressed="true"] {
  color: var(--ct);
  box-shadow: inset 0 0 0 1px var(--ct);
  background: var(--ct-wash);
}

.filters-active {
  border-top: 1px solid var(--line);
  padding-top: var(--sp-2-5);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1-5);
  min-height: 32px;
}

.filters-sample {
  font: 500 var(--fs-xs-2) var(--mono);
  color: var(--color-accent-300);
  margin-right: var(--sp-1-5);
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: var(--sp-1) var(--sp-1-5) var(--sp-1) var(--sp-2-5);
  border-radius: calc(var(--radius-md) * 0.75);
  background: var(--color-accent-900);
  box-shadow: inset 0 0 0 1px var(--accent-ring);
  font: 500 var(--fs-xs-2) var(--mono);
  color: var(--color-accent-300);
}

.filter-chip-x {
  width: 15px;
  height: 15px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 3px;
  background: var(--accent-chip-btn);
  color: var(--color-accent-200);
  cursor: pointer;
  font: 600 var(--fs-xs) var(--mono);
  line-height: 1;
  text-decoration: none;
}

.filters-none {
  font-size: var(--fs-xs-2);
  color: var(--dim);
}

/* R3b — the page-level refusal, under the control it is about.
   A sentence, in the sentence face, at the width of the bar: it is read once
   and then the cards below it carry statuses. Set apart with a rule rather
   than a box, because a box reads as an error and nothing here failed. */
.filters-refusal {
  margin: var(--sp-2) 0 0;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--dim);
  max-width: 68ch;
}

/* A sentence, so it is set in the sentence face. Mono was this warning's
   costume, not its content: nothing in it is a figure to align. */
.filters-warn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1-5);
  padding: 3px var(--sp-2);
  border-radius: calc(var(--radius-md) * 0.75);
  background: var(--t-wash);
  box-shadow: inset 0 0 0 1px var(--t-ring);
  font: 500 var(--fs-xs) var(--font-body);
  color: var(--t);
}

/* ── kpi_card ─────────────────────────────────────────────────────── */

/* `auto-FILL`, never `auto-fit`, and the whole density of the page turns on
   the difference. `auto-fit` COLLAPSES the tracks nothing landed in, so a
   family holding two cards stretched both to half the column — two 477px
   cards, each holding a 29px number and 40 characters of sample, with the
   rest of the card empty. `auto-fill` keeps the tracks: two cards occupy two
   of four, the row has room left in it, and every card on the page is the
   same size no matter how many its family has. Verified against the
   reference, whose own short rows leave exactly that gap.
   The 208px floor (was 224) buys a fourth column inside this column's real
   width, which is what makes the grid read 4-up like the reference's. */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(208px, 100%), 1fr));
  gap: var(--sp-3);
  min-width: 0;
}

/* W2-γ — the ragged right edge, within the ruling above rather than against it.
   `auto-fit` was the obvious fix and it is the wrong one: it stretches a family
   of two to half the column and gives one page two card sizes, which is exactly
   what the wide-screen note below `.map-grid` rules out. So the lever is the
   FLOOR, which changes how many tracks a measure resolves to and leaves every
   card on the page one size.

   300px is measured, not picked: Executes' body is 1141px once its empty rail
   track is collapsed (`.page-layout-solo`), and floor((1141 + 16) / (300 + 16))
   is 3 — the size of its largest family — so that family's row ends exactly
   where the filter bar and every module on the page end. Doubled selector so it
   outranks the wide-screen `.kpi-grid` floor further down this sheet, which is
   about a column growing past 1400 and has nothing to say about this. */
.kpi-grid.kpi-grid-wide {
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
}

.kpi {
  padding: var(--sp-3-5) var(--sp-4);
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1-5);
  min-width: 0;
}

.kpi-head {
  display: flex;
  /* `baseline` since R7b: the label wraps to two lines now, and `center` would
     float the definition `i` beside it to the middle of a two-line block
     instead of sitting on the first line where the words start. */
  align-items: baseline;
  gap: var(--sp-1-5);
  min-width: 0;
}

/* IT WRAPS (R7b). The label used to ellipsise, and the 2026-08-13 re-audit
   caught two cards losing their identity to it at 1440 — "PRE-PLANT FLA…" and
   "ENTRY CONVERSI…". Both strings are inside the ~19-character budget the
   registry's own comments were written against (web/statview.py), so nothing
   grew: the BOX narrowed, which is what a fluid card grid does and will keep
   doing at the next width somebody adds.
   A truncated label is a different card — the reader cannot tell "Pre-plant
   flashes" from "Pre-plant flash rate", and one is a count and the other a
   rate. Two lines cost a few pixels of card height; a clipped one costs the
   number's meaning. `.duo` made exactly this fix for exactly this reason and
   kept it local; this is that decision applied where it belongs, and the `.duo`
   override below now only carries the wrap its edge chip needs. */
.kpi-label {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  min-width: 0;
  overflow: visible;
  white-space: normal;
}

.kpi-trend {
  margin-left: auto;
  font: 600 var(--fs-xs) var(--mono);
  flex: none;
}

.kpi-trend-up {
  color: var(--win);
}
.kpi-trend-down {
  color: var(--loss);
}
.kpi-trend-flat {
  color: var(--dim);
}

.kpi-value {
  font: 500 var(--fs-kpi) / 1.05 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.025em;
  overflow-wrap: anywhere;
}

.kpi-unit {
  font-size: var(--fs-body);
  color: var(--dim);
}

/* The no-sample glyph. It is deliberately NOT dimmed to invisibility:
   "we have no rounds that met this definition" is an answer, and an
   answer the operator has to be able to see. */
.kpi-value-empty {
  color: var(--dim);
}

.kpi-sample {
  font-family: var(--mono);
  font-size: var(--fs-xs-2);
  color: var(--dim);
  overflow-wrap: anywhere;
}

.kpi-bar {
  height: 3px;
  border-radius: 2px;
  background: var(--track);
  overflow: hidden;
  /* R4: the containing block for the corpus-median tick. `overflow: hidden`
     stays — it is what keeps a 100% fill inside the radius — and the tick is
     inset by its own width at the far end so the clip never eats it. */
  position: relative;
}

/* `--bar` is the only thing a template sets inline, and it is a DATUM (the
   metric's own percentage), not styling: the rule itself lives here. */
.kpi-bar-fill {
  display: block;
  height: 100%;
  width: var(--bar, 0%);
  border-radius: 2px;
  background: var(--color-accent);
}

/* ── the comparison (R4) ──────────────────────────────────────────────
   The corpus median, as a tick on the card's own track and a figure under it.
   Same treatment as the Players page's ledger rows, on the component the four
   deep stat pages draw — one comparison idiom, not a second one. */

.kpi-bar-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  /* `--tick` is the median's own value on the same 0-100 scale the fill is
     drawn to. `max()` keeps the mark visible at 0 and the negative margin
     keeps it visible at 100, so neither end of the scale silently loses it. */
  left: max(0px, var(--tick, 0%));
  width: 2px;
  margin-left: -1px;
  background: var(--stat-neutral);
  opacity: 0.8;
}

.kpi-peer {
  display: flex;
  align-items: baseline;
  gap: var(--sp-1-5);
  min-width: 0;
}

.kpi-peer-value {
  font: 500 var(--fs-sm-2) / 1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--dim);
  flex: none;
}

.kpi-peer-note,
.kpi-peer-absent {
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The edge chip, in the Players page's own three tones (`.ts-kpi-edge`).
   --stat-* rather than --win/--loss on purpose, and tokens.css says why: those
   two mean "we won this match", and this chip means "we are ahead of the
   comparison". Two different claims must not share a colour. */
.kpi-edge {
  margin-left: auto;
  padding: 2px var(--sp-2);
  border-radius: calc(var(--radius-md) * 0.7);
  font: 600 var(--fs-micro) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--stat-neutral);
  background: var(--hover-tint);
  box-shadow: inset 0 0 0 1px var(--line-2);
  flex: none;
}

.kpi-trend + .kpi-edge {
  margin-left: var(--sp-1-5);
}

.kpi-edge[data-tone="good"] {
  color: var(--stat-good);
  background: var(--stat-good-wash);
  box-shadow: inset 0 0 0 1px var(--stat-good-ring);
}

.kpi-edge[data-tone="bad"] {
  color: var(--stat-bad);
  background: var(--stat-bad-wash);
  box-shadow: inset 0 0 0 1px var(--stat-bad-ring);
}

.kpi[data-tone="good"] .kpi-value {
  color: var(--stat-good);
}

.kpi[data-tone="bad"] .kpi-value {
  color: var(--stat-bad);
}

/* ── the sample floor (R4) ────────────────────────────────────────────
   A rate under its grain's floor is DEMOTED, never hidden: the review found a
   "Best performer" crowned off one map and a 4-round pistol rate at full KPI
   size, and both were honest — the sample was printed right there. What was
   missing is that the card looked exactly like a card standing on 400 rounds.

   The demotion is size and ground, not contrast. Dropping the figure to
   `--dim-2` would have made a thin number harder to READ, which is the
   opposite of the goal: the number is real and the reader is being told how
   much of it to spend, not being stopped from seeing it. */

.kpi[data-thin] {
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
}

.kpi[data-thin] .kpi-value {
  font-size: calc(var(--fs-kpi) * 0.66);
}

.kpi[data-thin] .kpi-sample {
  color: var(--t);
}

/* RANK-OF-N, under the median it is a second reading of. Sample weight, not
   value weight: a rank is context for the two numbers above it, and a position
   drawn at headline size would compete with the figure it is about. It is the
   `.kpi-peer-note` treatment with room above, so the card reads
   number / comparison / position down one column. */
.kpi-rank {
  margin: var(--sp-1) 0 0;
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim-2);
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;
}

/* The one page-level sentence about the floor. Drawn once, and only on a
   render that actually contains a thin card. */
.thin-note {
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--dim);
  margin-top: var(--sp-2);
  text-wrap: pretty;
}

/* ── the demoted line (R4, review §6) ─────────────────────────────────
   A number kept for the record and taken out of the headline grid. It is a
   LINE and not a small card on purpose: a card at 80% size still reads as a
   card, and the point of the demotion is that this is not one of the page's
   answers. It sits inside its family, under the grid, so the reader can see
   what it belongs to. */

.demoted {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1-5);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-md);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
}

.demoted-figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1-5) var(--sp-3);
  min-width: 0;
}

.demoted-label {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

/* The demotion itself. The element carries `.kpi-value` too — a demoted stat
   is still a stat and the registry-wide walks read it as one — so the size is
   set here, after that rule, and this is the whole visual difference. */
.demoted .demoted-value {
  font: 500 var(--fs-lg) / 1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

.demoted-sample,
.demoted-peer {
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
}

/* The floor's verdict, in the caution hue the cards use for it. */
.demoted-sample[data-thin] {
  color: var(--t);
}

.demoted-why,
.demoted-complement {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--dim);
  text-wrap: pretty;
}

/* The complement is the sentence the demotion EXISTS for — the number the
   headline never showed — so it reads at full strength while the reason for
   the demotion stays quiet. */
.demoted-complement {
  color: var(--color-text);
}

/* ── elsewhere (R4, review §12) ───────────────────────────────────────
   Families that used to be re-printed here, as doors to the one page that
   draws them. Rows rather than cards: a card would be a fifth thing competing
   with the four modules this page actually owns. */

.elsewhere-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: var(--sp-2);
}

.elsewhere-row {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-3-5);
  border-radius: var(--radius-md);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
  color: inherit;
  text-decoration: none;
  min-width: 0;
  transition: background var(--t-fast) var(--ease);
}

.elsewhere-row:hover {
  background: var(--panel-3);
}

.elsewhere-name {
  font-size: var(--fs-base);
  font-weight: 500;
}

.elsewhere-note {
  font-size: var(--fs-xs);
  color: var(--dim);
  text-wrap: pretty;
}

.elsewhere-cta {
  font-size: var(--fs-xs);
  color: var(--color-accent-300);
  padding-top: var(--sp-1-5);
}

/* ── the solo synthesis card (R4, review §7) ──────────────────────────
   Two rows, the two ends of one ordering. Rows and not cards: a card would
   put the strength and the weakness in separate boxes, and the whole point of
   gathering them is that they are one reading. */

.synth-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: var(--sp-2);
}

.synth-row {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1-5);
  padding: var(--sp-3) var(--sp-3-5);
  border-radius: var(--radius-md);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
  color: inherit;
  text-decoration: none;
  min-width: 0;
  transition: background var(--t-fast) var(--ease);
}

.synth-row:hover {
  background: var(--panel-3);
}

.synth-tag {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim);
}

.synth-row[data-testid$="strongest"] .synth-tag {
  color: var(--stat-good);
}

.synth-row[data-testid$="weakest"] .synth-tag {
  color: var(--stat-bad);
}

.synth-line {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  min-width: 0;
}

.synth-label {
  font-size: var(--fs-lg);
  font-weight: 500;
  min-width: 0;
  overflow-wrap: anywhere;
}

.synth-value {
  margin-left: auto;
  font: 500 var(--fs-xl) / 1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  flex: none;
}

.synth-rank {
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
}

.synth-cta {
  font-size: var(--fs-xs);
  color: var(--color-accent-300);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}

.kpi-reason {
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--dim);
  text-wrap: pretty;
}

/* Absent: the engine cannot answer this question here. Signalled with the
   caution hue, because it is a fact about the QUESTION, not about the team. */
.kpi--absent {
  box-shadow: inset 0 0 0 1px var(--t-ring);
}

.kpi--absent .kpi-value {
  color: var(--t);
}

.kpi--zero .kpi-value {
  color: var(--dim);
}

/* Suppressed: the question does not exist at this grain (M2). Neutral, not
   amber — nothing is wrong here, the page is declining to answer a malformed
   question. The value slot drops to body size because what it holds is the
   grain's NAME, and a word set at 29px in the stat slot would be read as a
   stat. */
.kpi--suppressed {
  box-shadow: inset 0 0 0 1px var(--line-2);
}

.kpi-value-grain {
  font: 500 var(--fs-body) / 1.05 var(--font-body);
  letter-spacing: 0;
  color: var(--dim);
}

/* ── gate (the entitlement wrapper) ───────────────────────────────── */

.kpi--gated {
  box-shadow: inset 0 0 0 1px var(--accent-ring-soft);
  background: linear-gradient(115deg, var(--panel-2), var(--panel) 62%);
}

.gate-value {
  display: flex;
  align-items: center;
  gap: var(--sp-2-5);
  min-height: calc(var(--fs-kpi) * 1.05);
}

.gate-lock {
  flex: none;
  color: var(--color-accent-300);
}

/* A redaction, not a blurred number: there is no value in the DOM to
   un-blur, and nothing here can be mistaken for a reading. */
.gate-redact {
  flex: 1 1 auto;
  height: 12px;
  min-width: 56px;
  max-width: 108px;
  border-radius: 3px;
  background: repeating-linear-gradient(
    -45deg,
    var(--line-2) 0 6px,
    var(--track-2) 6px 12px
  );
}

.gate-line {
  font-size: var(--fs-xs-2);
  color: var(--color-accent-300);
  text-wrap: pretty;
}

/* A plan's NAME. Mono made it look like a config key. */
.gate-plan {
  font: 600 var(--fs-label) var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent);
}

/* ── two_team_card (head to head) ─────────────────────────────────── */

.h2h {
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  overflow: hidden;
  min-width: 0;
}

.h2h-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1-5) var(--sp-2-5);
  padding: var(--sp-2-5) var(--sp-3-5);
  background: linear-gradient(90deg, var(--accent-wash), transparent);
}

.h2h-title {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

/* An annotation about the panel, not a figure in it. */
.h2h-note {
  font: 400 var(--fs-xs-2) var(--font-body);
  color: var(--dim);
}

.h2h-body {
  padding: var(--sp-1-5) var(--sp-3-5) var(--sp-3);
}

.h2h-row {
  padding: var(--sp-2-5) 0;
  border-bottom: 1px solid var(--line);
}

.h2h-row:last-child {
  border-bottom: 0;
}

.h2h-row-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: 7px;
  min-width: 0;
}

.h2h-map {
  font: 500 var(--fs-sm-2) var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.h2h-read {
  margin-left: auto;
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  flex: none;
}

.h2h-line {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) 54px;
  gap: var(--sp-2-5);
  align-items: center;
}

.h2h-line + .h2h-line {
  margin-top: 4px;
}

/* "us" / "them" are words, not readings. */
.h2h-side {
  font: 600 var(--fs-micro-2) var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.h2h-track {
  position: relative;
  height: 14px;
  border-radius: 3px;
  background: var(--track-2);
  overflow: hidden;
}

.h2h-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--bar, 0%);
  border-radius: 3px 0 0 3px;
}

.h2h-fill-us {
  background: var(--win);
}

.h2h-fill-them {
  background: var(--loss-bar);
}

.h2h-val {
  font: 500 var(--fs-xs-2) var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.h2h-foot {
  margin-top: var(--sp-2);
  font-size: var(--fs-label-2);
  color: var(--dim);
  text-wrap: pretty;
}

/* ── disclosure ───────────────────────────────────────────────────── */

.disc {
  margin-top: var(--sp-2-5);
}

.disc > summary {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: var(--sp-1-5) var(--sp-2-5);
  border-radius: var(--radius-sm);
  font: 500 var(--fs-xs-2) var(--font-body);
  color: var(--color-accent-300);
  box-shadow: inset 0 0 0 1px var(--line);
  list-style: none;
  cursor: pointer;
}

.disc > summary::-webkit-details-marker {
  display: none;
}

.disc > summary:hover {
  background: var(--hover-tint);
}

.caret {
  display: grid;
  transition: transform 0.15s var(--ease);
}

.disc[open] .caret {
  transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
  .caret {
    transition: none;
  }
}

.disc-body {
  margin-top: var(--sp-2);
}

/* ── card_door ────────────────────────────────────────────────────── */

/* a3/P2 — a KPI card that opens the surface explaining it, WITHOUT becoming an
   anchor. Every one of these cards carries a definition tip, and a `button` or
   a `span[tabindex]` inside an `a` is either invalid markup or a control the
   reader cannot reach on its own. So the link is a real anchor on the card's
   label (or the crown's name) and `::after` stretches its hit area over the
   card. The card is the positioning context, which is what `.has-door` buys.

   The link keeps the label's own colour: a card whose heading turned accent
   would read as a chip rather than as a heading, and the affordance the design
   wants here is the CARD lighting up, not the word. */
.has-door { position: relative; }

.card-door {
  color: inherit;
  text-decoration: none;
}

.card-door::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.has-door:hover { background: var(--hover-tint); }

/* The definition tip sits ABOVE the stretched hit area, so hovering the `i`
   opens the tip rather than highlighting the card, and pressing it does not
   navigate. Every tooltip mechanism in the product is listed: one left out is
   one icon that silently stops working inside a card. */
.has-door .tt,
.has-door .tt-anchor,
.has-door .mp-tip,
.has-door .ts-kpi-tip,
.has-door .disc {
  position: relative;
  z-index: 1;
}

/* The focus ring belongs to the CARD, because the card is what the link
   covers — a 90px outline round the label with a lit card behind it reads as
   two things focused. */
.card-door:focus-visible { outline: none; }
.has-door:has(.card-door:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ── ledger_note ──────────────────────────────────────────────────── */

/* P1-2. Provenance prose inside a disclosure: the same weight Firepower's
   `.mp-note` takes, declared here because the two surfaces that draw this one
   (Overview, Maps) do not link map-pool.css and must not start to for one
   paragraph. `text-wrap: pretty` for the same reason every other note in this
   sheet has it — a two-word last line reads as a broken layout. */
.ledger-note {
  margin: 0;
  max-width: 62ch;
  font-size: var(--fs-xs-2);
  color: var(--dim-2);
  text-wrap: pretty;
}

.ledger-note > a {
  margin-left: var(--sp-1-5);
  white-space: nowrap;
}

/* ── mp-disclose ──────────────────────────────────────────────────── */

/* W2-γ moved these out of map-pool.css, byte for byte. They are the chrome of
   a `<details>` that TWO pages draw — a map's detail ledger and Economy's
   pistol reconciliation — and Economy matched three of map-pool's 166
   selectors, so it was parsing 38 KB of pool rules to style one disclosure.
   The `.mp-` prefix is kept: renaming a class would be a second change riding
   along on a link removal, and it is exactly the kind of edit that makes a
   "no visual diff" claim untestable. The prefix is a name, not an owner. */
.mp-note { margin: 0; font-size: var(--fs-xs-2); color: var(--dim-2); text-wrap: pretty; }

.mp-disclose { border-top: 1px solid var(--line); padding-top: var(--sp-2); }
.mp-disclose-sum {
  font-size: var(--fs-xs-2);
  color: var(--dim);
  cursor: pointer;
  list-style: none;
  text-wrap: pretty;
}
.mp-disclose-sum::-webkit-details-marker { display: none; }
.mp-disclose-sum::before { content: "+ "; color: var(--color-accent); font-family: var(--mono); }
.mp-disclose[open] .mp-disclose-sum::before { content: "\2212 "; }
.mp-disclose-sum:hover { color: var(--color-text); }
.mp-disclose > .mp-note,
.mp-disclose > .mp-seam { margin-top: var(--sp-2); }
.mp-disclose > .mp-seam { border-top: 0; padding-top: 0; }

/* ── table_scroll ─────────────────────────────────────────────────── */

/* The 390px guarantee: a wide table scrolls INSIDE this box. The page
   body never scrolls sideways, which is the difference between a data
   product and a broken one. */
.xscroll {
  overflow-x: auto;
  overflow-y: hidden;
  max-width: 100%;
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* The scroll is ORDER-PROOF, and this doubled selector is why. `.mod`
   (modules.css) sets `overflow: hidden` for its own rounded corners, and
   modules.css loads AFTER this sheet — so on a box that is both, equal
   specificity handed the win to whichever rule came last and a `class="xscroll
   mod"` table was clipped instead of scrolled. /me's per-map splits shipped
   that way: eleven columns cut off at 500px with no way to reach them.
   Repeating the class raises this rule to (0,0,2,0) so the scroll survives ANY
   single-class `overflow` a later sheet sets on the same element, whichever
   order the sheets load in. It does not touch a `.mod` that is not also an
   `.xscroll`. */
.xscroll.xscroll {
  overflow-x: auto;
  overflow-y: hidden;
}

.xscroll::-webkit-scrollbar {
  height: 8px;
}

.xscroll::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--radius-md);
}

.xscroll::-webkit-scrollbar-track {
  background: transparent;
}

.xscroll::-webkit-scrollbar-button {
  display: none;
  width: 0;
  height: 0;
}

/* `--min-w` is the table's own column minimum, set inline by the caller;
   the rule stays here. */
.xscroll-inner {
  min-width: var(--min-w, 0);
}

/* THE HINT IS FOR THE WIDTH THAT NEEDS IT (R6). A line telling a reader to
   scroll sideways, printed under a table that fits, is a false instruction —
   so it is hidden by default and appears only below 720px, which is under the
   narrowest `min_width` any caller passes (560) plus the shell's own padding.
   Every table this component wraps is the right shape at every width; what the
   review found was that being clipped was SILENT, and silence is the part that
   is fixable without narrowing a comparison nobody wanted narrowed. */
.xscroll-hint {
  display: none;
  margin-top: var(--sp-1-5);
  font-size: var(--fs-label-2);
  color: var(--dim);
}

/* ══ W2-γ APPEND-ONLY BLOCK: the scroll's own edge cue ═══════════════════
   Everything above this comment is untouched by this lane.

   The hint below the table says a table CAN be scrolled. What nothing said is
   whether there is anything left to scroll TO: the 390 scoreboard is 640px of
   table in a 347px box, and at rest the eleventh column ends flush against a
   hard edge that looks exactly like the end of the table. The walkthrough
   found it there; the same silence is on every caller of `table_scroll`.

   So the right edge softens while there is more to the right of it, and goes
   hard when the reader reaches the end — the standard cue, driven by the
   scroller's own position rather than by a script. `scroll(self inline)` is a
   scroll-progress timeline: no JavaScript, no listener, no state, and it works
   with JS off, which is this product's rule for everything the picker and the
   filters do.

   THE BASE VALUE IS 0 AND THE ANIMATION SUPPLIES THE FADE, which is the whole
   safety argument. A scroll timeline on a box with nothing to scroll is
   INACTIVE, and an inactive timeline removes the animation's effect — so a
   table that fits falls back to the base declaration and draws no fade at all,
   instead of wearing a permanent "there is more" on content that has none.
   `animation-range` puts the whole transition in the last stretch of travel, so
   the cue is on for the scroll and off at the end rather than easing out under
   the reader's thumb the entire way.

   Gated at 720px because that is the width the hint already uses and for the
   same measured reason: below it every table this component wraps is clipped.
   Above it, a fade would be decoration on a box that fits.

   `@supports` because this degrades to exactly today's page, which is a table
   with no edge cue — not to a broken one. */
@property --xscroll-fade {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@keyframes xscroll-edge {
  from { --xscroll-fade: 34px; }
  to { --xscroll-fade: 0px; }
}

@media (max-width: 720px) {
  @supports (animation-timeline: scroll(self inline)) {
    .xscroll {
      --xscroll-fade: 0px;
      /* `black` and not a token, and not a hex either: a mask stop is an ALPHA
         value that never reaches a pixel, so it is not a colour role and must
         not spend one — the same keyword `.gp-hero-art`'s mask uses in
         game-page.css, for the same reason. */
      -webkit-mask-image: linear-gradient(
        to right, black calc(100% - var(--xscroll-fade)), transparent 100%);
      mask-image: linear-gradient(
        to right, black calc(100% - var(--xscroll-fade)), transparent 100%);
      animation: xscroll-edge linear both;
      animation-timeline: scroll(self inline);
      animation-range: 82% 100%;
    }
  }
}

@media (max-width: 720px) {
  .xscroll-hint { display: block; }
}

/* ── side split (team stats) ──────────────────────────────────────── */

/* Two tracks on one scale, under the two cards whose numbers they draw. The
   eye reads the gap without arithmetic; the number stays alongside because a
   bar is not a value.

   The geometry is the reference's map-pool row, verbatim: side, then the
   READING at 17px, then a 16px track, then the sample. It was a 6px hairline
   with the number pushed to the far right — a rule with a tint on it, not a
   bar. The DOM order is unchanged (side, track, value, sample); the columns
   are assigned explicitly so the reading sits beside its label and the bar
   gets the whole middle. */
.split {
  margin-top: var(--sp-3);
  padding: var(--sp-2-5) var(--sp-3);
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}

.split-line {
  display: grid;
  grid-template-columns: 30px 62px minmax(80px, 1fr) minmax(0, 152px);
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-1-5) var(--sp-1);
  border-radius: var(--radius-sm);
}

/* Every cell names its ROW as well as its column. Naming only the column
   leaves the rest to auto-placement, which packs forward and never backward:
   the value (column 2, written third) could not fit behind the track (column
   3, written second) and dropped to a second row. */
.split-side {
  grid-area: 1 / 1;
  font: 600 var(--fs-label) / 1 var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.split-side-t {
  color: var(--t);
}

.split-side-ct {
  color: var(--ct);
}

.split-track {
  grid-area: 1 / 3;
  position: relative;
  height: 16px;
  border-radius: 3px;
  background: var(--track-2);
  overflow: hidden;
}

.split-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--bar, 0%);
  border-radius: 3px 0 0 3px;
}

.split-fill-t {
  background: var(--t);
}

.split-fill-ct {
  background: var(--ct);
}

.split-val {
  grid-area: 1 / 2;
  font: 500 var(--fs-lg) / 1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  text-align: left;
}

.split-sample {
  grid-area: 1 / 4;
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
}

/* The sample belongs to the row it describes, so below the reading's own
   width it drops under the track rather than squeezing the bar to nothing. */
@media (max-width: 700px) {
  .split-line {
    grid-template-columns: 30px 62px minmax(0, 1fr);
    gap: var(--sp-1) var(--sp-2);
  }

  .split-sample {
    grid-area: 2 / 2 / auto / -1;
  }
}

/* ── match library ────────────────────────────────────────────────── */

/* One column. The upload budget used to sit here as a second track, which
   cost the filter bar a fifth of its width on every viewport; it now lives in
   the shell's widget slot, where a plan affordance belongs and where it
   dresses the space the sidebar was leaving blank. */
.lib-head {
  min-width: 0;
}

.lib-head-main {
  min-width: 0;
}

/* `--dim`, not `--dim-2`: this is a sentence the operator has to READ, and
   the two steps are split by exactly that test (tokens.css). Measured at
   2.74:1 on `--dim-2` and 5.09:1 here. */
.lib-foot {
  margin-top: var(--sp-2-5);
  font-size: var(--fs-xs-2);
  color: var(--dim);
  max-width: 74ch;
  text-wrap: pretty;
}

/* The series grid. ONE template, shared by the header row, a single-map row
   and a multi-map summary — so the columns cannot drift apart between the
   three places a series is drawn. */
.series-list {
  display: flex;
  flex-direction: column;
}

.series-row {
  display: grid;
  grid-template-columns: 34px minmax(190px, 1fr) 104px minmax(150px, 1.1fr) 104px 84px;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-2-5) var(--sp-3-5);
  min-width: 0;
  background: linear-gradient(
      to right,
      transparent,
      var(--row-rule) var(--rule-fade),
      var(--row-rule) calc(100% - var(--rule-fade)),
      transparent
    )
    no-repeat bottom / 100% 1px;
}

.series-head-row {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  padding-block: var(--sp-2);
}

.series-details > summary {
  list-style: none;
  cursor: pointer;
}

.series-details > summary::-webkit-details-marker {
  display: none;
}

/* Hover is an accent EDGE, not a background wash. Two reasons, and the second
   is the load-bearing one: Nocturne spends the accent as a line rather than a
   flood, and the result badges sit on this row — tinting the ground under them
   would move `--loss` from 4.8:1 to 4.3:1 for as long as the pointer is there,
   which is a contrast regression nobody would ever screenshot. */
.series-details > summary:hover,
.series-row:not(.series-head-row):hover {
  box-shadow: inset 2px 0 0 var(--color-accent);
}

.series-details[open] > summary .caret {
  transform: rotate(90deg);
}

.series-mark {
  display: grid;
  place-items: center;
  color: var(--dim);
}

/* A single-map series has nothing to expand, so its first column carries a
   dot where the expandable rows carry a caret. Same column, same width: the
   grid never shifts between the two kinds of row. */
.series-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--line-2);
}

.series-teams {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1-5);
  min-width: 0;
}

.series-us {
  font-size: var(--fs-md-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.series-vs {
  font-size: var(--fs-xs-2);
  color: var(--dim);
}

/* The opponent is a NAME, at the same weight as ours — only the joining word
   is furniture. Dimming it made every row read "us, and then some grey text",
   which is not what a head-to-head list is about. */
.series-them {
  font-size: var(--fs-md-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.series-event {
  font: 400 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}

.series-result {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1-5);
  min-width: 0;
}

.series-mapscore {
  font: 500 var(--fs-sm-2) var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--dim);
}

.series-maps {
  display: flex;
  flex-wrap: wrap;
  gap: 2px var(--sp-1-5);
  min-width: 0;
}

/* Unboxed. The reference lists a series' maps as plain mono text separated by
   a middle dot; boxing each one put four filled rectangles in a row that is
   already carrying a badge, a score and a date, and the row lost its subject.
   The separator is drawn by the row, not typed into the markup, so a
   one-map series never renders a dangling dot. */
.mapchip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
  white-space: nowrap;
}

/* The separator TRAILS its chip rather than leading the next one. Leading it
   put the dot at the start of a wrapped line, which reads as a bullet list;
   trailing it leaves the dot at the end of the line it belongs to, which is
   what running text does.

   The condition is "another CHIP follows", not "I am not the last element":
   W#47 puts a demo affordance after the chip it belongs to, and `:not(
   :last-child)` would then draw a separator between a map and its own state —
   a dot joining two things that are one thing. */
.mapchip:has(~ .mapchip)::after {
  content: "·";
  margin-left: var(--sp-1-5);
  color: var(--dim-2);
}

.mapchip-score {
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

.mapchip-more {
  color: var(--color-accent-300);
}

/* ── per-match demo availability (W#47) ───────────────────────────────
   Quiet by construction: a loaded demo draws nothing at all (its affordance is
   the chip, which is already a link), so anything visible here IS a gap. The
   two visible states are deliberately different SHAPES rather than two colours
   — one is a door and one is a statement, and colour alone would say neither
   to a reader who cannot see it. */
.demo-cta,
.demo-chip {
  font: 400 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.04em;
  white-space: nowrap;
  align-self: center;
}

/* The upload loop's driver. Dotted underline, not a button: it sits inside a
   list of scores, and a button per row would out-shout the results. */
a.demo-cta {
  color: var(--color-accent-300);
  text-decoration: none;
  border-bottom: 1px dotted currentColor;
}

a.demo-cta:hover {
  color: var(--color-accent);
}

/* A queue status is not actionable, so it is not drawn as anything clickable. */
.demo-chip {
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--dim);
}

/* The legend under the table: the vocabulary in full, for the states on this
   page. Grid rather than a run of paragraphs so the words line up under each
   other and the whole thing reads as one table of terms. */
.demo-legend {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-3);
  margin: var(--sp-2) 0 0;
  max-width: 74ch;
  font-size: var(--fs-xs-2);
  color: var(--dim);
  text-wrap: pretty;
}

.demo-legend > dt {
  font-family: var(--mono);
  color: var(--color-text);
  white-space: nowrap;
}

.demo-legend > dd {
  margin: 0;
}

.series-date,
.series-diff {
  font: 400 var(--fs-xs-2) var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--dim);
}

.series-diff {
  text-align: right;
}

/* The per-map detail sits under the summary, indented to the series column so
   the eye reads it as belonging to the row above. */
.series-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-05);
  padding: var(--sp-1) var(--sp-3-5) var(--sp-3) 48px;
}

.mapline {
  display: grid;
  grid-template-columns: 128px 26px 78px minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-1-5) var(--sp-2-5);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  scroll-margin-top: var(--sp-8);
}

.mapline-map {
  font: 500 var(--fs-sm-2) var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mapline-score {
  font: 500 var(--fs-sm-2) var(--mono);
  font-variant-numeric: tabular-nums;
}

.mapline-meta {
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
}

/* The result badge. A 20px square, not a pill: it sits in a numeric column and
   a pill would read as a button. Outlined rather than filled, which is both
   the system's grammar and the only way `--loss` clears 4.5:1 at this size —
   any tint under it lightens the ground and drops the letter below AA. */
.badge {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 3px;
  font: 600 var(--fs-label-2) var(--mono);
}

.badge-win {
  box-shadow: inset 0 0 0 1px var(--accent-ring);
  color: var(--win);
}

.badge-loss {
  box-shadow: inset 0 0 0 1px var(--loss-ring);
  color: var(--loss);
}

.badge-none {
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--dim);
}

/* ── duplicate tag (DATA-1) ───────────────────────────────────────────
   The mark a row wears when the corpus holds the same game twice. It
   started as `.db-tag` in demo-bank.css and moved here the moment a
   second surface needed it: the match library lists flagged rows too,
   because a library answers "what do we hold" and a duplicate is one of
   the things we hold. A page sheet reaching into another page's classes
   is the drift the sheet contract forbids, so the class that two pages
   draw belongs to the component sheet both of them already load. */

.dup-tag {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--t);
  border: 1px solid var(--t-wash);
  border-radius: 3px;
  padding: 0 4px;
  margin-left: 4px;
}

/* The same verdict at DETAIL scale (A3, `components/duplicate_banner.html`).
   A row can wear the tag alone because the row beside it is the context; a
   page whose whole subject is the copy has to say what that means about
   everything underneath, so the banner carries the tag, the sentence and the
   way to the counted twin. Amber and not red: a duplicate is a caution about
   what the numbers below are, not a failure — the same reading `--t-wash`
   already carries on the tag it contains. Tinted, never flooded. */

.dupbar {
  margin: 0 0 var(--sp-3);
  padding: var(--sp-3) var(--sp-3-5);
  border-radius: var(--radius-md);
  background: var(--t-wash);
  box-shadow: inset 0 0 0 1px var(--t-ring);
}

.dupbar-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--fs-sm);
}

.dupbar-head .dup-tag { margin-left: 0; }

.dupbar-why,
.dupbar-to {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-sm);
  color: var(--dim);
}

.dupbar-to { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

.dupbar-of { color: var(--dim); }

/* ── empty state ──────────────────────────────────────────────────── */

.empty {
  margin-top: var(--sp-5);
  padding: var(--sp-6) var(--sp-4-5);
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
}

.empty-title {
  font-size: var(--fs-body);
  margin: 0 0 var(--sp-1-5);
}

.empty-body {
  font-size: var(--fs-sm);
  color: var(--dim);
  margin: 0;
  max-width: 62ch;
  text-wrap: pretty;
}

/* A FIRST-RUN empty state has somewhere to send you, which an ordinary one does
   not. The buttons wrap onto their own line under the sentence rather than
   sitting beside the title: this is the one call to action on the page a
   brand-new workspace lands on, and it should be read after the reason rather
   than instead of it. */
.empty-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-3);
}

.empty-note {
  font-size: var(--fs-sm);
  color: var(--dim-2);
}

/* ── dev identity picker ──────────────────────────────────────────────
   The solo empty state's dev-only half (templates/_solo.html). Deliberately
   quiet — it is scaffolding for a build, not a product control, and it must
   not read as the page's primary action. It never renders in production; see
   web/solo.py:dev_identity_choices. */

.devpick {
  margin-top: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px dashed var(--line);
}

.devpick-note {
  font-size: var(--fs-xs);
  color: var(--dim);
  margin: 0 0 var(--sp-2);
  max-width: 62ch;
}

.devpick-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1-5);
}

.devpick-maps {
  color: var(--dim);
  margin-left: var(--sp-1);
}

/* ── upload_budget ────────────────────────────────────────────────── */

.ubudget {
  padding: var(--sp-3) var(--sp-3-5);
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1-5);
  min-width: 0;
}

/* In the shell's widget slot the panel is already drawn, so the component
   drops its own. A card inside a card is the one container mistake that never
   has a good reason. */
.side-int .ubudget {
  padding: 0;
  background: transparent;
  box-shadow: none;
}

.ubudget-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.ubudget-label {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ubudget-head .tag {
  margin-left: auto;
  flex: none;
}

.ubudget-value {
  font: 500 var(--fs-xl) / 1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.ubudget-cap {
  font-size: var(--fs-xs-2);
  color: var(--dim);
  margin-left: var(--sp-1-5);
}

/* The inactive reading is a phrase, not a number, so it takes body type. A
   number here would be a claim nothing in this system can back. */
.ubudget-value-off {
  font: 500 var(--fs-sm-2) / 1.3 var(--font-body);
  letter-spacing: 0;
  color: var(--dim);
}

.ubudget-meter {
  height: 6px;
  border-radius: 3px;
  background: var(--track);
  overflow: hidden;
}

/* Empty, not zero: the track keeps the slot's shape so the header does not
   change height the day billing turns on. */
.ubudget-meter-off {
  background: repeating-linear-gradient(
    -45deg,
    var(--track) 0 5px,
    var(--track-2) 5px 10px
  );
}

.ubudget-fill {
  display: block;
  height: 100%;
  width: var(--bar, 0%);
  border-radius: 2px;
  background: var(--color-accent);
}

.ubudget-note {
  margin: 0;
  font-size: var(--fs-label-2);
  line-height: 1.45;
  color: var(--dim);
  text-wrap: pretty;
}

/* ── styleguide chrome (this page only) ───────────────────────────── */

.sg-state-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(208px, 100%), 1fr));
  gap: var(--sp-3);
}

.sg-swatches {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(128px, 100%), 1fr));
  gap: var(--sp-2);
}

.sg-swatch {
  border-radius: var(--radius-sm);
  padding: var(--sp-2);
  box-shadow: inset 0 0 0 1px var(--line);
  font: 400 var(--fs-label-2) var(--mono);
  color: var(--dim);
}

.sg-swatch-chip {
  height: 34px;
  border-radius: 3px;
  margin-bottom: var(--sp-1-5);
  box-shadow: inset 0 0 0 1px var(--line-2);
}

/* Swatch fills as classes, so the specimen never needs an inline color and
   the token-only rule holds on the templates too. */
.sw-accent { background: var(--color-accent); }
.sw-accent-300 { background: var(--color-accent-300); }
.sw-accent-700 { background: var(--color-accent-700); }
.sw-accent-900 { background: var(--color-accent-900); }
.sw-bg { background: var(--color-bg); }
.sw-panel { background: var(--panel); }
.sw-panel-2 { background: var(--panel-2); }
.sw-panel-3 { background: var(--panel-3); }
.sw-t { background: var(--t); }
.sw-ct { background: var(--ct); }
.sw-win { background: var(--win); }
.sw-loss { background: var(--loss); }
.sw-text { background: var(--color-text); }
.sw-dim { background: var(--mark); }

.sg-type-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--line);
}

.sg-type-row:last-child {
  border-bottom: 0;
}

.sg-type-name {
  flex: none;
  width: 92px;
  font: 400 var(--fs-xs) var(--font-body);
  color: var(--dim-2);
}

.sg-type-display {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--fs-display);
  letter-spacing: -0.03em;
}

.sg-type-h1 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: var(--fs-h1);
  letter-spacing: -0.025em;
}

.sg-type-num {
  font: 500 var(--fs-kpi) var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.025em;
}

.sg-type-body {
  font-size: var(--fs-body);
}

.sg-type-label {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.sg-panel {
  padding: var(--sp-3-5);
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
  min-width: 0;
}

.sg-meta {
  font: 400 var(--fs-xs) var(--font-body);
  color: var(--dim);
}

.sg-mt-3 {
  margin-top: var(--sp-3);
}

.sg-mt-4 {
  margin-top: var(--sp-4);
}

.sg-field {
  min-width: 0;
  flex: 1 1 180px;
  max-width: 260px;
}

.sg-caption {
  font: 500 var(--fs-label) var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim-2);
  margin-bottom: var(--sp-1-5);
}

.sg-specimen {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2-5);
}

/* ---- Error page (the fail-closed surface) ------------------------------- */
/* One card, centered, quiet. The refusal message is the engine's own words
   and reads as data (mono), not as an apology. */

.error-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-4);
  background: var(--color-bg);
}

.error-card {
  max-width: 34rem;
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--sp-5);
}

.error-code {
  font-family: var(--mono);
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  color: var(--dim);
  margin: 0 0 var(--sp-2);
}

.error-title {
  font-size: var(--fs-2xl);
  font-weight: 500;
  margin: 0 0 var(--sp-3);
}

.error-detail {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--dim);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--sp-3);
  margin: 0 0 var(--sp-4);
  overflow-wrap: anywhere;
}

/* B1: two actions, not one — the way back into the product and the door that is
   true for a reader with no session (web/errors.py). Wrapping rather than a row
   that overflows, because both labels are sentences ("Back to the demo bank",
   "Sign in with Steam") and the card is 34rem on a phone. */
.error-back {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

/* ── create-a-workspace form ──────────────────────────────────────────
   It lives on the error-card surface because it is the same kind of moment: one
   card, one decision, nothing else on the page. The kind is a radio GROUP with
   a sentence per option and no default selection — M7 drawn as an interface,
   where the browser itself refuses to submit an unanswered choice. */

.ws-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}

.ws-field {
  margin: 0;
}

.ws-kinds {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.ws-kinds > legend {
  padding: 0;
  margin-bottom: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--dim);
}

/* The whole option is the target, not just the 13px circle. */
.ws-kind {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-2-5);
  align-items: start;
  padding: var(--sp-2-5) var(--sp-3);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  cursor: pointer;
}

.ws-kind:hover {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
}

/* The selected option is marked by its EDGE, the same grammar the series rows
   use for hover — a filled ground under a paragraph of body text would drop the
   description's contrast for the one option a person has chosen. */
.ws-kind:has(input:checked) {
  border-color: var(--color-accent);
  box-shadow: inset 2px 0 0 var(--color-accent);
}

.ws-kind:has(input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.ws-kind > input {
  margin: 3px 0 0;
  accent-color: var(--color-accent);
}

.ws-kind-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.ws-kind-label {
  font-size: var(--fs-md);
  color: var(--color-text);
}

.ws-kind-desc {
  font-size: var(--fs-xs-2);
  color: var(--dim);
  text-wrap: pretty;
}

/* The name field's sentence. One field is asked and it means two things
   depending on the choice above it, so the hint sits under the input rather
   than in the placeholder — a placeholder disappears the moment somebody types,
   which is exactly when they are deciding what to write. */
.ws-hint {
  margin: var(--sp-1-5) 0 0;
  font-size: var(--fs-xs-2);
  color: var(--dim);
  text-wrap: pretty;
}

/* A form-level refusal that is one word away from succeeding — the name is
   taken, or empty. Marked by its edge in the loss tone, the same grammar the
   rest of the product uses for "this went the wrong way". */
.ws-error {
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-md);
  border: 1px solid color-mix(in srgb, var(--loss) 45%, transparent);
  box-shadow: inset 2px 0 0 var(--loss);
  background: color-mix(in srgb, var(--loss) 8%, transparent);
}

.ws-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.ws-foot {
  margin-bottom: 0;
}

/* ---- Round-2 filter chips (reference chipbtn semantics) ------------------ */

.chipbtn {
  padding: 5px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--dim);
  font: 500 var(--fs-sm) var(--font-body);
  cursor: pointer;
  transition: color 0.13s ease, background 0.13s ease;
}

.chipbtn:hover { background: var(--hover-tint); }

.chipbtn[data-active="1"] {
  color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
  background: var(--accent-wash);
}

.chipbtn[data-side="T"][data-active="1"] { color: var(--t); box-shadow: inset 0 0 0 1px var(--t); background: var(--t-wash); }
.chipbtn[data-side="CT"][data-active="1"] { color: var(--ct); box-shadow: inset 0 0 0 1px var(--ct); background: var(--ct-wash); }

.chipbtn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.chip-group-label {
  font-size: var(--fs-sm);
  color: var(--dim);
  margin-bottom: 5px;
}

.filters-coverage {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--dim);
}

.filters-coverage [data-recompute-hint] { color: var(--color-accent); }
.filters-coverage [data-updating] { color: var(--color-accent); }

/* Auto-apply (Round 3): under JS every selection applies itself, so the
   Recompute button would repeat what each click already did. It stays in the
   server HTML as the working no-JS form's submit. */
/* The recompute button is HIDDEN BY DEFAULT and put back by no-js.css, which
 * only a browser without JavaScript downloads (base.html, inside <noscript>).
 *
 * It used to be the other way round — visible in the markup, removed by
 * `filters.js` setting `data-js="1"` — and that cost a layout shift on every
 * single filtered page load: measured, `.filters-active` painted at 40.98px
 * and settled at 32px about 50 ms later, so the whole page below the filter
 * bar jumped up 9px just after it appeared. CLS 0.0027 with `SPAN
 * .filters-coverage` and the page grid named as the sources. Small number,
 * very visible thing — it is the only element in the product that moves after
 * first paint, and it moved on the exact click the operator called janky.
 *
 * The settled layout is unchanged. What changed is that the JS-on page is now
 * correct in its first frame instead of being corrected in its second.
 * `filters.js` still sets `data-js` — nothing reads it, and rewriting a file
 * whose behaviour is not in question was not worth the diff. */
[data-component="filter-bar"] .filters-recompute { display: none; }

.navbtn-disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ---- Overview page (Round 3) — the design's landing modules ------------- */

/* ONE column, and the two-column row is a child of it (`.ov-top`).
 *
 * It was the two-column grid itself, with `.ov-band`, `.ov-recent` and
 * `.rs-panel` spanning `1 / -1` through it — which put three full-width panels
 * UNDER the sticky right column and gave the operator's report: the Team card
 * riding down the page over the KPI band, the results table and the manage
 * panel, measured in Chromium at 479x441px of cover at scrollY 1500.
 *
 * A sticky item is bounded by its containing block and NOT by the grid cell it
 * was placed in: Chromium clamps a sticky grid item to the grid container's
 * box, so `.ov-side` was free to travel the whole 2472px of this element.
 * `grid-row: 1 / 2` does not change that — measured, it still travelled.
 *
 * The single track is `minmax(0, 1fr)` for the reason `.ov-result`'s note
 * below gives: a `1fr` track would take its floor from its subtree's
 * min-content and put the page 13px past a 390px viewport. `1 / -1` still
 * resolves to this one column, so the three full-width panels keep their
 * spans and their geometry unchanged. */
.ov-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}

/* The roster and the travelling column, and nothing else — the same shape as
   `.ts-grid` on Players, which is the product's sticky-rail pattern. Its
   height is the roster panel's, so the rail releases exactly where its
   neighbour ends and can no longer reach the full-width panels below. */
.ov-top {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--sp-4);
}

/* A DECLARED panel: drawn, and explicitly not carrying a number yet.
 *
 * R7b: this was `opacity: 0.75`, and opacity fades a subtree — the words as
 * well as the frame. `.wip-meta` is `--dim-2`, which R6 measured at 4.64-5.03:1
 * on every ground; through this fade it rendered at 3.34:1, and the same
 * multiplication took four other classes under AA. A panel that says "nothing
 * is measured here yet" is a panel whose TEXT is the entire content, so the
 * text is the last thing that may be quietened.
 *
 * The fade moves onto the decoration, and the decoration is the only place it
 * was ever wanted. `color-mix` reproduces the composite EXACTLY: 0.75 of
 * `--panel` over `--color-bg` is what the browser was already compositing, so
 * the frame is byte-identical to what shipped and only the type changed. Same
 * arithmetic on the hairline. */
.panel-disabled {
  background: color-mix(in srgb, var(--panel) 75%, var(--color-bg));
  border-color: color-mix(in srgb, var(--line) 75%, transparent);
}

/* R7c — the one frame the move above did not reach.
 *
 * `wip.html` renders `panel panel-disabled wip`, and `.wip` sets the BORDER
 * SHORTHAND (`1px dashed var(--line-2)`), which resets `border-color`. Equal
 * specificity, later sheet, so it won the colour and the dashed frame came out
 * of R7b at `--line-2`'s full 0.16 where the opacity it replaced had been
 * compositing it at 0.12. The whole point of that change was that the frame
 * stayed byte-identical, and on this one component it did not.
 *
 * Same arithmetic, one specificity step up, and on `--line-2` because that is
 * the line THIS element draws. `.wip-compact` is deliberately not included: it
 * is not a disabled panel and its edge was never faded. */
.panel-disabled.wip {
  border-color: color-mix(in srgb, var(--line-2) 75%, transparent);
}

.panel-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: var(--sp-3);
}

.panel-kicker {
  font-size: var(--fs-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim);
}

.panel-kicker.accent { color: var(--color-accent); }

.panel-meta {
  font-size: var(--fs-sm);
  color: var(--dim);
}

.absent-note {
  font-size: var(--fs-sm);
  color: var(--dim);
  margin: var(--sp-2) 0 0;
}

.meter {
  height: 4px;
  border-radius: 2px;
  background: var(--row-rule);
  overflow: hidden;
  margin-top: var(--sp-2);
}

.meter-fill {
  display: block;
  height: 100%;
  background: var(--color-accent);
  border-radius: 2px;
}

.ov-player {
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--row-rule);
}
.ov-player:last-child { border-bottom: 0; }

.ov-avatar {
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  background: var(--panel-3);
  color: var(--dim);
  font: 600 var(--fs-label) var(--font-body);
  display: grid;
  place-items: center;
}

.ov-player-main { flex: 1 1 auto; min-width: 0; }
.ov-player-name { font-weight: 500; font-size: var(--fs-base); }
.ov-player-sub { font-size: var(--fs-sm); color: var(--dim); }

.ov-player-nums { display: flex; gap: var(--sp-4); flex: none; }
.ov-num { display: flex; flex-direction: column; align-items: flex-end; }
.ov-num-cap {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim-2);
}

.num-lg { font-size: var(--fs-2xl); }
.num-xl { font-size: 30px; }
.num-lg small, .num-xl small { font-size: 0.55em; color: var(--dim); }

/* THE RIGHT COLUMN TRAVELS (R6). Three panels — the team, the last ten, the
   next fixture — end around 470px while the column beside them runs past
   1500, so the review found a thousand pixels of nothing to the right of the
   KPI band and the recent-results table.

   Filling it was the other option and it was rejected: everything this route
   already computes is already printed somewhere on this page, so a fourth
   panel here would be a number said twice — which is the defect the same
   review spent §12 removing from four other pages. Inventing a fact to fill a
   hole is worse again.

   So the space does a job instead. What is in this column — who we are, how
   the last ten went, who is next — is exactly the context a reader wants while
   they read down the roster beside it, and the product already parks its
   sidebars this way (`.ts-rail`, `.fp-rail`, `.mp-rail` are all sticky). Below
   the collapse breakpoint the grid is one column and sticky would pin a panel
   over the content under it, so it is released there.

   WHAT R6 GOT WRONG, and the operator caught: it read the space as running
   past the KPI band and the results table, and those two are FULL WIDTH — they
   run underneath this column, not beside it. So "travel" meant covering them.
   The declarations here are unchanged; what changed is the parent. `.ov-top`
   is now the only two-column box on the page and this rail's containing block,
   so the travel ends at the roster panel's bottom edge and the panels below
   are out of reach by construction rather than by a number somebody tuned. */
.ov-side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  position: sticky;
  top: var(--sp-4);
  align-self: start;
  /* A column that outgrows the viewport must still be readable to its end. */
  max-height: calc(100vh - var(--sp-8));
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ov-recline { display: flex; gap: var(--sp-5); margin-top: var(--sp-3); }

.ov-strip { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.ov-chip {
  width: 24px;
  height: 24px;
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  font: 600 var(--fs-label) var(--mono);
}
.ov-chip-w { color: var(--win); box-shadow: inset 0 0 0 1px var(--win); }
.ov-chip-l { color: var(--loss); box-shadow: inset 0 0 0 1px var(--loss); }
/* B4: the same two treatments keyed on the tone the SHARED form strip writes
   (components/form_strip.html). The `-w`/`-l` pair above stays, because the
   Recent results rows draw an `.ov-chip` that is not part of a strip and has
   no tone to be keyed on. */
.ov-chip[data-tone="win"] { color: var(--win); box-shadow: inset 0 0 0 1px var(--win); }
.ov-chip[data-tone="loss"] { color: var(--loss); box-shadow: inset 0 0 0 1px var(--loss); }
.ov-chip[data-tone="draw"] { color: var(--dim); box-shadow: inset 0 0 0 1px var(--line-2); }

/* ── form direction (B4) ──────────────────────────────────────────────
   The word that stops a recent-form strip being ambiguous. It was in an
   `aria-label` on three surfaces and nowhere at all on the fourth, which is
   the same as nowhere: the reader who misreads a strip is the one looking at
   it. Drawn as quiet micro-copy beside the strip or once in the head of a
   table whose every row carries one — never repeated per row. */
.form-dir {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  letter-spacing: 0;
  text-transform: none;
  color: var(--dim-2);
  white-space: nowrap;
}

.ov-band {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--sp-4);
}

.ov-recent { grid-column: 1 / -1; }
/* The row is four things on one line — chip, opponent, map·score, date — and
   `flex-wrap` is what lets it stop being one line (night QA task 6).

   Why it had to: a flex item's default `min-width: auto` is its MIN-CONTENT, so
   an unwrapping row's own minimum is the sum of its children's longest
   unbreakable words plus the gaps. That minimum propagated up through
   `.ov-side` (a flex column, also `min-width: auto`) into `.ov-grid`, whose
   `minmax(0, 1fr)` track cannot shrink a child below what the subtree demands —
   so at 390px the grid's box was the correct 362px while its TRACK resolved to
   389.234px and EVERY panel on the page sat 13px past the viewport. The Recent
   results row was never visibly wrong; the page around it was.

   Wrapping alone is not enough, which is why the two text cells also get
   `min-width: 0` and a break rule: a team or map name is one token
   (`ZZRMK_de_inferno`), and a single token longer than the column re-creates
   the same minimum on its own line. `overflow-wrap: anywhere` breaks it rather
   than pushing the document; the alternative — ellipsis — would hide the end of
   a map name, and which map it was is the point of the row.

   Desktop is untouched by construction: at any width where the row fits,
   `wrap` never triggers and `min-width: 0` changes nothing. Verified in
   Chromium — every `.ov-result` box byte-identical at 1440px, and the document
   390 instead of 403 at 390px. */
.ov-result {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2-5) 0;
  border-bottom: 1px solid var(--row-rule);
}
.ov-result:last-child { border-bottom: 0; }
.ov-result-name {
  font-weight: 500;
  min-width: 0;
  overflow-wrap: anywhere;
}
.ov-result-meta {
  color: var(--dim);
  font-size: var(--fs-sm);
  min-width: 0;
  overflow-wrap: anywhere;
}
/* `margin-left: auto` keeps the date at the right end of whatever line it
   lands on, wrapped or not — the row's shape survives the break. */
.ov-result-date { margin-left: auto; color: var(--dim-2); font-size: var(--fs-sm); }

@media (max-width: 900px) {
  .ov-top { grid-template-columns: 1fr; }

  /* One column: there is nothing beside this to stay level with, and a pinned
     panel would sit on top of the content it used to sit next to. */
  .ov-side {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}

/* ---- Mode toggle (the design's TEAM | SOLO seg) ------------------------- */

.side-mode {
  display: flex;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  margin-bottom: var(--sp-3);
}

.modebtn {
  flex: 1 1 0;
  text-align: center;
  padding: 5px 7px;
  border-radius: var(--radius-sm);
  font: 500 var(--fs-label) var(--font-body);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  text-decoration: none;
}

.modebtn[data-active="1"] {
  background: var(--accent-wash-3);
  color: var(--color-text);
  box-shadow: inset 0 0 0 1px var(--accent-wash-4);
}

.modebtn:hover { color: var(--color-text); }
.modebtn-disabled { opacity: 0.45; cursor: not-allowed; }

/* ---- Round 3 sweep 1: Maps, Map detail, Match detail, Demo bank ---------
   Additive and token-only. Sizes that are not in the token scale (the map
   card's 24px headline, the hero's 54px score) are the reference's own
   numbers, kept literally; every colour is a token, because a colour literal
   here forks the theme. */

.stack { display: flex; flex-direction: column; gap: var(--sp-6); min-width: 0; }

.backlink { display: inline-flex; margin-bottom: var(--sp-3-5); }

/* -- the map pool ------------------------------------------------------- */

.map-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(258px, 100%), 1fr));
  gap: var(--sp-3);
  min-width: 0;
}

.map-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-3-5) var(--sp-4);
}

.map-card-head { display: flex; align-items: baseline; gap: var(--sp-2); }

.map-name {
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--fs-base);
  color: var(--color-text);
  text-decoration: none;
}
.map-name:hover { color: var(--color-accent); }

.map-name-sm {
  font-family: var(--mono);
  font-size: var(--fs-label-2);
  color: var(--color-accent-300);
  text-decoration: none;
}

.map-wr {
  margin-left: auto;
  font-size: 24px;
  line-height: 1;
  letter-spacing: -0.02em;
}
.map-wr small { font-size: 0.55em; color: var(--dim); }

.map-sample { font-family: var(--mono); font-size: var(--fs-xs); color: var(--dim-2); }

.meter-marked { overflow: visible; position: relative; }
.meter-mark {
  position: absolute;
  top: -3px;
  bottom: -3px;
  left: 50%;
  width: 1.5px;
  background: var(--mark);
}
.meter-fill-t { background: var(--t); }
.meter-fill-ct { background: var(--ct); }

.map-sides {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1-5);
  padding-top: var(--sp-2-5);
  border-top: 1px solid var(--line);
}

.map-side {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) 52px;
  gap: var(--sp-2);
  align-items: center;
}
.map-side .meter { height: 3px; margin-top: 0; }
.map-side-cap {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.08em;
}
.map-side-t { color: var(--t); }
.map-side-ct { color: var(--ct); }
.map-side-val {
  text-align: right;
  font-size: var(--fs-xs);
  color: var(--dim);
}
.map-side-val small { font-size: 0.8em; }

.map-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-3);
  padding-top: var(--sp-2-5);
  border-top: 1px solid var(--line);
  font-size: var(--fs-label-2);
  color: var(--dim-2);
}
.map-foot-end { margin-left: auto; }
.map-foot small { font-size: 1em; }

/* -- the side-balance plot ---------------------------------------------- */

.scatter { display: block; min-width: 250px; overflow: visible; }
.sc-axis { stroke: var(--line-2); stroke-width: 1; }
.sc-even { stroke: var(--line-2); stroke-width: 1; stroke-dasharray: 4 4; }
.sc-dot { fill-opacity: 0.26; stroke-width: 1.5; }
.sc-dot-t { fill: var(--t); stroke: var(--t); }
.sc-dot-ct { fill: var(--ct); stroke: var(--ct); }
.sc-dot-balanced { fill: var(--color-accent); stroke: var(--color-accent); }
.sc-label { fill: var(--dim-2); font-family: var(--mono); font-size: 8px; }
.sc-axis-cap { fill: var(--dim-2); font-family: var(--mono); font-size: 9px; }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-2-5);
  font-size: var(--fs-label-2);
  color: var(--dim-2);
}
.legend-item { display: inline-flex; align-items: center; gap: var(--sp-1-5); }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.legend-dot-t { background: var(--t); }
.legend-dot-ct { background: var(--ct); }
.legend-dot-balanced { background: var(--color-accent); }

/* -- the two heroes ------------------------------------------------------ */

.hero {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-4) var(--sp-8);
  padding: var(--sp-4-5) var(--sp-5);
  background: linear-gradient(115deg, var(--panel-2), var(--panel) 62%);
}
.hero-main { min-width: 0; }
.hero-side { margin-left: auto; text-align: right; }
.hero-title {
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--fs-display);
  letter-spacing: -0.03em;
  margin: var(--sp-1) 0 var(--sp-1-5);
}

.hero-teams {
  display: flex;
  align-items: center;
  gap: var(--sp-4-5);
  flex-wrap: wrap;
  min-width: 0;
}
.hero-team { min-width: 0; }
.hero-team-ours { text-align: right; }
.hero-team-name {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--fs-xl);
}
.hero-team-cap {
  font-size: var(--fs-label-2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim-2);
}

.score {
  display: flex;
  align-items: center;
  gap: var(--sp-2-5);
  font-weight: 500;
  font-size: clamp(34px, 6vw, 54px);
  line-height: 1;
  letter-spacing: -0.04em;
}
.score-win { color: var(--win); }
.score-loss { color: var(--loss); }
.score-them { color: var(--dim); }
.score-sep { color: var(--dim-2); font-size: 0.56em; }

.hero-facts {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-6);
}
.hero-fact .num { font-size: var(--fs-body); }

/* -- match detail: scoreboard and round strip ---------------------------- */

.board { padding: 0; overflow: hidden; margin-bottom: var(--sp-3); }
.board-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3-5);
  background: var(--panel-2);
}
.board-rule {
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: var(--mark-quiet);
  flex: none;
}
.board-rule-ours { background: var(--color-accent); }
.board-name { font-family: var(--font-heading); font-weight: 500; font-size: var(--fs-base); }
.board-score { font-size: var(--fs-sm); color: var(--dim); }
.board .absent-note { padding: var(--sp-3) var(--sp-3-5); }

.tl-strip { display: flex; gap: 3px; min-width: 520px; }
.tl-chip {
  flex: 1 1 0;
  min-width: 14px;
  height: 34px;
  border-radius: 2px;
  display: grid;
  align-content: end;
  justify-items: center;
  padding-bottom: 3px;
  font: 600 var(--fs-micro) var(--mono);
  color: var(--dim-2);
  box-shadow: inset 0 0 0 1px var(--line-2);
}
.tl-chip-t { color: var(--t); box-shadow: inset 0 0 0 1px var(--t-ring); }
.tl-chip-ct { color: var(--ct); box-shadow: inset 0 0 0 1px var(--ct-wash); }
.tl-chip-knife { color: var(--dim-2); box-shadow: inset 0 0 0 1px var(--line); }
.tl-chip-t.tl-chip-won { background: var(--t-wash); }
.tl-chip-ct.tl-chip-won { background: var(--ct-wash); }

/* The team-neutral match page draws the strip ONCE PER TEAM (there is no "our"
   strip to draw a single one of), so each carries its team's name above it.
   web/templates/match_view.html. */
.rbr-team + .rbr-team { margin-top: var(--sp-3); }
.rbr-team .ov-num-cap { margin-bottom: var(--sp-1); }

.leadline { display: block; margin-top: var(--sp-2); }
.lead-path {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 2;
  stroke-linejoin: round;
}

/* -- demo bank ----------------------------------------------------------- */

.facet-row { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5); }
.facet { min-width: 0; display: flex; flex-wrap: wrap; gap: var(--sp-1-5); align-items: center; }
.facet-cap {
  width: 100%;
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim-2);
}
.facet-n { margin-left: var(--sp-1-5); color: var(--color-text); }

.state { display: inline-flex; align-items: center; gap: var(--sp-1-5); white-space: nowrap; }
.state-dot { width: 5px; height: 5px; border-radius: 50%; flex: none; background: var(--mark-quiet); }
.state-dot-loaded { background: var(--win); }
.state-dot-duplicate { background: var(--t); }
.state-dot-no-rounds { background: var(--loss); }

.rowlink { color: var(--color-text); text-decoration: none; }
.rowlink:hover { color: var(--color-accent); }

a.mapchip { text-decoration: none; }
a.mapchip:hover { color: var(--color-accent-300); }

@media (max-width: 700px) {
  .hero-side, .hero-facts { margin-left: 0; text-align: left; }
  .hero-team-ours { text-align: left; }
}

/* ---- Round 3 sweep 2: Scouting ------------------------------------------
   The export's scout report is a MOSAIC, not a card grid: one tall panel on
   the left with two stacked beside it, then an even two-up. Token-only; the
   few raw px are the export's own geometry (bar heights, the 30px side gutter),
   which the token scale deliberately does not name. */

.sc-topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-2) var(--sp-3);
}

.sc-pick {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-2);
}
.sc-pick .field { min-width: 0; }
.sc-pick .input { min-width: 190px; }

.sc-hero { align-items: flex-end; }
.sc-hero .hero-title { font-family: var(--font-heading); font-size: var(--fs-display); }
.sc-hero-meta {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--dim-2);
  margin-top: var(--sp-1);
}

.sc-mosaic {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: start;
}
.sc-mosaic-even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.sc-tall { grid-row: span 2; }

.sc-map {
  padding: var(--sp-2-5) 0;
  border-bottom: 1px solid var(--row-rule);
}
.sc-map:last-of-type { border-bottom: 0; }

.sc-map-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1-5);
}
.sc-map-read {
  margin-left: auto;
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.sc-map .map-side { margin-bottom: var(--sp-05); }
.sc-map .meter { height: 6px; margin-top: 0; }
.sc-side-them { color: var(--loss); }
.sc-side-us { color: var(--win); }
.sc-fill-them { background: var(--loss-bar); }

.sc-map-foot {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
  margin-top: var(--sp-1-5);
}

/* R6 — the door out of one veto row and into the rounds behind it. It sits on
   the sample line because that is what it opens: the games those numbers were
   counted over, filtered to this map and this opponent. */
.sc-map-open { margin-left: var(--sp-2); white-space: nowrap; }

.sc-foot {
  font-size: var(--fs-label-2);
  color: var(--dim-2);
  margin: var(--sp-2-5) 0 0;
  text-wrap: pretty;
}

/* The feed half of the veto panel: the organiser's own picks and bans. It sits
   ABOVE our per-map read and is set apart from it — the two halves come from
   two different sources and a reader must never take one for the other. */
.sc-veto {
  padding: 0 0 var(--sp-3);
  margin-bottom: var(--sp-1);
  border-bottom: 1px solid var(--row-rule);
}
.sc-veto-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-bottom: var(--sp-2);
}
.sc-veto-cap {
  font: 600 var(--fs-label-2) var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
.sc-veto-meta {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
}

.sc-veto-list { list-style: none; margin: 0; padding: 0; }
/* Fixed columns, not `auto`: every step is its OWN grid container (one per
   <li>), so an intrinsic column sizes per row and the ladder goes ragged —
   which is what a scout reads down. 52px holds "NOBODY"; the open tail (an
   action by a side neither id matched keeps the FEED's own team name) is
   clipped with an ellipsis and keeps its full text in `title`, because one
   long stranger name must not push six aligned rows out of line. */
.sc-veto-step {
  display: grid;
  grid-template-columns: 16px 52px 58px minmax(0, 1fr);
  gap: var(--sp-2);
  align-items: baseline;
  padding: var(--sp-1) 0;
}
.sc-veto-n { font-size: var(--fs-micro-2); color: var(--dim-2); }
.sc-veto-who {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sc-veto-act { font-size: var(--fs-xs); color: var(--dim); }
.sc-veto-map { font-size: var(--fs-xs-2); }
/* A ban is a map that is gone: struck through, so the ladder reads at a glance
   without anybody parsing the word. */
.sc-veto-step[data-action="ban"] .sc-veto-map {
  color: var(--dim-2);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.sc-veto-foot {
  font-size: var(--fs-label-2);
  color: var(--dim);
  margin: var(--sp-2) 0 0;
  text-wrap: pretty;
}
.sc-veto-decider { color: var(--dim-2); }
.sc-veto-source {
  font-size: var(--fs-label);
  color: var(--dim-2);
  margin: var(--sp-1) 0 0;
  text-wrap: pretty;
}

.sc-shape .kpi-grid { gap: var(--sp-2); }
/* REMAKE-1 widened this from 150px. The head row carries an edge chip since
   the cards started printing OUR number beside theirs, and at 150px the label
   wrapped to four lines under it ("WON AFTER OPENING KILL") and pushed the
   figure out of the card. The strip is full-width now, so six fit in one row
   at 1440 and reflow on their own content below it. */
/* W2-γ raised the floor from 168 to 300, which is the whole of the fix: SIX
   cards fit across a 1107px panel at 168 and drew themselves 178px wide, so
   "Won after opening kill" set as four wrapped lines and pushed its own figure
   down the card — a 209px-tall card of which the number was the last quarter.
   At 300 the row resolves to three tracks, six cards make two full rows of
   three at 358px, and every label sets on one or two lines. Two rows and not
   the four-plus-two the walkthrough suggested: six divides evenly by three, so
   nothing is left short, and every card on the panel stays one size — the
   ruling stated above `.kpi-grid` and further down this sheet. */
.sc-kpis { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }

.sc-close {
  display: grid;
  grid-template-columns: 104px minmax(0, 1fr) 62px;
  gap: var(--sp-2-5);
  align-items: center;
  padding: var(--sp-1) 0;
}
.sc-close .meter { height: 6px; margin-top: 0; }
.sc-close-label { font-size: var(--fs-xs-2); color: var(--dim); }
.sc-close-val { text-align: right; }
.sc-close-val .num { display: block; font-size: var(--fs-xs-2); }
.sc-close-n { color: var(--dim-2); font-size: var(--fs-micro) !important; }

.sc-pace {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4-5);
  margin-top: var(--sp-2-5);
  padding-top: var(--sp-2-5);
  border-top: 1px solid var(--line);
}
.sc-pace-cell { display: flex; flex-direction: column; min-width: 0; }
.sc-pace-cell .num { font-size: var(--fs-base); }
.sc-pace-cell .num small { font-size: 0.7em; color: var(--dim); }

.sc-players {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--sp-3);
}

.sc-player {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  min-width: 0;
}
.sc-player-rating {
  flex: none;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background: var(--panel-3);
  font-size: var(--fs-md);
  color: var(--color-accent-300);
}
.sc-player-main { min-width: 0; }
.sc-player-name {
  display: block;
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--fs-base);
}
.sc-player-sub {
  display: block;
  font-size: var(--fs-sm);
  color: var(--dim);
  margin-top: var(--sp-05);
}

/* The merge, named under the heading it belongs to. Quiet on purpose: it is a
   fact about the ROW, not about the club, and a badge would make a bookkeeping
   note compete with the name it explains. */
.sc-aka {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
  margin: var(--sp-1) 0 0;
}

/* ==== REMAKE-1: the scout report's new modules ===========================
   2026-08-20. Token-only, one radius scale, and no new breakpoint: every grid
   below is `auto-fill minmax()`, so it reflows on its own content rather than
   on a viewport width somebody has to remember to keep in step with the single
   `@media (max-width: 900px)` the shell already owns. */

/* The row four modules share: label · track · value · sample. It exists so
   that openings, the buy tiers and the zone cards are ONE geometry rather than
   the eighth, ninth and tenth dialect of `.sc-close`. */
.sc-mrow {
  display: grid;
  grid-template-columns: 116px minmax(0, 1fr) 68px;
  gap: var(--sp-2-5);
  align-items: center;
  padding: var(--sp-1) 0;
}
.sc-mrow .meter { height: 6px; margin-top: 0; }
.sc-mrow-label {
  font-size: var(--fs-xs-2);
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sc-mrow-val { text-align: right; }
.sc-mrow-val .num { display: block; font-size: var(--fs-xs-2); }
.sc-mrow-n { color: var(--dim-2); font-size: var(--fs-micro) !important; }
/* The tail behind the disclosure is the same row, quieter: it is the long tail
   of five players standing somewhere, not the read. */
.sc-mrow-quiet .sc-mrow-label { color: var(--dim-2); }

/* How they open rounds — the headline rate over the two meters. */
.sc-open-head,
.sc-save-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-2-5);
  padding-bottom: var(--sp-2);
  margin-bottom: var(--sp-1);
  border-bottom: 1px solid var(--row-rule);
}

/* Economy: share on the left, win rate on the right, because they are two
   different questions about the same tier and one column would hide which. */
.sc-buyrow {
  display: grid;
  grid-template-columns: 74px minmax(0, 1fr) 76px;
  gap: var(--sp-2-5);
  align-items: center;
  padding: var(--sp-1) 0;
}
.sc-buyrow-label { font-size: var(--fs-xs-2); color: var(--dim); }
.sc-buyrow-share {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 48px;
  gap: var(--sp-2);
  align-items: center;
}
.sc-buyrow-share .meter { height: 6px; margin-top: 0; }
.sc-buyrow-pct { font-size: var(--fs-micro-2) !important; color: var(--dim-2); }
.sc-buyrow-wr { text-align: right; }
.sc-buyrow-wr .num { display: block; font-size: var(--fs-xs-2); }
/* A tier under the floor keeps its count and loses its rate. It is drawn back
   rather than hidden — the count IS the finding there. */
.sc-buyrow[data-thin="1"] .sc-buyrow-label { color: var(--dim-2); }

/* Common opponents: one grid for the head row and every row under it, so the
   columns line up without either side knowing the other's widths. */
.sc-common,
.sc-common-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 56px 56px 104px;
  gap: var(--sp-2-5);
  align-items: center;
  padding: var(--sp-1-5) 0;
}
.sc-common-head {
  font: 400 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim-2);
  padding-bottom: var(--sp-1);
  border-bottom: 1px solid var(--row-rule);
}
.sc-common { border-bottom: 1px solid var(--row-rule); }
.sc-common:last-of-type { border-bottom: 0; }
.sc-common-name {
  font-size: var(--fs-xs-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sc-common-col { text-align: right; font-size: var(--fs-xs-2); }
.sc-common-read {
  text-align: right;
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Where they go on T. THE fix: this module was 7,000px of a 10,752px page.
   Cards reflow on their own width and each one shows the map's three
   most-held callouts, with the rest one click away. */
/* W2-γ: `auto-fit`. A workspace holds this opponent on as many maps as it
   holds them on, and `auto-fill` kept minting tracks for the ones it does not:
   two maps in a 1107px panel drew two 268px lists and 571px of nothing beside
   them. These are LISTS of callouts, not a family of cards — the card-size
   ruling above `.kpi-grid` is about a set of figures that must read as one
   size, and this is a set of maps that must read as everything there is. So
   the empty tracks collapse and two maps take half the panel each. */
.sc-zones {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-2);
}
.sc-zone {
  min-width: 0;
  padding: var(--sp-2) 0;
  border-top: 1px solid var(--row-rule);
}
.sc-zone-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1);
}
.sc-zone-n {
  margin-left: auto;
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
  white-space: nowrap;
}

/* The disclosure idiom, shared by the zone tails and the declared-not-drawn
   section. `summary` is a real control, so it carries a touch target that
   clears 44px at 390 without growing the row at 1440. */
.sc-more > summary,
.sc-declared > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1-5);
  min-height: 44px;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px var(--line);
  font-size: var(--fs-label-2);
  color: var(--color-accent-300);
  cursor: pointer;
  list-style: none;
}
.sc-more > summary::-webkit-details-marker,
.sc-declared > summary::-webkit-details-marker { display: none; }
/* A real control needs a hover state. The focus ring is the sheet's global
   `:focus-visible`; this is the pointer half, and it moves the EDGE rather than
   the background so the row does not shift under the cursor. */
.sc-more > summary:hover,
.sc-declared > summary:hover {
  box-shadow: inset 0 0 0 1px var(--line-2);
  color: var(--color-accent-200);
}
.sc-more > summary::before,
.sc-declared > summary::before { content: "+"; font-family: var(--mono); }
.sc-more[open] > summary::before,
.sc-declared[open] > summary::before { content: "\2212"; }
.sc-more { margin-top: var(--sp-1); }
.sc-declared > summary { min-height: 40px; }
.sc-declared[open] > .panel { margin-top: var(--sp-2); }

/* The role badge and the stand-in flag. Both are INFERENCES and both are drawn
   as quiet chips rather than as colour: a scout who reads "AWP" as a fact of
   the schema has been misled, and the evidence line under the name is the
   answer to that. */
.sc-player-top {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-1-5);
}
.sc-role {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 1px var(--sp-1);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px var(--line-2);
  color: var(--dim);
}
/* The one role that changes how a site is TAKEN rather than who takes it.
   `color-mix` and not `rgba()`: the edge is a STRENGTH of the side token this
   badge already uses for its text, so it stays one colour role and follows the
   token if that role is ever retuned (tests/test_styleguide_render.py). */
.sc-role[data-role="awp"] {
  color: var(--ct);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ct) 35%, transparent);
}
.sc-role[data-role="entry"] {
  color: var(--t);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--t) 35%, transparent);
}
.sc-standin {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-2);
}
.sc-player[data-standin="1"] .sc-player-rating { color: var(--dim); }
.sc-player-open {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim);
  margin-top: var(--sp-05);
}
.sc-player-open .num { font-size: inherit; }
/* `--dim` and not `--dim-2`: this line is the EVIDENCE a role badge was
   inferred from, which is the thing that makes the badge honest rather than a
   claim. Footnote weight is for footnotes; a reader is meant to be able to
   check the label against this without hunting for it. */
.sc-player-role-ev {
  display: block;
  font-size: var(--fs-micro-2);
  color: var(--dim);
  margin-top: var(--sp-05);
}

/* ==== E-SCOUT: the next fixture, and the last ten ========================
   2026-08-21. Two surfaces, both extensions of geometry this page already
   owns — the hero's meta stack and `.sc-mrow`'s row — so neither introduces a
   token, a radius or a breakpoint of its own. */

/* WHY THIS TEAM, in the hero. It sits between the title and the season line
   because that is the order a reader asks it in: who, when, then how they have
   been going. Baseline-aligned rather than boxed: a card here would out-weigh
   the `h1` above it, and the fact is a caption, not a headline. */
.sc-next {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-2);
  margin: var(--sp-1-5) 0 0;
}
/* The one accent on the line, and it is a WORD rather than a colour block —
   `--accent-wash-2` behind two uppercase characters reads as a label, and a
   filled chip would be the second button-weight thing in a hero that has none. */
.sc-next-tag {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-accent);
  padding: 2px var(--sp-1);
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px var(--accent-ring);
}
.sc-next-when { font-size: var(--fs-sm); }
.sc-next-meta { font-size: var(--fs-micro-2); color: var(--dim); }

/* Their last games. `.sc-mrow`'s grammar (a fixed left, a fluid middle, a fixed
   right) widened to six columns, because this row carries six facts and a
   table would have made the doors on the end a seventh column of chrome. */
.sc-last-rows { display: grid; }
.sc-last-row {
  display: grid;
  grid-template-columns: 20px 74px minmax(96px, 1fr) minmax(0, 1.1fr) 46px minmax(0, 1.4fr);
  gap: var(--sp-2);
  align-items: center;
  padding: var(--sp-1-5) 0;
  border-bottom: 1px solid var(--row-rule);
}
.sc-last-row:last-child { border-bottom: 0; }
.sc-last-when { font-size: var(--fs-micro-2); color: var(--dim); }
.sc-last-vs,
.sc-last-comp {
  font-size: var(--fs-xs-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sc-last-comp { font-size: var(--fs-micro-2); }
.sc-last-score { font-size: var(--fs-xs-2); text-align: right; }
/* The doors. Wrapping rather than truncating: a Bo3 is three map names and the
   third one being invisible is the third map being unopenable. */
.sc-last-doors {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-1-5);
  justify-content: flex-end;
}
/* The TAG that stands where the anchors would be. Two or three words, quiet,
   and deliberately NOT link-coloured: it occupies the slot a door would, so
   anything that read as an affordance would be a control that does nothing. The
   full sentence is on its `title` and, once, in the legend below. */
.sc-last-why {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
  line-height: 1.3;
  text-align: right;
  white-space: nowrap;
}
/* The legend: each distinct absence, once, under the rows it is about. It sits
   ABOVE `.sc-foot` and reads quieter than it, because the foot is about the
   whole panel and this is about some of its rows. */
.sc-last-legend {
  margin: var(--sp-2) 0 0;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--row-rule);
}
.sc-last-note {
  margin: 0 0 var(--sp-05);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
  line-height: 1.4;
}
.sc-last-note:last-child { margin-bottom: 0; }
/* The tag inside the legend is the SAME object as the tag in the rows — same
   type, same colour — so a reader matches them by sight rather than by
   position. Only its alignment changes, because here it starts a sentence. */
.sc-last-legend .sc-last-why { text-align: left; }

/* Their recent form, in the hero. The shared strip, at the shared scale. */
.sc-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-2);
  margin-top: var(--sp-2);
}

@media (max-width: 900px) {
  /* The shell's own breakpoint, and the only one this page adds to. The two
     row grids below are the only fixed-width columns on the page; at 390 the
     label column has to give its width back to the track or the value wraps. */
  .sc-mrow { grid-template-columns: minmax(84px, 36%) minmax(0, 1fr) 60px; gap: var(--sp-2); }
  /* The label WRAPS here rather than truncating. At 96px the openings module
     rendered "Won after open…" over "Won after conc…" — two rows whose whole
     difference was in the characters the ellipsis ate, which is worse than a
     second line. Zone callouts are short enough that this costs them nothing. */
  .sc-mrow-label {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    line-height: 1.25;
  }
  .sc-buyrow { grid-template-columns: 60px minmax(0, 1fr) 66px; gap: var(--sp-2); }
  .sc-buyrow-share { grid-template-columns: minmax(0, 1fr) 40px; }
  .sc-common,
  .sc-common-head { grid-template-columns: minmax(0, 1fr) 48px 48px; }
  /* The verdict is the first thing to go: it is a reading of the two records
     beside it, and on a phone the records are the fact. */
  .sc-common-read { display: none; }

  /* E-SCOUT. Six columns do not fit in 390px and none of the six is droppable —
     the date, the opponent, the score and the door are all the row. So it
     becomes two lines: the result line, then the doors under it, full width.
     `grid-template-areas` rather than six overrides, because the second line's
     whole job is to span. */
  .sc-last-row {
    grid-template-columns: 20px 74px minmax(0, 1fr) 46px;
    grid-template-areas:
      "badge when vs    score"
      "comp  comp comp  comp"
      "doors doors doors doors";
    row-gap: var(--sp-1);
    padding: var(--sp-2) 0;
  }
  .sc-last-row > .badge { grid-area: badge; }
  .sc-last-when { grid-area: when; }
  .sc-last-vs { grid-area: vs; }
  .sc-last-score { grid-area: score; }
  /* The division gets its own line rather than being dropped. Which division a
     game was played in is half of what this panel exists to carry, and a phone
     is where a coach reads it on the way to the venue. */
  .sc-last-comp { grid-area: comp; }
  .sc-last-doors { grid-area: doors; justify-content: flex-start; }
  .sc-last-why { text-align: left; }
}

/* ---- the shared prep checklist (components/prep_checklist.html) ----------
   One list treatment for the two surfaces that keep one: the scout report's
   per-opponent list and the hub's. Rows are a three-column grid — box, text,
   remove — so every checkbox lines up down the left whatever the text does. */
.pc-list { list-style: none; margin: 0 0 var(--sp-2-5); padding: 0; }
.pc-item {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) auto;
  gap: var(--sp-2);
  align-items: baseline;
  padding: var(--sp-1-5) 0;
  border-bottom: 1px solid var(--row-rule);
}
.pc-item:last-child { border-bottom: 0; }
.pc-tick, .pc-drop { display: contents; }

/* The box is the control. `aria-pressed` carries the state for a reader who
   never sees the tick, and the border does it for one who does. */
.pc-box {
  width: 16px;
  height: 16px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-accent);
  cursor: pointer;
  transition: border-color 0.13s ease, background 0.13s ease;
}
.pc-box:hover { border-color: var(--color-accent); }
.pc-item[data-done="1"] .pc-box {
  border-color: var(--color-accent);
  background: var(--panel-3);
}
/* `overflow-wrap: anywhere` because this line is TYPED (R6). Every other
   string on this page is one the product wrote; a checklist item is one a coach
   wrote, and a coach can paste a 200-character URL or a run of Xs with no space
   in it. Measured at 390px with a 200-character unbroken line: the document
   scrolled sideways by 937px — the whole page, not the panel, because
   `text-wrap: pretty` is about where to BREAK a line and has no opinion about a
   word that cannot break at all. `anywhere` rather than `break-word` so the
   panel's intrinsic min-content width collapses with it; `break-word` leaves
   the element demanding the long word's width and the grid still blows out. */
.pc-text {
  font-size: var(--fs-xs-2);
  text-wrap: pretty;
  overflow-wrap: anywhere;
  min-width: 0;
}
/* A done line is not deleted and not hidden: it is quiet. An hour before a
   match the question is what HAS been done, so the row has to stay legible. */
.pc-item[data-done="1"] .pc-text { color: var(--dim-2); }
.pc-drop .btn { visibility: hidden; }
.pc-item:hover .pc-drop .btn,
.pc-drop .btn:focus-visible { visibility: visible; }

.pc-add { display: flex; gap: var(--sp-2); align-items: center; }
.pc-add .input { flex: 1 1 auto; min-width: 0; }

/* Who wrote the line (0039). Inline after the text so a three-column row stays
   three columns, and quiet: it is provenance, not a status. */
.pc-source { margin-left: var(--sp-1-5); vertical-align: baseline; }

/* The suggestion strip: lines the page can OFFER, above the list and visibly
   not on it. A dashed edge rather than a filled panel, because "not yet a row"
   is exactly what the treatment has to say. */
.pc-suggest {
  margin: var(--sp-2-5) 0;
  padding: var(--sp-2) var(--sp-2-5);
  border: 1px dashed var(--line-2);
  border-radius: var(--radius-sm);
}

.pc-suggest-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: var(--sp-2);
  align-items: baseline;
  padding: var(--sp-1-5) 0;
  border-bottom: 1px solid var(--row-rule);
}
.pc-suggest-row:last-of-type { border-bottom: 0; }
.pc-suggest .sc-foot { margin-top: var(--sp-2); }

@media (max-width: 560px) {
  /* At phone width the two controls sit under the sentence rather than
     squeezing it into a column two words wide. */
  .pc-suggest-row { grid-template-columns: minmax(0, 1fr); }
  .pc-suggest-add, .pc-suggest-row > .btn { justify-self: start; }
}

@media (max-width: 900px) {
  .sc-mosaic, .sc-mosaic-even { grid-template-columns: 1fr; }
  .sc-tall { grid-row: auto; }
  .sc-pick { margin-left: 0; }
}

@media (hover: none) {
  /* Touch has no hover, so a control revealed by one is a control that does not
     exist. The Remove button is always visible where there is no pointer. */
  .pc-drop .btn { visibility: visible; }
}

/* ---- Round 3 sweep 2: League + Manage leagues (feed-gated skeletons) -----
   The export's own mosaic reused from .sc-mosaic; what is added here is the
   tab strip, the standings column rule and the two empty plot boxes. They are
   drawn at full size on purpose: a skeleton whose panels collapse to their
   text is a missing page, not a declared one. */

.lg-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-5);
  padding-bottom: var(--sp-2-5);
  border-bottom: 1px solid var(--line);
}

/* A shared League module with nothing to say is a NOTE, not a card (LOOK-1).
   The same rule the two composition sheets apply to their own modules, on the
   three the compositions share (demos delivered, integration, the sync log),
   so an awaiting page is one language rather than notes beside boxes. Scoped
   to `.lg-slot` on purpose: `.panel-disabled` is worn across this product and
   every other page's meaning for it is untouched. */
.lg-slot.panel-disabled {
  opacity: 1;
  background: none;
  border-color: transparent;
  border-top-color: var(--line);
  border-radius: 0;
  padding: var(--sp-3) var(--sp-1) var(--sp-2);
}

.lg-slot.panel-disabled .panel-head { margin-bottom: var(--sp-2); }
.lg-slot.panel-disabled .panel-kicker { color: var(--dim-2); }

/* LP-2 — the one line the League page keeps of its feed. Freshness, one health
   word with the fact it summarises, and the way to the detail on Manage
   leagues. It reads as a caption rather than a panel on purpose: it is the
   smallest true statement about the connection, and everything larger moved. */
.lg-conn {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  font-size: var(--fs-xs-2);
}
.lg-conn-health {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}
.lg-conn-health[data-health="healthy"] { color: var(--win); }
.lg-conn-health[data-health="reporting an error"] { color: var(--loss); }
.lg-conn-note { color: var(--dim-2); font-size: var(--fs-label-2); min-width: 0; }
.lg-conn-more { margin-left: auto; white-space: nowrap; }

/* The organiser's own name for a side the bank knows by another one (R5). Small
   print under the heading, because it is the same club and not a second fact. */
.lgv-next-aka {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
  margin-top: var(--sp-05);
}

/* The organiser's own id for a demo. Provenance, so it sits under the map name
   at caption size instead of standing in for it (LP-2). */
.lg-map-id {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
}

/* Manage leagues — whose connection a row is, and how that row is doing. */
.page-scope, .section-lede {
  font-size: var(--fs-label-2);
  color: var(--dim-2);
  margin: var(--sp-1-5) 0 0;
  text-wrap: pretty;
}
.cn-owner {
  font: 600 var(--fs-micro-2) var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
  text-decoration: none;
}
a.cn-owner:hover { color: var(--color-accent); }
.cn-health {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  color: var(--dim-2);
}
.cn-health[data-health="healthy"] { color: var(--win); }
.cn-health[data-health="reporting an error"] { color: var(--loss); }
.cn-ops { margin-top: var(--sp-2-5); border-top: 1px solid var(--line); padding-top: var(--sp-2); }
.cn-ops .lg-log { margin-top: var(--sp-2-5); }
/* The card runs the page's full width now (see modules-b3's `.cn-list`), and a
   label/value pair stretched across 1200px is two things at opposite ends of a
   line nobody's eye crosses. The measure is capped where the longest value —
   the scope list — still fits on one line. */
.cn-ops .lg-facts-rows, .cn-ops .lg-log, .cn-ops .lg-meta { max-width: 62ch; }

@media (max-width: 640px) {
  /* The link stops being pushed to a line of its own by `margin-left: auto`
     once the strip wraps: at this width every part is already on its own row. */
  .lg-conn-more { margin-left: 0; }
}

.lg-tab {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--fs-md);
  color: var(--dim);
  text-decoration: none;
}
.lg-tab-off { color: var(--dim-2); cursor: not-allowed; }
.lg-tab-link { margin-left: auto; font-family: var(--mono); font-size: var(--fs-xs-2); }
.lg-tab-link:hover { color: var(--color-accent); }

/* The competition switch, drawn only when a team has two or more of them.
   The reference's own treatment: the league's name on the baseline with its
   competition beside it in mono caps, and the ACTIVE tab underlined against the
   strip's own rule — which is why the bottom padding is pulled back through the
   container's, so the underline lands ON the line rather than floating above it. */
.lg-tab-switch {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding-bottom: var(--sp-2-5);
  margin-bottom: calc(-1 * var(--sp-2-5));
  transition: color 0.13s ease;
  /* A tab is allowed to be narrower than the longest token inside it, so the
     strip's min-content width is never a competition identifier's width. The
     rendered page at 390px does not overflow without this — it was measured
     in an iframe at a true 390, because a Chrome window clamped to its own
     platform minimum renders a 500px page and crops it, which reads as an
     overflow that is not there. This is the guard, not a fix: a connection
     whose organiser publishes no competition name still prints its raw id.
     LOOK-1. */
  min-width: 0;
  max-width: 100%;
}
.lg-tab-switch:hover { color: var(--color-text); }
.lg-tab-name { font-family: var(--font-heading); font-weight: 500; }
.lg-tab-meta {
  font-family: var(--mono);
  font-weight: 600;
  font-size: var(--fs-micro-2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-2);
  min-width: 0;
  overflow-wrap: anywhere;
}
.lg-tab-on { color: var(--color-text); box-shadow: inset 0 -2px 0 var(--color-accent); }
.lg-tab-on .lg-tab-meta { color: var(--dim); }

/* 0048 — a division the sweep TABULATES and never fetches demos for. The mark
   is deliberately quiet and deliberately typographic rather than coloured: it
   is a fact about depth, not a warning, and a four-division strip with two
   coloured badges in it would read as two errors. The tab itself is dimmed one
   step, so the strip's own hierarchy says "these two have stats behind them"
   before a word is read — and the word is still there, because colour and
   weight alone are not facts every reader receives. */
.lg-tab-thin { color: var(--dim-2); }
.lg-tab-thin:hover { color: var(--dim); }
.lg-tab-note {
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--fs-micro-2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim-2);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 0 var(--sp-1);
  white-space: nowrap;
}
.lg-tab-on .lg-tab-note { color: var(--dim); border-color: var(--line-2, var(--line)); }

@media (max-width: 640px) {
  /* Four divisions do not fit one row at 390px, so the strip wraps and each tab
     keeps its own baseline rather than the note dropping onto a line of its
     own away from the tab it qualifies. */
  .lg-tab-switch { flex-wrap: wrap; row-gap: var(--sp-1); }
}

/* `.lg-mosaic` no longer overrides `.sc-mosaic`'s `align-items: start`.
   Stretching made the two panels one tidy rectangle and put the difference
   between them INSIDE the shorter one — on a finished conference that was
   183px of nothing under Our season, next to a field chart with eleven score
   groups in it. A ragged pair is what the rest of this page does (integration
   and the sync log have never matched) and each panel is now as tall as what
   it has to say. LAYOUT-1. */

/* `.lg-table-head` was deleted in LAYOUT-1 with its one caller. It had FOUR
   grid tracks for the standings' five column names, so "Pts" wrapped onto a
   second row — invisible while a 120px dash box sat under it, and the whole
   module once that box was gone. The gated head draws on `.lgv-thead` now,
   which is the live table's own grid, so the two cannot disagree again. */

/* `.lg-table-empty` — a 120px box holding one em dash — was deleted in
   LAYOUT-1 along with its seven callers. A module with nothing to say now
   draws its head and ONE line (`.lgv-absent` / `.lgs-absent`), because seven
   of those boxes on one League page was the "huge empty spaces" the operator
   reported. Nothing outside the League page ever used it. */

.lg-identity { display: flex; flex-direction: column; gap: var(--sp-05); }
.lg-pos {
  font-size: var(--fs-display);
  line-height: 1;
  color: var(--dim-2);
}

.lg-kpis { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5); }
.lg-kpi { display: flex; flex-direction: column; min-width: 0; }
.lg-kpi .num { color: var(--dim-2); }

.lg-plot { display: block; width: 100%; margin: var(--sp-2) 0; }

.lg-facts { display: flex; flex-wrap: wrap; gap: var(--sp-2-5) var(--sp-5); }
.lg-fact { display: flex; flex-direction: column; min-width: 0; }
.lg-fact .num { color: var(--dim-2); }

/* ---- Round 3 sweep 2: Account -------------------------------------------
   The export's three-up card sections plus one row shape used everywhere a
   setting is DECLARED: label, value slot, reason. The reason is part of the
   row rather than a tooltip because it is the row's content here — the value
   is the em dash, and a dash with no explanation beside it is just missing. */

.acct-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-3) var(--sp-6);
}

.acct-id { display: flex; align-items: center; gap: var(--sp-3-5); min-width: 0; }

.acct-avatar {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--panel-3);
  box-shadow: inset 0 0 0 1px var(--line-2);
  font: 600 var(--fs-base) var(--mono);
  color: var(--color-accent-300);
}

.acct-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.acct-actions-row { margin-left: 0; margin-top: var(--sp-2-5); }

.acct-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(288px, 100%), 1fr));
  gap: var(--sp-3);
  align-items: start;
}

.acct-plan {
  background: linear-gradient(115deg, var(--panel-2), var(--panel) 62%);
  border-color: var(--accent-ring-soft);
}

.acct-plan-name {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--fs-2xl);
  letter-spacing: -0.02em;
}

.acct-price {
  font-size: var(--fs-lg);
  color: var(--dim-2);
  margin-top: var(--sp-1);
}
.acct-price-note {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  color: var(--dim);
}

.acct-caps { margin-top: var(--sp-3); }

/* The Usage card's door into the intake (R7b). It replaced a sentence saying
   upload was not built, so it is drawn as a real onward link in the accent the
   rest of the product gives "go here next" — not as a button, because the card
   it sits in is a statement about the plan and the upload happens elsewhere. */
.acct-upload {
  color: var(--color-accent-300);
  text-decoration: none;
}

.acct-upload:hover { text-decoration: underline; }

.acct-danger { border-color: var(--loss-ring); }
.acct-danger .panel-kicker { color: var(--loss); }

.set-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--sp-1) var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--row-rule);
}
.set-row:last-child { border-bottom: 0; }

.set-label { font-size: var(--fs-sm-2); min-width: 0; }
.set-value { text-align: right; font-size: var(--fs-sm-2); }
.set-value-off { color: var(--dim-2); }
.set-reason {
  grid-column: 1 / -1;
  font-size: var(--fs-label-2);
  color: var(--dim-2);
  text-wrap: pretty;
}

.side-foot[aria-current="page"] { background: var(--accent-wash); }

@media (max-width: 700px) {
  .acct-actions { margin-left: 0; }
}

/* ── solo section (Round 3 sweep 3) ───────────────────────────────────
   The reference opens every solo view with the same band of cards and then
   varies the module under it. One vocabulary here, four pages using it: the
   `ov-*` band that sweeps 1-2 copied into four templates is what this replaces
   for the solo side. */

.sl-band {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(228px, 100%), 1fr));
  gap: var(--sp-3);
  min-width: 0;
}

.sl-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1-5);
  padding: var(--sp-3-5) var(--sp-4);
  min-width: 0;
}

.sl-kpi-head { display: flex; align-items: center; gap: var(--sp-1-5); }

.sl-kpi-label {
  font-size: var(--fs-label-2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.sl-kpi-value {
  font-weight: 500;
  font-size: 30px;
  line-height: 1.02;
  letter-spacing: -0.03em;
}
.sl-kpi-value small { font-size: 0.55em; color: var(--dim); }
.sl-kpi-value-good { color: var(--color-accent); }
.sl-kpi-value-bad { color: var(--loss); }
.sl-kpi-value-empty { color: var(--dim-2); }

.sl-kpi-sample { font-family: var(--mono); font-size: var(--fs-xs-2); color: var(--dim-2); }
.sl-kpi-context { font-size: var(--fs-label-2); color: var(--dim-2); text-wrap: pretty; }

/* The SAMPLE FLOOR on a solo card (W3), and it is the `.kpi[data-thin]` rule
   above said again for this band's own class names — same demotion, same
   argument. The figure shrinks and the card takes the recessed ground; the
   COLOUR does not move, because dropping a thin number to `--dim-2` would make
   it harder to read, which is the opposite of the goal. The reader is being
   told how much of the number to spend, not stopped from seeing it.

   It matters more here than on a team page: these cards sit at the top of a
   page whose filter bar exists to make samples smaller. */
.sl-kpi[data-thin] {
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
}
.sl-kpi[data-thin] .sl-kpi-value { font-size: 20px; }
.sl-kpi[data-thin] .sl-kpi-sample { color: var(--t); }

/* The same statement on a roster card, where a narrow selection cuts six
   samples at once and the module's whole job is to rank them against each
   other. A class rather than `data-thin` because that tag's attribute order is
   pinned by two suites — see the comment in templates/skill.html. */
.rs-card.rs-thin .rs-value { font-size: 0.7em; }
.rs-card.rs-thin .rs-sample { color: var(--t); }

/* -- my matches: the library, one player's perspective ------------------- */

.sl-mine {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--dim);
  white-space: nowrap;
}
.sl-mine-tone { color: var(--color-accent-300); }

/* -- my maps: the per-map card's own foot ------------------------------- */

.sl-mapfoot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  padding-top: var(--sp-2-5);
  border-top: 1px solid var(--line);
}
.sl-mapfoot > span { min-width: 0; }
.sl-mapfoot-end { margin-left: auto; text-align: right; }
.sl-mapcap {
  display: block;
  font-size: var(--fs-micro-2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim-2);
  white-space: nowrap;
}
.sl-mapval { display: block; font-family: var(--mono); font-weight: 500; font-size: var(--fs-sm-2); }
.sl-mapsub { display: block; font-family: var(--mono); font-size: var(--fs-micro-2); color: var(--dim-2); }

/* -- progress: the month columns and the movers ------------------------- */

.sl-months {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(140px, 100%), 1fr));
  gap: var(--sp-2-5);
  padding: var(--sp-3-5) var(--sp-3);
}

.sl-month { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
/* An anchor since R2 made the column a door into that month's games. Kept
   visually identical at rest — the panel is a row of bars and an underlined
   month key would read as a different kind of object — and marked on hover and
   on focus, where the affordance is being asked for. */
.sl-month-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  text-decoration: none;
  color: inherit;
}
.sl-month-head:hover .sl-month-cap,
.sl-month-head:focus-visible .sl-month-cap {
  color: var(--color-text);
  text-decoration: underline;
}
.sl-month-cap {
  font: 600 var(--fs-xs-2) var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
.sl-month-val {
  margin-left: auto;
  font-family: var(--mono);
  font-weight: 500;
  font-size: var(--fs-xl);
  letter-spacing: -0.02em;
}
.sl-month-track {
  position: relative;
  height: 78px;
  border-radius: var(--radius-sm);
  background: var(--track);
  overflow: hidden;
}
.sl-month-fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--color-accent);
}
.sl-month-sub { font: 400 var(--fs-xs-2) var(--mono); color: var(--dim-2); }

.sl-movers {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(268px, 100%), 1fr));
  gap: var(--sp-3);
  min-width: 0;
}
.sl-mover { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3-5) var(--sp-4); }
.sl-mover-head { display: flex; align-items: baseline; gap: var(--sp-2); }
.sl-mover-label { font-size: var(--fs-sm-2); min-width: 0; }
.sl-mover-delta {
  margin-left: auto;
  font: 600 var(--fs-body) var(--mono);
  white-space: nowrap;
}
.sl-mover-up { color: var(--color-accent); }
.sl-mover-down { color: var(--loss); }
.sl-mover-flat { color: var(--dim-2); }
.sl-mover-arc { display: flex; align-items: baseline; gap: var(--sp-2); font-family: var(--mono); }
.sl-mover-from { font-size: var(--fs-sm-2); color: var(--dim-2); }
.sl-mover-to { font-size: var(--fs-body); }
.sl-mover-note {
  font-size: var(--fs-label-2);
  color: var(--dim-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
  text-wrap: pretty;
}

/* -- panel with the reference's tinted head ----------------------------- */

.sl-panel { padding: 0; overflow: hidden; }
.sl-panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2-5) var(--sp-3);
  background: var(--accent-wash);
}
.sl-panel-cap {
  font-size: var(--fs-label-2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}
.sl-panel-meta { font: 400 var(--fs-xs-2) var(--mono); color: var(--dim-2); }
.sl-panel-chips { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--sp-1-5); }

/* -- the since-last-month digest (R4, review §7) ------------------------
   Four figures side by side, each with the month it moved from. Cells in one
   grid rather than four KPI cards: the reading is the RELATIONSHIP between
   them ("rating up, damage down"), and four cards would be four separate
   headlines with nothing drawn between any of them. */

.digest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: 1px;
  background: var(--line);
}

.digest-cell {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-3-5);
  background: var(--panel);
  min-width: 0;
}

.digest-label {
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

.digest-line {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  min-width: 0;
}

.digest-value {
  font: 500 var(--fs-2xl) / 1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.digest-delta {
  font: 600 var(--fs-sm) var(--mono);
  color: var(--dim);
}

.digest-delta[data-tone="up"] { color: var(--stat-good); }
.digest-delta[data-tone="down"] { color: var(--stat-bad); }

.digest-sample {
  font: 400 var(--fs-xs) var(--mono);
  color: var(--dim);
  overflow-wrap: anywhere;
}

.digest-note {
  margin: 0;
  padding: var(--sp-3) var(--sp-3-5);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--dim);
  text-wrap: pretty;
}

/* -- skill category: the header's score, and the detail disclosure ------ */

.sl-cat-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-4) var(--sp-8);
}
.sl-cat-intro { max-width: 64ch; min-width: 0; }
.sl-cat-score { margin-left: auto; text-align: right; }
.sl-cat-score-cap {
  font-size: var(--fs-micro-2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim-2);
}
.sl-cat-score-val {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 30px;
  line-height: 1.1;
  color: var(--dim-2);
}
.sl-cat-score-sub { font: 400 var(--fs-xs) var(--mono); color: var(--dim-2); max-width: 34ch; }

.sl-detail > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-xs-2);
  font-weight: 500;
  color: var(--color-accent-300);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
}
.sl-detail > summary::-webkit-details-marker { display: none; }
.sl-detail[open] > summary .caret { transform: rotate(90deg); }
.sl-detail-body { margin-top: var(--sp-2); }

/* -- my demos: the header's declared-inactive actions ------------------- */

.sl-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--sp-2); }

@media (max-width: 700px) {
  .sl-actions { margin-left: 0; }
  .sl-cat-score { margin-left: 0; text-align: left; }
}

/* ── share links: the public shell and the Share affordance ───────────── */

/*
   The SHARED shell (components/page_shell.html). Not a variant of `.shell`:
   that one is a two-column grid whose first column is the rail, and a rail-less
   page in it would be a grid with an empty column. This is a header over a
   single centred column, which is what the page actually is.
*/
.sharedview {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.sharedview > .main {
  width: 100%;
  margin: 0 auto;
}

.sharedbar {
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--panel-2), var(--color-bg));
  padding: var(--sp-3) var(--sp-3-5);
}

.sharedbar-in {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  max-width: var(--main-max);
  margin: 0 auto;
}

.sharedbar-brand {
  color: var(--color-accent);
  align-self: center;
  display: inline-flex;
}

.sharedbar-team {
  font-size: var(--fs-body);
  font-weight: 500;
  letter-spacing: -0.015em;
}

/* The page this link opens, in the rail's own caption voice — it is the same
   role the sidebar's `aria-current` entry plays for somebody who has one. */
.sharedbar-page {
  font-size: var(--fs-label-2);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.sharedbar-by {
  font-size: var(--fs-sm);
  color: var(--dim);
  margin-left: auto;
}

.sharedbar-note {
  max-width: var(--main-max);
  margin: var(--sp-2) auto 0;
  font-size: var(--fs-sm);
  color: var(--dim-2);
}

@media (max-width: 700px) {
  .sharedbar-by { margin-left: 0; }
}

/* -- the Share panel (components/share_panel.html) ---------------------- */

/*
   `<details>` styled so the summary IS the design system's secondary button.
   `display: inline-flex` on the summary is what removes the native marker in
   Firefox; `::-webkit-details-marker` covers the other family, the same pair
   `.sl-detail` above already uses.
*/
/* `inline-block` sizes to MAX-CONTENT, and the content is a button whose label
   is built from the page's name — "Create a link to Head to head · every
   opponent" is not a width anybody chose. Measured on Overview at 390: the
   panel and its form reached 453px against a 390px viewport, so the widest
   thing on the phone was a control nobody had opened. The three lines below
   cap the box at its container and let the label wrap; nothing about the
   desktop placement moves, because at that width the content was never the
   binding constraint. (R6) */
.sharep {
  position: relative;
  display: inline-block;
  max-width: 100%;
}

.sharep-body,
.sharep-new,
.sharep-new > .btn {
  max-width: 100%;
  min-width: 0;
}

/* A button label is normally one line — that is what makes a row of them read
   as a row. This one is a SENTENCE with a page name in it, so it wraps rather
   than setting the width of everything around it. */
.sharep-new > .btn {
  white-space: normal;
  text-align: center;
}

.sharep > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  list-style: none;
  cursor: pointer;
}

.sharep > summary::-webkit-details-marker { display: none; }

.sharep-body {
  margin-top: var(--sp-2);
  padding: var(--sp-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: min(560px, 100%);
}

/* Above 700px the panel floats over the page rather than pushing the header
   down: it is a transient control, and a header that jumps 200px when you open
   it moves the thing you were about to read. */
@media (min-width: 700px) {
  .sharep[open] > .sharep-body {
    position: absolute;
    right: 0;
    z-index: 20;
    box-shadow: var(--shadow-lg);
    /* WIDTH, EXPLICITLY (R7b, caught in the browser pass). An absolutely
       positioned box resolves a percentage against its POSITIONED PARENT, and
       that is `.sharep` — an `inline-block` shrink-wrapped to the Share button,
       about 74px. So the `max-width: 100%` above (written for the in-flow case,
       where 100% is the header) was clamping the floating panel to the width of
       the button that opens it: measured at 1440 with the panel open, the lede
       rendered one word per line. `min-width` could not save it — a min always
       beats a max, so the two were fighting over the same 74px.
       420 and not the in-flow 560: the widest thing this panel floats out of is
       the overview's `.ov-side`, a 485px column with `overflow: auto` — so a
       560px panel anchored to its right edge had its first 75px clipped by the
       column, measured at 1440. A control that opens from a card has to fit
       inside the card's own scrollport, and the viewport clamp keeps it on
       screen where the header puts the button near an edge. */
    width: min(420px, calc(100vw - var(--sp-8) * 2));
    max-width: none;
  }
}

/* ── B1: the clamp guarded the wrong edge ──────────────────────────────────
   `right: 0` anchors the panel's RIGHT edge to the trigger's, and the width
   clamp above bounds it against the viewport's WIDTH — which is a guard for
   the right gutter and no guard at all for the left. Measured on Players
   (`/team/<id>/stats`) at 700, 768, 820, 880, 899 and 900: the trigger sits at
   x=14, the panel renders at x=-338 and 338 of its 420px are off the left of
   the screen, unscrollable. Overview never showed it because its trigger is
   the last item of a `.panel-head` row and therefore always right-of-centre.

   The difference is not a breakpoint and a media query would be a guess at
   one: Players' head row is `flex-wrap: wrap`, so the trigger opens a wrapped
   line at x=14 below ~940px and closes an unwrapped one at x=809 above it. The
   flip point is where a title, a button and its caption stop fitting — it moved
   with the caption's text once already, and it is not 900 (measured: still 102px
   off-screen AT 900).

   So the question is "is there room on that side", which no selector can ask —
   and this stylesheet already answers that question twice, in the definition
   note's section variant: `position-try-fallbacks` re-runs the placement only
   when the first one would overflow. Same mechanism, same `@supports` gate,
   same reason `anchor-scope` is part of it (an unscoped name resolves to the
   LAST element carrying it, which would point every panel at one trigger).

   `block-end span-inline-start` IS the rule above, said declaratively: hang
   below, spanning from the trigger's inline-end edge leftwards — the panel's
   right edge on the trigger's right edge. The single fallback is the mirror,
   and the browser takes it only when the first would leave the viewport.
   `position: fixed` is what anchor positioning needs, and it pays for itself
   here: the panel stops being clipped by `.ov-side`'s `overflow: auto`, which
   is the scrollport the 420px width was chosen against. The width stays 420
   anyway — this is a placement fix, not a resizing of the panel. */
@supports (anchor-scope: --a) and (position-area: block-end) {
  @media (min-width: 700px) {
    .sharep {
      anchor-name: --sharep;
      anchor-scope: --sharep;
    }

    .sharep[open] > .sharep-body {
      position: fixed;
      position-anchor: --sharep;
      position-area: block-end span-inline-start;
      position-try-fallbacks: block-end span-inline-end;
      inset: auto;
      margin: var(--sp-2) 0 0;
      /* The same `min-width` fight R7b recorded above, from the other side. In
         the absolute placement `min(560px, 100%)` resolved 100% against the
         74px trigger and never bound; a FIXED box resolves it against the
         viewport, so the in-flow minimum came back as 560 and beat the 420
         width — measured, on both pages, at every width from 700 to 1920. The
         width above is the panel's size; this is the line that lets it be. */
      min-width: 0;
    }
  }
}

/* A browser with no anchor positioning yet (Firefox and Safari at time of
   writing) keeps the absolute placement above, which is correct on every
   placement where the trigger CLOSES its row — Overview and the match page, at
   every width, measured. The one placement where it opens one is Players' head
   row, and the reversion for it lives in that page's own sheet
   (`team-stats.css`), because `.ts-head-row` is that sheet's class and a
   component stylesheet naming a page's row would be the coupling this
   project's UI rule exists to prevent. */

.sharep-lede {
  margin: 0;
  font-size: var(--fs-sm-2);
  color: var(--dim);
  text-wrap: pretty;
}

.sharep-new { margin: 0; }

/* WHAT THE LINK SHOWS (R7b). It is the one sentence on this panel a person can
   regret not reading, so it is not another `--dim` line under the lede: it wears
   the caution hue the product already spends on "your filter moved but these
   numbers did not", which is the same kind of statement — a fact about what you
   are about to look at, before you act on it. Body colour, not `--dim`: a
   disclosure printed quieter than the text around it is a disclosure with a
   footnote's weight and a headline's consequence. */
.sharep-expose {
  margin: 0;
  padding: var(--sp-2) var(--sp-2-5);
  border-radius: var(--radius-sm);
  border: 1px solid var(--t-ring);
  background: var(--t-wash);
  font-size: var(--fs-sm-2);
  line-height: 1.5;
  color: var(--color-text);
  text-wrap: pretty;
}

/* Beside the revoke controls, which is the only thing that ends a link. */
.sharep-forever {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--dim);
  text-wrap: pretty;
}

.sharep-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

/* THREE tracks since R7b: the URL, the copy control, and the revoke that spans
   both rows. It was two, and the copy button had nowhere to go that was not the
   revoke's column. */
.sharep-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--sp-1) var(--sp-2);
  padding: var(--sp-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
}

/* The row for the page you are standing on. One accent edge, no second copy of
   the word: the tag inside it already says "this page". */
.sharep-row-here { border-color: var(--accent-ring-soft); }

/* A readonly INPUT since R7b, so the value can be reached with a keyboard. It
   is drawn as text and not as a form field — a bordered box here would read as
   something to fill in — but it keeps a field's behaviour: the caret enters it,
   the value scrolls inside it, and `text-overflow` never eats a character
   somebody is about to copy. */
.sharep-url {
  font-family: var(--mono);
  font-size: var(--fs-xs-2);
  width: 100%;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text);
  text-overflow: ellipsis;
}

.sharep-url:focus-visible {
  outline: 2px solid var(--accent-ring);
  outline-offset: 2px;
  border-radius: 2px;
}

.sharep-meta {
  grid-column: 1 / span 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--dim-2);
}

.sharep-when { font-family: var(--mono); }

.sharep-copy { grid-column: 2; white-space: nowrap; }

.sharep-revoke { grid-column: 3; grid-row: 1 / span 2; }

/* Match detail's header strip: the backlink and the Share control share a line,
   and on a shared page exactly one of the two is present. */
.md-top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3-5);
}

.md-top .backlink { margin-bottom: 0; }

/* ── rail badges and the connected-leagues panel ──────────────────────
   The 2026-08-06 screenshots: Demo bank and My Demos carry a dim mono count,
   League carries its position in an accent pill, and the foot of the rail is a
   real panel rather than empty field. Appended as one block so the two other
   lanes editing this file tonight cannot land inside it. */

.navbadge {
  margin-left: auto;
  flex: none;
  font: 600 var(--fs-micro-2) var(--mono);
  color: var(--dim-2);
  font-variant-numeric: tabular-nums;
}

/* The position is an ASSERTION, not a tally, and the design spends the accent
   on exactly that difference. */
.navbadge-rank {
  padding: 2px 5px;
  border-radius: 3px;
  background: var(--color-accent-900);
  color: var(--color-accent-300);
  text-transform: uppercase;
}

/* In the sub-900px strip the rail is a horizontal scroller and every pixel is
   a destination; `margin-left: auto` inside a nowrap tab would push the badge
   off its own label. Sits directly beside it there. */
.navbadge { margin-left: var(--sp-1-5); }

@media (min-width: 900px) {
  .navbadge { margin-left: auto; }

  /* Two panels can share the foot (the match library hands in its upload
     budget). Only the first claims the free space, or `margin-top: auto` on
     both would distribute the gap between them. */
  .side-int + .side-int { margin-top: var(--sp-2); }

  .side-lg { display: flex; }
}

/* NOT `display: flex` here: `.side-int` is `display: none` in the sub-900px
   strip (there is no room for a panel in a horizontal scroller, and the
   reference hides its own for the same reason), and a same-specificity rule
   later in this file would quietly turn it back on. The layout is declared
   inside the 900px block below, where the panel actually exists. Caught in the
   browser at 500px, where the panel had landed on top of the nav. */
.side-lg { flex-direction: column; gap: 7px; }

.side-lg-cap {
  display: block;
  padding: 0;
}

.side-lg-row {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  color: var(--dim);
  text-decoration: none;
}

.side-lg-row:hover {
  background: var(--hover-tint);
  color: var(--color-text);
}

.side-lg-dot {
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  background: var(--win);
}

/* A paused connection is not a broken one — it is a decision somebody made, so
   it reads as inert rather than as an error. */
.side-lg-dot[data-enabled="0"] { background: var(--mark-quiet); }

.side-lg-name {
  font-size: var(--fs-xs-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.side-lg-pos {
  margin-left: auto;
  flex: none;
  font: 600 var(--fs-micro-2) var(--mono);
  color: var(--dim-2);
  text-transform: uppercase;
}

.side-lg-sync {
  font: 400 var(--fs-label) var(--mono);
  color: var(--dim-2);
}

.side-lg-manage {
  width: 100%;
  padding: 4px var(--sp-2);
  font-size: var(--fs-xs-2);
  text-align: center;
  text-decoration: none;
}

/* You are already there (R7b). The button is the shell's only link to Manage
   leagues, so on that page it is the affordance that carries `page` — and a
   primary button pointing at the page you are on has to stop asking to be
   pressed. Ring instead of fill, which is this system's own way of saying a
   control is current rather than available. */
.side-lg-manage[aria-current="page"] {
  background: transparent;
  color: var(--color-accent-300);
  box-shadow: inset 0 0 0 1px var(--accent-ring);
}

/* ── the solo card's bar SCALE (night-QA polish p15) ──────────────────────
   Drawn directly under the meter it belongs to, in the mono voice the sample
   line already uses for "what this number is measured over" — this is the same
   kind of fact about the shape above it. Dimmer and smaller than the sample:
   it is a caption on a graphic, not a second reading. */
.sl-kpi-scale {
  font-family: var(--mono);
  font-size: var(--fs-micro-2);
  letter-spacing: 0.04em;
  color: var(--dim-2);
}

/* ══ WIDE VIEWPORTS ══════════════════════════════════════════════════════
   Everything below 1600px is untouched BY CONSTRUCTION: every rule in this
   file from here down lives inside the one media query, and a test asserts
   that no copy of any of it escaped the block. tokens.css carries the
   reasoning for the breakpoint and for the 1800px ceiling `--main-max`
   takes here; this block is the rest of the system.
   ════════════════════════════════════════════════════════════════════════ */



/* ── duo_card — the two-team KPI card (LF-6) ──────────────────────────
   `components/match_family.html`. It is a `.kpi` with two values in it
   rather than a card of its own: the frame, the head, the label, the
   tooltip trigger and the refusal states are all the KPI card's, so a
   change to the stat card's chrome reaches this one too instead of
   leaving two cards that used to look alike.

   TWO SIGNALS, TWO FORMS, on purpose. COLOUR means "this side leads on
   this measure" and it is the same accent on both match pages, because
   leading is a fact about the pair and not about whose page it is. The
   3px RULE before a column's name means "this column is ours", and it
   only exists on the team page — the same mark, the same width and the
   same token `.board-rule-ours` already puts on our own scoreboard. Had
   both been colour, the neutral page would have had to invent a third
   one or drop a real distinction. */

.duo-grid {
  /* Wider floor than `.kpi-grid`: this card carries two values, two
     captions and two sample lines where that one carries one of each.
     Measured rather than guessed — at the shared 208px floor the 29px
     leading value and the 17px trailing one collide on the longest real
     pair on the corpus ("100.0%" against "100.0%"). */
  grid-template-columns: repeat(auto-fill, minmax(min(248px, 100%), 1fr));
}

/* A GRID, not a flex row, and the reason is measured: the sample lines are
   long ("18,341 total damage") and a flex item sizes to its content, so the
   trailing column was pushed onto its own line on every card whose sample ran
   past the card's half — five of them at 1440 in the first browser pass. Two
   `minmax(0, 1fr)` tracks make each side exactly half the card and let the
   sample wrap INSIDE its own column instead of widening it. */
.duo-vals {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: end;
  gap: var(--sp-1) var(--sp-2-5);
  min-width: 0;
}

.duo-side {
  min-width: 0;
}

.duo-side-b {
  text-align: right;
}

.duo-cap {
  font-size: var(--fs-micro-2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "This column is ours" — form, not colour. See the note above. */
.duo-side-ours .duo-cap::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 9px;
  margin-right: 5px;
  border-radius: 2px;
  background: var(--color-accent);
  vertical-align: -1px;
}

.duo-val {
  font: 500 var(--fs-kpi) / 1.05 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.025em;
}

.duo-val-b {
  font-size: var(--fs-lg);
  line-height: 1.15;
  color: var(--dim);
}

/* The pair's leader. Never applied when either side has no sample: an
   absent value cannot lead, and the em dash must not be able to glow. */
.duo-val-lead {
  color: var(--color-accent);
}

.duo-sub {
  font-family: var(--mono);
  font-size: var(--fs-label-2);
  color: var(--dim);
  overflow-wrap: anywhere;
}

/* The head WRAPS ITS CHILDREN on this card where `.kpi`'s does not, and the
   reason is the edge chip: it carries a TEAM NAME ("edge Sashi Academy"), which
   is as long as the league makes it, so it needs its own line rather than
   squeezing the label beside it.
   The label's own wrap moved to `.kpi-label` at R7b — the same clipping this
   card fixed locally ("SAVED AFTER CONCEDI…") turned out to be happening on the
   plain cards too, so the rule went where it belongs and this override is down
   to the one thing that is genuinely this card's. */
.duo .kpi-head {
  flex-wrap: wrap;
}

.duo-edge {
  margin-left: auto;
  font: 600 var(--fs-xs) / 1.3 var(--mono);
  color: var(--dim);
  white-space: nowrap;
}

/* The bar is the pair's own SPLIT, not a scale to 100 — the reference's
   own note, and the reason a pair with an absent side draws none at all
   (the template omits it rather than styling it away). */
.duo-bar {
  display: flex;
  height: 4px;
  border-radius: 2px;
  overflow: hidden;
  background: var(--track);
}

.duo-fill {
  height: 100%;
  width: var(--bar);
}

.duo-fill-a {
  background: var(--color-accent);
}

.duo-fill-b {
  background: var(--color-neutral-700);
  margin-left: 2px;
}

.duo-note {
  font-size: var(--fs-xs);
  color: var(--dim);
  margin: 0;
  max-width: 60ch;
}

/* No sample on either side: the em dash stays legible for the same
   reason `.kpi-value-empty` does. "Nothing met this definition in this
   match" is an answer and the operator has to be able to read it. */
.duo--zero .duo-val,
.duo--zero .duo-val-b {
  color: var(--dim);
}

/* Under 420px a card is the whole column and half of it is ~150px, which
   is four monospace characters short of the longest real sample. The two
   sides stack instead, keeping the sample on one line where the number
   it belongs to can still be read beside it. */
@media (max-width: 420px) {
  .duo-vals {
    grid-template-columns: minmax(0, 1fr);
  }

  .duo-side-b {
    text-align: left;
  }
}

/* ══ R2 · CONNECTIVE TISSUE ═══════════════════════════════════════════════
   Two things, and both exist because the 2026-08-12 review's P0-4 was that
   this product's surfaces do not link to each other: a ROW that is a link,
   and the line that says what a link carried.

   The rule the row obeys is the one the Maps chain already proved (`a.mp-card`
   / `a.mp-game-row` in map-pool.css): the row keeps every pixel of its
   layout and gains a hover tint, and the row's own NAME takes the accent the
   way `.map-name` does. No chevron, no underline, no second column of
   affordance — a design whose rows are already dense cannot afford chrome
   that says "this is clickable" next to chrome that says what the row is.

   The tint bleeds into the panel's padding through a symmetric negative
   margin so the highlight reads as the full-width row it is; without it the
   tint stops at the text and looks like a selection rather than a target. */

/* `.ov-result-open` and not `a.ov-result`: the result row stopped being an
   anchor when it grew a second destination (the 3D chip). Everything below is
   the rule it had as an anchor, unchanged, keyed on the class the template puts
   there under exactly the same condition — a row with an `href`. The roster row
   is still an anchor and keeps its own selector. */
.ov-result-open,
a.ov-player {
  color: inherit;
  text-decoration: none;
  margin-inline: calc(-1 * var(--sp-3));
  padding-inline: var(--sp-3);
  border-radius: var(--radius-sm);
  transition: background var(--t-fast) var(--ease);
  /* The ground the row's own overlay is positioned against. */
  position: relative;
}

.ov-result-open:hover,
a.ov-player:hover {
  background: var(--hover-tint);
}

.ov-result-open:hover .ov-result-name,
a.ov-player:hover .ov-player-name {
  color: var(--color-accent);
}

/* THE ROW-WIDE TARGET, as an overlay on the row's own name.
   The anchor is real and carries the href, so the keyboard reaches it, a screen
   reader announces the opponent as the link text, and the status bar shows the
   destination. The `::after` is what makes the rest of the row clickable — the
   whole box, including the padding the negative margin bleeds into. */
.ov-result-link {
  color: inherit;
  text-decoration: none;
}

.ov-result-link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* Above the overlay, or the row would swallow it. This is the whole mechanism
   that lets one row hold two destinations. */
.ov-result > .v3-chip {
  position: relative;
  z-index: 1;
}

/* ══ THE RESULT ROW'S PHONE SHAPE (2026-08-23) ═══════════════════════════════
   The reported defect: at 390 a long opponent name pushes the 3D door onto a
   line of its own, so the door lands in a different place on that row than on
   the four above it and the row grows to make space. Measured on the operator's
   corpus — door 13px from the row's top on three rows and 49px on the fourth,
   row height 101px against 94/95.

   IT IS A CONTAINER QUERY AND NOT A VIEWPORT ONE, because the viewport does not
   tell the truth about this row's width. The Overview's top block is two columns
   at the desk and one below 900, so this panel's inline size is 567px inside a
   900px viewport and 743px inside an 820px one — WIDER on the narrower screen. A
   viewport breakpoint would restructure the row at widths where it fits and
   leave it broken at widths where it does not. `.ov-recent`'s own inline size is
   the fact that decides it, and `container-type` is already this product's
   vocabulary for exactly that argument (`zone-view.css`, the `zoneview`
   container).

   370px is the phone tier of THIS container, measured: the panel's inline size
   is 403px at a 480px viewport and 313px at 390. The break the operator saw sits
   inside that band, and the threshold is deliberately set at its top rather than
   at the exact width this corpus happens to fail on — the wrap is a function of
   the OPPONENT NAME's length, so a corpus with longer names would break wider.
   The grid does not depend on the name at all, which is the property being
   bought; below this the door's place is a fact about the layout instead of a
   fact about the data. */
.ov-recent {
  container-type: inline-size;
  container-name: ovrecent;
}

@container ovrecent (max-width: 370px) {
  /* Flex `wrap` puts the door wherever the line happens to break, which is the
     whole defect. A grid gives it a NAMED place that no name length can move:
     the result flag leads both lines, the opponent and the date share the first,
     and the match's facts share the second with the way into them. */
  .ov-result {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "flag name date"
      "flag meta door";
    column-gap: var(--sp-2);
    row-gap: var(--sp-1);
    align-items: center;
  }

  .ov-result > .ov-chip { grid-area: flag; }
  .ov-result-name { grid-area: name; }
  .ov-result-meta { grid-area: meta; }

  .ov-result-date {
    grid-area: date;
    /* `margin-left: auto` is how the flex row held the right end; in a grid cell
       it is a stray indent. `justify-self` is the grid's own spelling of it. */
    margin-left: 0;
    justify-self: end;
  }

  /* A row whose match has no payload simply leaves this cell empty, and every
     other row keeps its door in the same place regardless — which is the
     property the flex layout could not hold. */
  .ov-result > .v3-chip {
    grid-area: door;
    justify-self: end;
  }
}

/* The row rule is drawn by the shared `.ov-result` / `.ov-player` selectors
   above these, so a linked LAST row would keep its border where an unlinked
   one drops it. Restated at the same specificity as the anchor form. */
.ov-result-open:last-child,
a.ov-player:last-child {
  border-bottom: 0;
}

/* 44px of tappable height at phone widths (the touch-target floor). At the
   desk the rows are pointer targets and the design's own 10px rhythm holds;
   this only ever grows them where a finger is the input. Measured: the
   result row's ink is ~17px, so 13px a side clears 44 with the border.

   The rest of R2's links reach the same floor at the END of this file (search
   "R3 · TOUCH-TARGET FLOOR"), and they are down there rather than here for a
   mechanical reason: `.xlink` and `.map-name` are DEFINED below this point,
   and a media query carries no extra specificity — a later base rule would
   simply win. */
@media (max-width: 900px) {
  .ov-result-open {
    padding-block: 13px;
  }

  a.ov-player {
    padding-block: 13px;
  }
}

/* The link and the sentence about it, as ONE flex item.
   `inline-flex` column is what makes this work across seven head rows with
   four different class names (`.page-head-row`, `.fp-head-row`,
   `.ut-head-row`, `.ts-head-row`): each of them is a flex row, so the group
   lands wherever the button landed and the caption hangs under it,
   right-aligned to it. The first draft put the note after the row instead and
   it read as a page subtitle — 500px from the control it describes, and above
   the page's actual sub-line. */
.head-link {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-1);
  min-width: 0;
}

/* The group holds the END of the row — but only when it IS the end. Team
   stats' head row carries a Share panel after this button, and an `auto`
   margin absorbs every pixel of free space before the group, which pushed
   Share onto a second line the moment the caption widened the group.
   `:last-child` is the whole condition: right-align where there is a right
   edge to hold, sit where you were put where there is not. */
.head-link:last-child {
  margin-left: auto;
}

/* The carry note (components/carry_note.html). A caption, not a paragraph:
   it sits under a control and states a fact about where that control goes.
   `--dim` and not `--dim-2` because the second half is a real disclosure the
   operator has to be able to READ — `--dim-2` is interface chrome contrast
   (see tokens.css) and this sentence is the difference between a filtered
   page and an unfiltered one.
   A BLOCK with inline spans rather than a flex row: the two halves are one
   sentence and have to wrap as one. As flex items they broke after "Carries
   map" at every width, which turned a caption into a two-line stack. */
.carry-note {
  display: block;
  margin: 0;
  font-size: var(--fs-xs-2);
  line-height: 1.5;
  color: var(--dim);
  max-width: 62ch;
}

/* MEASURED, not chosen. The caption is a flex item inside its head row, so
   its width is the row's to pay for: at 62ch the group ran 450px and pushed
   Team stats' Share panel onto a second line (that row is 627px wide and
   already carries a 169px title). 40ch keeps the group at ~290px, which the
   narrowest of the seven rows fits with room, and the sentence is short
   enough that three lines of it is still a caption rather than a paragraph. */
.head-link .carry-note {
  text-align: right;
  max-width: 40ch;
}

.carry-kept {
  color: var(--color-accent-300);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.carry-sep {
  color: var(--dim-2);
  padding: 0 var(--sp-05);
}

/* The one head row with no right edge to hold. The solo skill pages compose
   their header as two columns — a 64ch intro and a score block hard against
   the viewport — so `margin-left: auto` puts the button at the intro column's
   edge, which is the MIDDLE of a 1440 screen and reads as a control floating
   in open space rather than as the header's action. Here it stays beside the
   title it belongs to, and its caption reads left-to-right like the blurb
   directly under it. */
.sl-cat-intro .head-link:last-child {
  margin-left: 0;
  align-items: flex-start;
}

.sl-cat-intro .head-link .carry-note {
  text-align: left;
}

/* Below the head rows' own wrap point the group is the full column, so the
   caption reads left-to-right with everything else on the page rather than
   ragged-left against nothing. */
@media (max-width: 900px) {
  .head-link {
    align-items: flex-start;
    width: 100%;
  }

  .head-link .carry-note {
    text-align: left;
  }
}

/* Cross-links out of a map's detail into the pages that explain it. The row
   of them is a list of destinations, so it wraps rather than scrolls: three
   short labels on one line at the desk, one per line at 390 where each is
   then a full-width tap target. */
.xlinks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-2-5);
}

.xlink {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1-5);
  min-height: 32px;
  padding: var(--sp-1-5) var(--sp-3);
  border-radius: var(--radius-sm);
  background: var(--panel-3);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--color-text);
  font-size: var(--fs-sm);
  text-decoration: none;
  transition: background var(--t-fast) var(--ease);
}

.xlink:hover {
  background: var(--hover-tint-2);
  box-shadow: inset 0 0 0 1px var(--line-2);
}

.xlink-map {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--color-accent-300);
}

@media (max-width: 520px) {
  .xlinks {
    flex-direction: column;
  }

  .xlink {
    /* The height moved to the R3 touch-target block below, which owns the
       floor for every phone width. What is left here is the layout change
       this breakpoint is actually about: a full-width chip with its scope
       pushed to the far edge. */
    justify-content: space-between;
  }
}

/* ══ R3 · TOUCH-TARGET FLOOR ══════════════════════════════════════════════
   The other half of the 44px floor documented above `a.ov-result`. R2 added
   five kinds of link across the product and gave one of them a phone height;
   its own audit flagged the other four, and this is that follow-up.

   It lives HERE, four thousand lines down, because `.xlink` and `.map-name`
   are defined below the R2 block and a media query adds no specificity — a
   rule up there would be overwritten by the base rule down here. Anything
   added to this floor belongs in this block for the same reason.

   Three shapes, three treatments:
   * `.xlink` (map detail → the page that explains a map) already carries a
     min-height and only needs the real one. The 520px block above it says 40;
     this raises the floor for every phone width and that rule's 40 is gone
     with it, because 40 was never the number.
   * "Scout <them>" and "My games" are `.btn-sm`, whose padding is part of the
     design's rhythm everywhere else in the product. They get a min-height and
     `inline-flex` — which is what makes a min-height mean anything on an
     inline element — rather than padding, so their type and their density are
     untouched at the desk.
   * My Maps' map name is a bare text link with no box at all. Same treatment;
     `align-items: center` keeps the word on the row's own baseline. */
@media (max-width: 900px) {
  .xlink {
    min-height: 44px;
  }

  a.btn[data-testid="scout-opponent"],
  a.btn[data-testid="skill-my-games"] {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  a.map-name {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   W2-ζ — THE MOBILE NAV DRAWER (lane byte/w2-mobile-nav). SELF-CONTAINED.

   Everything this lane added to this sheet is between this banner and the one
   that closes it. Nothing above was edited: the block sits at the end (ahead
   of the wide block, which `tests/test_wide_layout.py` requires to stay last)
   and wins on source order where it needs to, which is what lets a parallel
   lane append its own block without the two having to be merged line by line.

   THE DEFECT, MEASURED. Below 900px `.side-nav` is a horizontal scroller
   sharing the bar with the brand, the mode toggle and the account chip. At
   390px that strip is 198px wide and holds 1,317px of links: three of the team
   rail's THIRTEEN destinations are on screen, the third clipped mid-word, and
   the other ten are behind roughly six swipes of a scroller with no chevron
   and no fade. On every page in the product. `nav.js` already scrolls the
   strip to the entry you are on, which is everything scrolling can do; it
   cannot make a 198px window show thirteen destinations.

   THE TRADE, AND WHERE IT INVERTS. The note this sheet has carried since the
   strip was built is right at the widths it was written for: "a menu that
   hides four links behind a tap is a worse answer than showing them." Four,
   not thirteen. Between 720 and 900px four to five entries do fit and the
   strip is left exactly as it is. At 720 and below it becomes a drawer.

   WHY 720 AND NOT 900. It is the audited width and the operator's ruling for
   this lane, and it keeps the change to the viewports where the strip is
   provably useless. The band from 721 to 899 is untouched by construction:
   every rule below is inside `@media (max-width: 720px)`.

   THE MECHANISM. A native `<details>` in the bar publishes the open state and
   the stylesheet reads it across the SIBLING axis — `.navdraw[open] ~
   .side-nav`. So the drawer IS the rail: the same element, the same thirteen
   entries in the same order, the same `aria-current`, the same badges, the
   same section captions, moved off-canvas and given the desk's own vertical
   treatment. No second copy of the nav in the document, which is the failure
   mode a duplicated mobile menu ships with — two elements carrying one set of
   `data-testid="nav-*"` hooks and two answers to "which one is current".
   ══════════════════════════════════════════════════════════════════════════ */

/* The control does not exist above 720px. Stated here, once, unconditionally:
   the desk rail must not be able to grow a hamburger because somebody adds a
   width query later. */
.navdraw {
  display: none;
}

@media (max-width: 720px) {
  .navdraw {
    display: block;
    flex: none;
    /* THE ANTI-SHIFT. When the drawer opens, the button below leaves the flow
       (it becomes the panel's close control), and without this the bar would
       lose its tallest child and shrink ~10px — moving the whole page behind
       the scrim on every open. The element the button came out of holds the
       height instead, so `.main` does not move by a pixel. */
    min-height: 44px;
  }

  /* The button. `<summary>`'s own marker goes rather than being restyled: the
     state is drawn by the two glyphs inside it, and a UA triangle beside them
     would be the same fact told twice in two hands. */
  .navdraw-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-height: 44px;
    padding: 0 var(--sp-2);
    border-radius: var(--radius-md);
    color: var(--dim);
    font: 500 var(--fs-md) var(--font-body);
    white-space: nowrap;
    cursor: pointer;
    list-style: none;
    user-select: none;
    transition: color var(--t-fast) ease;
  }

  .navdraw-btn::-webkit-details-marker {
    display: none;
  }

  .navdraw-btn:hover {
    background: var(--hover-tint);
    color: var(--color-text);
  }

  /* One control, one NAME. The word stays "Menu" in both states and only the
     mark changes, because `<summary>` already reports expanded/collapsed to a
     screen reader — a button that renamed itself to "Close" on the second
     press would be announcing a different control each time. */
  .navdraw-cross {
    display: none;
  }

  .navdraw[open] .navdraw-bars {
    display: none;
  }

  .navdraw[open] .navdraw-cross {
    display: block;
  }

  /* Open, the button is the panel's close control — fixed at the coordinates
     it already occupied in the bar (the bar's own 8px padding on both axes,
     with a 44px button in a 60px row), so the mark morphs in place instead of
     jumping. Fixed and not absolute because the bar scrolls with the document
     and a close control that scrolls out of reach is a trap for the reader
     without JavaScript, who has no scrim tap and no Escape. */
  .navdraw[open] > .navdraw-btn {
    position: fixed;
    top: var(--sp-2);
    left: var(--sp-2);
    z-index: 72;
    color: var(--color-text);
  }

  /* The scrim exists only while the drawer is open, which is what `<details>`
     already says about its own content — so there is no permanently-present
     overlay to keep `pointer-events: none` honest about.

     `touch-action: none` is the half of "do not scroll the page behind" that
     needs no script: a fixed overlay does not stop a touch drag from scrolling
     the document under it, and this does. The other half (a wheel over the
     scrim, and the document's own scrollbar) is the lock below. */
  .navdraw-scrim {
    position: fixed;
    inset: 0;
    z-index: 70;
    background: color-mix(in srgb, var(--color-bg) 82%, transparent);
    touch-action: none;
  }

  /* THE BAR RECOMPOSES, because the strip was what used to fill it. With the
     thirteen destinations gone the row was a button, a mark, a toggle and then
     198px of nothing before the account chip. The free space moves in front of
     the toggle instead of behind it, which puts the two controls that answer
     "who am I looking as" beside each other on the right and leaves the two
     that answer "where can I go" together on the left. The chip's own auto
     margin is released so the pair stays a pair — two auto margins would split
     the space and strand the toggle in the middle again. */
  .side-mode {
    margin-left: auto;
  }

  .side-foot {
    margin-left: 0;
  }

  /* THE PANEL — `.side-nav` itself, off-canvas.

     `visibility: hidden` and not merely translated: an off-screen nav is still
     in the tab order, and thirteen invisible links between the button and the
     page content is a keyboard trap on every mobile page. The visibility
     transition is delayed by the slide's own duration on the way out and
     instant on the way in, so the panel is reachable the moment it starts
     arriving and unreachable the moment it has finished leaving.

     `overflow-x` is deliberately NOT declared. It is `auto` from the rule at
     the top of this sheet and stays `auto`; in a column with full-width rows
     there is nothing to scroll sideways, `nav.js` reads a slack of 0 and
     returns, and the sub-900 invariant `tests/test_shell_layout.py` pins goes
     on being true. */
  .side-nav {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 71;
    width: min(82vw, 320px);
    flex-direction: column;
    gap: var(--sp-05);
    padding: 60px var(--sp-1-5) var(--sp-5);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: linear-gradient(180deg, var(--panel-2), var(--color-bg));
    border-right: 1px solid var(--line);
    visibility: hidden;
    transform: translateX(-101%);
    transition:
      transform var(--t-reveal) cubic-bezier(0.16, 1, 0.3, 1),
      visibility 0s linear var(--t-reveal);
  }

  .navdraw[open] ~ .side-nav {
    visibility: visible;
    transform: none;
    transition:
      transform var(--t-reveal) cubic-bezier(0.16, 1, 0.3, 1),
      visibility 0s;
  }

  /* Inside the panel the rail is the DESK's rail, because that is the layout
     the entries were designed in and the one the reader already knows from
     the other half of their week. */

  /* The captions come back. They are `display: none` below 900 because a
     heading over a horizontal scroller is in the way; in a column they are the
     grouping — TEAM · CRASHOUT, STAT CATEGORIES, OPPONENTS — and without them
     the drawer is thirteen undifferentiated rows. Scoped through `.side-nav`
     so the strip between 721 and 899px keeps hiding its own. */
  .side-nav .side-cap {
    display: block;
  }

  /* ...including the workspace line, which the sub-900 block clips to 1px to
     keep it in the accessibility tree while the bar has no room for it. The
     panel has room, so it is drawn: these declarations put back exactly what
     `.visually-hidden` took, one for one. */
  .side-nav .side-cap-workspace {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    padding: var(--sp-2) var(--sp-2-5) 5px;
    overflow: visible;
    clip-path: none;
  }

  /* The entries get their padding back (the strip's budget cut it to 6px to
     buy horizontal room that a column does not need) and a 44px target. */
  .side-nav .navbtn {
    width: 100%;
    min-height: 44px;
    padding-left: var(--sp-2-5);
    padding-right: var(--sp-2-5);
  }

  /* The you-are-here mark turns with the axis, exactly as it does at 900px: a
     bar under a row in a column points at nothing. Wash plus inset left bar
     for the page you are ON; the bar alone, no wash, for the SECTION a
     off-rail page sits under — the same two truths ARIA tells apart with
     `page` and `true`. */
  .side-nav .navbtn[aria-current="page"] {
    background: linear-gradient(90deg, var(--accent-wash-4), transparent);
    box-shadow: inset 2px 0 0 var(--color-accent);
  }

  .side-nav .navbtn[aria-current="true"] {
    background: transparent;
    box-shadow: inset 2px 0 0 var(--accent-ring);
  }

  /* The badge goes back to the right edge. It sits beside its label in the
     strip because `margin-left: auto` inside a nowrap tab would push it off
     the label; in a full-width row it is a column of numbers again. */
  .side-nav .navbadge {
    margin-left: auto;
  }

  /* THE SCROLL LOCK, and the reason it is not `overflow: hidden` alone.
     Taking the document's scrollbar away widens the viewport by its width, and
     the page behind the scrim jumps sideways on every open. `nav.js` measures
     that width before locking and publishes it here; on a phone it is 0 and
     this costs nothing. Scoped inside the query so a class left behind by a
     resize to desktop can never lock a desk. */
  html.navdraw-locked,
  html.navdraw-locked body {
    overflow: hidden;
  }

  html.navdraw-locked body {
    padding-right: var(--navdraw-gutter, 0);
  }
}

/* The slide is the drawer's one authored moment; a reader who has asked for
   less motion still gets the drawer, instantly. Both halves are named, or the
   panel would animate in and appear to teleport out. */
@media (max-width: 720px) and (prefers-reduced-motion: reduce) {
  .side-nav,
  .navdraw[open] ~ .side-nav {
    transition: none;
  }
}

/* ═════════════════ end W2-ζ — THE MOBILE NAV DRAWER ═══════════════════════ */

/* ═════════════════════════════════════════════════════════════════════════
   W2-α — the shared stat rail, the filter bar's active row, the zero-round
   panel, and the unrecognised-family strip.

   ONE BLOCK, appended, and it is appended rather than merged into the filter
   and rail sections above because a second lane is appending to this file the
   same night. Nothing here edits an existing rule.

   It has to sit ABOVE the wide block: that one is required to be the last
   thing in the sheet (tests/test_wide_layout.py) and nothing may follow it.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── .stat-rail — the family picker's skin, in the shared sheet ────────────
   The engineering audit measured this rail copy-pasted five times across the
   page sheets — `.fp-`, `.ot-`, `.ts-`, `.ut-` and the match page's `.gp-` —
   19.2KB of one control with four spellings of its own current mark. R7b had
   already made the four AGREE; agreeing copies are still copies, and the next
   change to the skin is four edits and a screenshot each.

   This is the Players skin, unchanged in every measurement, under a name no
   page owns. Openings & trading draws it (`prefix='stat'`). The other four
   sheets keep theirs until their own lanes move them, so this file gains a
   vocabulary and takes nothing away from anybody. */

.stat-rail {
  position: sticky;
  top: var(--sp-5);
  max-height: calc(100vh - 40px);
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-3) 11px var(--sp-4);
  border-radius: var(--radius-md);
  background: linear-gradient(160deg, var(--panel-2), var(--panel) 62%);
  box-shadow: inset 0 0 0 1px var(--line);
  align-self: start;
  min-width: 0;
}

.stat-rail-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0 var(--sp-05) 11px;
  border-bottom: 1px solid var(--line);
}

.stat-rail-dot {
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 3px var(--accent-wash-4);
}

.stat-rail-cap {
  font-size: var(--fs-label);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
}

.stat-rail-hint {
  margin-left: auto;
  font: 400 var(--fs-micro) var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim-2);
}

.stat-rail-list {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.stat-railbtn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-05);
  padding: var(--sp-2) 11px;
  border-left: 2px solid var(--line);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--dim);
  text-decoration: none;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}

.stat-railbtn:hover,
.stat-railbtn:focus-visible {
  background: var(--hover-tint);
  color: var(--color-text);
}

/* THE CURRENT MARK, and it is ONE SELECTOR now. The `:target` rails need
   fourteen — two per family, spelled out, because CSS cannot compare one
   element's attribute to another's — and openings.css carried exactly that,
   twice, once per breakpoint. A rail whose entries are real URLs has the
   server's own answer sitting on the element, so the mark is the attribute. */
.stat-railbtn[aria-current] {
  color: var(--color-text);
  border-left-color: var(--color-accent);
  border-left-width: 3px;
  padding-left: 10px;
  background: linear-gradient(90deg, var(--accent-wash-4), transparent 78%);
  font-weight: 600;
}

.stat-railbtn-title {
  font: 500 var(--fs-md) / 1.25 var(--font-body);
  white-space: nowrap;
}

.stat-railbtn-tag,
.stat-railbtn-note {
  font: 400 var(--fs-micro-2) / 1.4 var(--mono);
  color: var(--dim-2);
}

/* A `note` is a phrase and wraps; a `tag` is three words and does not. */
.stat-railbtn-note {
  overflow-wrap: anywhere;
}

.stat-railbtn-tag {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.stat-rail-note {
  margin: auto 0 0;
  padding-top: 11px;
  border-top: 1px solid var(--line);
  font: 400 var(--fs-micro) / 1.5 var(--mono);
  color: var(--dim-2);
  text-wrap: pretty;
}

/* At 1120 the rail becomes a strip above the body: the panel stays, the list
   turns into a row of ring chips, and the current mark is the ring. A hidden
   picker is a set of families nobody can reach. */
@media (max-width: 1120px) {
  .stat-rail {
    position: static;
    max-height: none;
    order: -1;
  }

  .stat-rail-list {
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--sp-1);
  }

  .stat-railbtn {
    border-left: 0;
    border-radius: var(--radius-sm);
    box-shadow: inset 0 0 0 1px var(--line);
    padding: var(--sp-1-5) var(--sp-2);
    /* The touch floor, on the one control that IS the page's navigation. */
    min-height: 44px;
    justify-content: center;
  }

  .stat-railbtn[aria-current] {
    border-left: 0;
    padding-left: var(--sp-2);
    box-shadow: inset 0 0 0 1px var(--color-accent);
  }

  .stat-railbtn-note {
    display: none;
  }

  .stat-rail-note {
    margin-top: var(--sp-2);
  }
}

/* ── the filter bar's ACTIVE row ──────────────────────────────────────────
   `.filter-chip` and `.filter-chip-x` are already in this sheet, above, and
   had never been rendered by anything: the chip was designed, styled and left
   without markup. This is the rest of the row.

   THE COLOUR LANGUAGE, stated once. A chip in this row is a narrowing the
   READER applied, so it is solid: accent wash, accent ring, accent text. The
   controls above it draw the same accent as a FILL for a picked value and
   leave the default value (All, Both) as a bare outline, so "solid means you
   chose this" reads the same in both halves of the bar. */

.filters-active-cap {
  font: 400 var(--fs-micro) var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim-2);
  margin-left: var(--sp-2);
}

.filters-clear {
  font: 500 var(--fs-xs-2) var(--mono);
  color: var(--color-accent-300);
  text-decoration: none;
  border-bottom: 1px solid var(--accent-ring);
  line-height: 1.2;
  padding-bottom: 1px;
}

.filters-clear:hover,
.filters-clear:focus-visible {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}

/* The default chip stays an OUTLINE while it is on. Without this the two
   halves of the bar contradict each other: "All maps" lit exactly as brightly
   as a map the reader picked says the reader picked "All maps". */
.chipbtn[data-default][aria-pressed="true"] {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--dim);
}

/* Anything the page wants to say about its own selection — what a link out of
   here cannot carry, most of all. Under the controls, above the body. */
.filters-notes {
  border-top: 1px solid var(--line);
  padding-top: var(--sp-2);
}

/* The refusal already drew the rule that separates these lines from the
   controls. Two of them stacked is a divider dividing nothing. */
.filters-refusal + .filters-notes {
  border-top: 0;
  padding-top: var(--sp-1);
}

.filters-notes .carry-note {
  margin: 0;
}

/* ── the zero-round panel ─────────────────────────────────────────────────
   The page's body, so it is not a card: a box drawn around the whole body is
   a container around nothing. A rule and generous space do the work. */

.empty-selection {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2-5);
  padding: var(--sp-6) 0 var(--sp-6);
  max-width: 68ch;
}

.empty-title {
  margin: 0;
  font: 600 var(--fs-xl) / 1.2 var(--font-body);
  letter-spacing: -0.01em;
  color: var(--color-text);
}

.empty-lede {
  margin: 0;
  font: 400 var(--fs-md) / 1.6 var(--font-body);
  color: var(--dim);
  text-wrap: pretty;
}

/* The selection the panel is about, for a page whose narrowing is not a chip
   (Maps' date form). Sits directly under the lede and above the fact, so the
   reader reads: what is empty, why, WHICH WINDOW, and what exists instead. */
.empty-scope {
  margin: calc(var(--sp-1) * -1) 0 0;
  font: 400 var(--fs-sm) / 1.5 var(--mono);
  color: var(--dim-2);
}

.empty-newest {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-2-5);
  padding: var(--sp-2-5) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.empty-newest-cap {
  font: 400 var(--fs-micro) var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim-2);
}

.empty-newest-value {
  font: 500 var(--fs-md) var(--mono);
  color: var(--color-text);
}

.empty-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
}

/* One click per narrowing, which is one step of widening each. Written as
   links rather than as buttons because that is what they are. */
.empty-drop {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: var(--sp-1) var(--sp-2-5);
  border-radius: calc(var(--radius-md) * 0.75);
  box-shadow: inset 0 0 0 1px var(--line);
  font: 500 var(--fs-xs-2) var(--mono);
  color: var(--dim);
  text-decoration: none;
}

.empty-drop:hover,
.empty-drop:focus-visible {
  color: var(--color-text);
  box-shadow: inset 0 0 0 1px var(--accent-ring);
}

/* ── the unrecognised-family strip ────────────────────────────────────────
   A statement, not an error: nothing failed, a word was not in the list. So
   it is the page's own line rules and dim type rather than the loss hue. */

.ot-unknown {
  margin: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2-5);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 1px var(--line);
  font: 400 var(--fs-sm) / 1.5 var(--font-body);
  color: var(--dim);
}

.ot-unknown b {
  color: var(--color-text);
  font-weight: 600;
}

.ot-unknown-text {
  flex: 1 1 auto;
  min-width: 0;
  text-wrap: pretty;
}

.ot-unknown-x {
  flex: none;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  color: var(--dim-2);
  text-decoration: none;
  font: 600 var(--fs-md) var(--mono);
  line-height: 1;
}

.ot-unknown-x:hover,
.ot-unknown-x:focus-visible {
  background: var(--hover-tint);
  color: var(--color-text);
}

@media (max-width: 560px) {
  .empty-selection {
    padding: var(--sp-4) 0 var(--sp-5);
  }

  .empty-actions .btn,
  .empty-drop {
    min-height: 44px;
  }

  .ot-unknown-x {
    width: 44px;
    height: 44px;
  }
}

/* ======================================================================
   W2-B — wave-2 lane beta. APPEND-ONLY BLOCK: nothing above this line was
   edited, and everything this lane added to the shared sheet is inside it.

   It sits BEFORE the sheet's wide block (min-width 1600, immediately below)
   rather than at the end of the file, because that block must remain the last
   thing in the sheet —
   `tests/test_wide_layout.py::test_each_sheet_opens_exactly_one_wide_block`
   asserts it, for the reason written there: two wide blocks let a later one
   shadow the first with nothing on screen saying so. That test counts the
   query's own text, so this note must not spell it either.
   ====================================================================== */

/* ---- Overview identity header (W2-B) ---------------------------------
   The page's own masthead. It exists because this page did not have one: the
   `h1` lived inside a panel in the right-hand column, so at 390px the team's
   name and record rendered at y=959, under eight hundred pixels of roster.

   Not a `.panel`: a page header is not a module, and giving it a frame would
   have made the first thing on the page look like the first CARD on the page.
   The hairline under it is the same `--row-rule` every table in the product
   separates rows with. */
.ov-identity {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--row-rule);
}

.ov-identity-who { min-width: 0; }

/* The last ten, on the identity line. `flex-wrap` because the strip is ten
   24px chips plus two labels and that is wider than 390px: it wraps to a
   second line there rather than pushing the document sideways. */
.ov-identity-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
}

/* ---- Section head, outside the map pool (W2-B) -----------------------
   `.mp-section-head` is exactly this lockup and it lives in map-pool.css,
   which the Overview does not link and must not start linking for two rules.
   Same shape, sheet-appropriate name. */
.ov-sechead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  margin-bottom: var(--sp-3);
}

.ov-sechead-title {
  font-size: var(--fs-lg);
  font-weight: 600;
  margin: 0;
}

.ov-sechead-tag {
  font-size: var(--fs-sm);
  color: var(--dim);
}

.ov-band-wrap { min-width: 0; }

/* The headline number and its edge chip on one baseline. The chip was the one
   part of the deep pages' card grammar that had nowhere to sit on this band:
   `.ov-kpi` draws its value at 30px and a chip stacked under it reads as a
   second figure rather than as a comparison of the first. */
.ov-kpi-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

/* ---- Next up, action-led (W2-B) -------------------------------------- */
.ov-next-lead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-3);
}

/* The sentence beside the button. `min-width: 0` + `flex: 1 1 16rem` is what
   lets the button keep its own width and the sentence take the rest, and drop
   to its own line rather than squeezing the button off the panel at 390. */
.ov-next-say {
  flex: 1 1 16rem;
  min-width: 0;
  margin: 0;
  font-size: var(--fs-base);
}

/* ---- Roster bar tick (W2-B) ------------------------------------------
   The roster's own median, on the fixed rating scale each row's fill is drawn
   to. Same treatment as `.kpi-bar-tick` on the headline cards — one mark, one
   pixel of ink, on the track it belongs to — expressed on `.meter`, which is
   the row-scale component.

   `.meter` sets `overflow: hidden` and the tick is inside it, so a median at
   0% or 100% is clipped to a half-tick at the end of the track rather than
   painting outside it. That is correct: the mark is on the scale. */
.meter { position: relative; }

.meter-tick {
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: var(--tick);
  width: 2px;
  margin-left: -1px;
  background: var(--color-text);
  opacity: 0.55;
  border-radius: 1px;
}

.ov-roster-scale { margin: 0 0 var(--sp-2); }

/* ---- The empty-window panel (W2-B, retired W2-final) ------------------
   `.ew-panel`, `.ew-scope`, `.ew-found` and `.ew-doors` are GONE with the
   page-local component they styled. Maps' empty window is the shared
   `.empty-selection` panel now — one shape for the one thing both states say —
   and its window-specific line is `.empty-scope`, up beside the rest of that
   block rather than four hundred rules away from it. */

/* ---- Recent results, by series (W2-B) --------------------------------
   A series is one meeting and the maps under it are how it went. The header
   row carries the series verdict; the map rows are indented under it and drop
   the opponent's name, which the header above them has already said. */
.ov-series {
  padding: var(--sp-2-5) 0;
  border-bottom: 1px solid var(--row-rule);
}
.ov-series:last-child { border-bottom: 0; }

.ov-series-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}

/* `margin-left: auto` on the date keeps it at the right end of whatever line
   it lands on, which is the rule `.ov-result-date` already follows. */
.ov-series-score {
  font-family: var(--mono);
  color: var(--dim);
}

.ov-series .ov-result-date { margin-left: auto; }

/* The maps of a series. The rule between them is lighter than the rule between
   SERIES, so the grouping is visible without a second border weight arriving
   from nowhere: it is the same colour at half the reach. */
.ov-result-map {
  padding: var(--sp-1-5) 0 var(--sp-1-5) var(--sp-6);
  border-bottom: 0;
}
.ov-result-map:first-of-type { padding-top: var(--sp-2); }

.ov-result-map .ov-chip {
  width: 18px;
  height: 18px;
  font-size: var(--fs-micro-2);
}

@media (max-width: 900px) {
  /* The indent is a desk affordance. At 390 it is 24px of the 342 a map row
     has to say a map name, a score and hold a 3D door. */
  .ov-result-map { padding-left: var(--sp-3); }
}

@media (min-width: 1600px) {
  /* The residual becomes a GUTTER instead of a void. `.main` is a grid item
     in the shell's `1fr` track with a max-width and no auto margins, so it
     is start-aligned: at 2560px the whole 1024px leftover sat on the right
     of the page and none of it beside the rail. Auto margins split it —
     272px a side at 2560 — and below the ceiling they are worth exactly
     zero, because there the column already fills its track. `.sharedview >
     .main` has centred itself this way since it was written; this is that
     rule stated for the shell, not a new idea.

     `width: 100%` is load-bearing and was found by measuring rather than by
     reading: an auto inline margin on a grid item takes away `justify-self:
     stretch`, so `.main` alone with auto margins stops filling its track and
     shrinks to its CONTENTS. Measured that way at 2560, `/maps` came out at
     1240px — narrower than the 1320 it had before the change. `width: 100%`
     puts the stretch back as an explicit size that `max-width` then caps,
     which is the same pair `.sharedview > .main` already carries. */
  .main {
    width: 100%;
    margin-inline: auto;
  }

  /* ── the tile grids keep the size the reference drew them at ──────────
     The ruling above `.kpi-grid` (`auto-FILL`, never `auto-fit`) is right
     and stays: a family of two cards must not stretch to half the column,
     and every card on a page is one size no matter how many its family
     has. What that ruling could not see is what happens to the CARD when
     the column grows past the width it was verified at. `auto-fill` keeps
     minting tracks and `1fr` shares the row across all of them, empty ones
     included — so at a 1800px column the cards shrink toward the px floor
     while the row empties behind them. Measured at 2560 with the column at
     1800 and no rule here: `/maps` drew its four tiles at 205px where the
     design width draws them at 237, i.e. the wide screen made the cards
     SMALLER. Four grids do this; every other grid in the product grows or
     holds, which is why only four are named.

     The fix is one number each, and the number is measured rather than
     picked: it is the track that grid resolves to at the design width. A
     floor set to the current track reproduces the current column count
     exactly — with N tracks, track = (W - (N-1)g)/N, and
     floor((W + g)/(track + g)) is N again — so these rules are a no-op at
     the breakpoint and only ever bite above it, where they stop the count
     from multiplying past the point where a card is smaller than the
     reference's own. More columns as the column grows is still what
     happens; a card thinner than the design's is what no longer does.

     `min(…, 100%)` is carried on all four, including `.ov-band`, which
     never had it: with a raised floor an auto-fill track can exceed a
     narrow container and push the grid out of it. */
  .kpi-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(241px, 100%), 1fr));
  }

  .sg-state-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(255px, 100%), 1fr));
  }

  .ov-band {
    grid-template-columns: repeat(auto-fill, minmax(min(238px, 100%), 1fr));
  }

  .map-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(304px, 100%), 1fr));
  }

  /* ── running text does not widen with the page ────────────────────────
     The scaffolding and the data surfaces are what the extra width is for.
     A sentence is not: `.page-sub` and `.absent-note` carry no measure at
     all today and already ran to 1252px at 1920px before any of this, and
     a wider column would only have made a long line longer. 74ch is the
     measure this sheet already uses for prose that sits in a page column
     (`.sg-note`, `.section-lede`); using it again is not a new decision.
     Deliberately NOT applied below the breakpoint, where it would change
     the desk width this task may not touch — the sentences are too long
     at 1440 too, and that is a separate ruling to ask for.

     The list is measured, not guessed, and the measurement is of the LINE
     rather than the box: a Range over each paragraph's contents at 2560px,
     taking the widest client rect, which is the longest line the reader
     actually gets. Everything over 640px of ink is here. Three candidates
     that a box-width scan had flagged are deliberately absent —
     `.panel-meta`, `.ubudget-note` and `.ld-cta-note` sit in wide boxes but
     draw short lines, and capping a box whose ink is already inside the
     measure would only have moved a caption for nothing. The rest of the
     list is in the sheets that own those classes. */
  .page-sub,
  .absent-note,
  .sc-foot,
  .h2h-foot,
  .sg-meta,
  /* `.lg-meta` and `.cn-step-note` belong in modules-b3.css. They landed
     here while that sheet was missing a closing brace (fixed the same
     night — see the note above the league section there); they stay only
     because moving them buys nothing and the wide block must remain this
     sheet's last block (the guard test enforces it). */
  .lg-meta,
  .cn-step-note {
    max-width: 74ch;
  }
}
