/* ═══════════════════════════════════════════════════════════════════════════
   CORE — the shared framework
   ───────────────────────────────────────────────────────────────────────────
   This file is the product: layout, components, motion, states. It carries NO
   game identity. valorant.css (and later league.css) supply the palette, the
   texture and the type — so the two dashboards are two versions of one
   analytics product rather than two websites.

   THE CSP FORBIDS WEBFONTS. The site ships with
       default-src 'none'; style-src 'self' 'unsafe-inline'
   and no font-src at all, so every @font-face and every Google Fonts link is
   blocked outright. The hierarchy here is built from weight, size, tracking
   and tabular numerals on the system stack — not from a typeface. Numbers are
   the product, so they get the most attention.

   MOTION IS A BUDGET, NOT A FEATURE. Everything animates on transform and
   opacity only, nothing longer than 320ms, and prefers-reduced-motion switches
   the whole application to instant. A stat that takes a second to arrive is
   slower than a stat that was simply printed.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* structure — identity files never change these */
  --r:      14px;
  --r-sm:   9px;
  --r-xs:   5px;
  --gap:    16px;
  --pad:    20px;

  --t-fast: 140ms;
  --t:      220ms;
  --t-slow: 320ms;
  --ease:   cubic-bezier(.22, .61, .36, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  --num: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', Menlo, Consolas, monospace;
  --ui:  system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

  --board-min: 300px;
}

[data-density="compact"] {
  --gap: 11px;
  --pad: 14px;
  --r: 11px;
}

/* ══ player identity ═════════════════════════════════════════════════════
   WHO someone is, never HOW THEY ARE DOING. These live in the shared file,
   not in either game's theme, precisely because they are not brand colours:
   a player is the same colour on both boards.

   This exists because the old scheme used --accent for player A and --win for
   player B. On Valorant --accent is #ff4655, so player A was RED and player B
   was GREEN, sitting next to --bad red and --win green that mean bad and
   good. Two people's names were being rendered in the vocabulary of praise
   and blame. Worse, --accent also marked the winner of each head-to-head row,
   so one colour meant "player A" in one panel and "the better value" in the
   next.

   So: no red and no green in this ramp, at either end. Hues are spaced for
   deuteranopia and protanopia (blue / amber / violet / magenta / cyan), which
   is also why there is no sixth -- five is where a qualitative ramp stops
   being reliably tellable apart, and it is the cap on the Compare picker.

   They are for FILLS AND MARKS, never for body text. The light-theme values
   are darkened to carry a swatch against --surface; none of them is being
   asked to clear a text contrast bar, because every name stays --ink. */
:root, [data-theme="dark"] {
  --id-1: #5aa9ff;   /* blue   */
  --id-2: #f2a93b;   /* amber  */
  --id-3: #c98bff;   /* violet */
  --id-4: #ff7ab8;   /* magenta*/
  --id-5: #4fd8e8;   /* cyan   */
}
[data-theme="light"] {
  --id-1: #1a68c9;
  --id-2: #a25f00;
  --id-3: #7b3fe4;
  --id-4: #bd1f6b;
  --id-5: #0a6f85;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 14.5px;
  line-height: 1.55;
  min-height: 100svh;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: none; background: none; text-align: inherit; }
a { color: inherit; text-decoration: none; }
svg { display: block; }

:where(a, button, input, select, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* Numerals must not jitter when a value animates, and they must line up in a
   column. Both come from the same two declarations, applied everywhere. */
.n {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ══ shell ═══════════════════════════════════════════════════════════════ */
.shell { max-width: 1440px; margin: 0 auto; padding: 0 clamp(14px, 3vw, 30px) 120px; }

.top {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 13px 0;
  background: var(--bg-blur);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid var(--line);
  margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 11px; min-width: 0; }
.brand .mark { width: 30px; height: 30px; flex: none; }
.brand h1 {
  font-size: 1.02rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  line-height: 1.1; white-space: nowrap;
}
.brand .sub {
  font-size: .72rem; color: var(--ink-3); letter-spacing: .05em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.top .sp { flex: 1 1 auto; }

/* primary navigation — three concepts, never more */
/* position:relative is load-bearing: the sliding pill is absolutely positioned,
   and without it the nearest positioned ancestor is the sticky .top — so the
   pill sized itself to the whole header and rendered as a 62px blob over a
   42px bar. Absolute positioning always needs its containing block named. */
.nav { position: relative; display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: var(--sunk); border: 1px solid var(--line); }
.nav button {
  position: relative; padding: 7px 17px; border-radius: 999px;
  font-size: .82rem; font-weight: 600; letter-spacing: .04em; color: var(--ink-3);
  transition: color var(--t-fast) var(--ease);
}
.nav button:hover { color: var(--ink-2); }
.nav button[aria-selected="true"] { color: var(--on-accent); }
/* the moving pill sits behind the label, so switching tabs slides rather than
   repaints — the cheapest way to make navigation feel deliberate */
.nav .pill {
  position: absolute; inset: 3px auto 3px 3px; border-radius: 999px;
  background: var(--accent); z-index: 0;
  transition: transform var(--t) var(--ease-out), width var(--t) var(--ease-out);
  will-change: transform, width;
}
.nav button span { position: relative; z-index: 1; }

.iconbtn {
  width: 33px; height: 33px; flex: none; display: grid; place-items: center;
  border-radius: var(--r-sm); border: 1px solid var(--line); color: var(--ink-2);
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              background-color var(--t-fast) var(--ease);
}
.iconbtn:hover { color: var(--ink); border-color: var(--line-2); background: var(--raised); }
.iconbtn svg { width: 16px; height: 16px; }
/* Back to the hub at cannotfivestack.com. Deliberately the SAME 33px .iconbtn
   as Refresh and Preferences beside it, so the header gains one more control
   rather than a new kind of chrome. The boards are subdomains of that apex, so
   this is ordinary navigation -- the CSP sets form-action 'none' but no
   navigate-to, and a link is not a fetch, so nothing has to be relaxed.
   The nudge on hover is the only thing genuinely new: it is the affordance
   that says "this leaves the page" rather than "this does something here",
   and the global prefers-reduced-motion block already cancels it.
   `a` inherits colour, text-decoration and the focus ring from the base reset
   at the top of this file, so none of that needs restating here. */
.backhub svg { transition: transform var(--t-fast) var(--ease); }
.backhub:hover svg { transform: translateX(-2px); }
.stamp { font-family: var(--num); font-size: .7rem; color: var(--ink-3); white-space: nowrap; }

/* ══ primitives ══════════════════════════════════════════════════════════ */
.panel {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--shadow);
}
.panel.pad { padding: var(--pad); }
.hd { display: flex; align-items: baseline; gap: 12px; margin-bottom: 14px; }
.hd h2 { font-size: .84rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.hd .sp { flex: 1; }
.hd .note { font-size: .74rem; color: var(--ink-3); }
.eyebrow {
  font-size: 9.5px; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-3);
}

/* ── the span problem ─────────────────────────────────────────────────────
   A <button> may only contain phrasing content, so every clickable card is
   built from <span>, not <div> — a div inside a button is invalid and browsers
   reflow it unpredictably. Spans are inline by default, which made every
   label sit ON THE SAME LINE as its value ("WIN%53.2%", "Current leadertjh00").

   Blockifying them here, once, is what keeps the component markup valid AND
   the layout correct. Flex and grid children are blockified automatically, so
   only the ones inside a plain block need naming. */
.pcard .met .k, .pcard .met .v,
.leader .t, .leader .who,
.match .mi, .match .mi .l2,
.mdetail .f .k, .mdetail .f .v,
.summary .k, .summary .v, .summary .s { display: block; }
.muted { color: var(--ink-3); }
/* minmax(0, 1fr), not the default implicit track. A bare `display: grid` gets
   ONE implicit column sized `auto`, and `auto`'s minimum is min-content -- so
   any single wide child stretches the column past the container instead of
   being contained by it. On Squad that child is the activity heatmap: 58 weeks
   x 13px = 925px of min-content, which grew this track to 981px inside a 390px
   phone. The page then panned sideways and .mobnav -- fixed, left:0 right:0 --
   sized itself to the DOCUMENT, so two of the three tabs sat off-screen and
   could not be pressed. The child's own `overflow-x: auto` does not stop this:
   overflow zeroes the automatic minimum of a FLEX/GRID ITEM, not the
   min-content contribution of a block box. Capping the track is what makes
   .heatscroll's scrollbar do its job. */
.grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--gap); }
.g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.split { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); align-items: start; }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: 999px; border: 1px solid var(--line);
  font-size: .76rem; color: var(--ink-2); white-space: nowrap;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease),
              background-color var(--t-fast) var(--ease);
}
.chip:hover { color: var(--ink); border-color: var(--line-2); }
.chip[aria-pressed="true"], .chip.on {
  color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft);
}
.segs { display: flex; gap: 2px; padding: 3px; border-radius: var(--r-sm); background: var(--sunk); border: 1px solid var(--line); }
.segs button {
  padding: 6px 13px; border-radius: var(--r-xs); font-size: .78rem; font-weight: 600;
  color: var(--ink-3); transition: all var(--t-fast) var(--ease);
}
.segs button:hover { color: var(--ink-2); }
.segs button[aria-pressed="true"] { background: var(--raised); color: var(--ink); }

.toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: var(--gap); }

/* ══ result marks ════════════════════════════════════════════════════════
   §48: colour is never the only signal. Every win/loss carries its letter as
   well as its colour, and a loss is DELIBERATELY quiet rather than red —
   colouring both ends of a scale makes a board of numbers shout. */
.wl {
  display: inline-grid; place-items: center;
  width: 19px; height: 19px; border-radius: var(--r-xs);
  font-size: 10px; font-weight: 800; letter-spacing: 0; font-family: var(--num);
}
.wl.w { background: var(--win-soft); color: var(--win); box-shadow: inset 0 0 0 1px var(--win-line); }
.wl.l { background: var(--sunk); color: var(--ink-3); box-shadow: inset 0 0 0 1px var(--line); }
/* A draw was previously styled EXACTLY like a loss — same ground, same ink,
   same border — so the only thing separating them was the glyph. It is a
   third result, not a soft loss, so it gets its own treatment: a dashed edge,
   which reads as "unresolved" without spending a colour on it. Losses stay
   deliberately quiet; colouring both ends of the scale makes a board shout. */
.wl.d {
  background: transparent; color: var(--ink-2);
  box-shadow: none; border: 1px dashed var(--line-2);
}
.form { display: flex; gap: 4px; }
.form .wl { transition: transform var(--t-fast) var(--ease); }
.form .wl:hover { transform: translateY(-2px); }

.delta { font-family: var(--num); font-size: .74rem; font-weight: 700; white-space: nowrap; }
.delta.up { color: var(--win); }
.delta.down { color: var(--bad); }
.delta.flat { color: var(--ink-3); }

/* ══ board summary ═══════════════════════════════════════════════════════ */
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px;
           background: var(--line); border: 1px solid var(--line); border-radius: var(--r);
           overflow: hidden; margin-bottom: var(--gap); }
.summary .cell { background: var(--surface); padding: 14px 17px; }
.summary .k { font-size: 9.5px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; color: var(--ink-3); }
.summary .v { font-family: var(--num); font-size: 1.5rem; font-weight: 700; margin-top: 5px; letter-spacing: -.03em; }
.summary .s { font-size: .74rem; color: var(--ink-3); margin-top: 1px; }

/* the "who is on fire" strip — factual superlatives, never invented */
.leaders { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--gap); margin-bottom: var(--gap); }
.leader {
  display: flex; align-items: center; gap: 12px; padding: 13px 15px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  transition: transform var(--t) var(--ease), border-color var(--t) var(--ease);
  width: 100%;
}
.leader:hover { transform: translateY(-2px); border-color: var(--line-2); }
.leader .ico { width: 34px; height: 34px; flex: none; border-radius: var(--r-sm); display: grid; place-items: center;
               background: var(--accent-soft); color: var(--accent); }
.leader .ico svg { width: 17px; height: 17px; }
.leader .b { min-width: 0; flex: 1; }
.leader .t { font-size: 9.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); }
.leader .who { font-size: .92rem; font-weight: 650; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.leader .val { font-family: var(--num); font-size: .96rem; font-weight: 700; color: var(--accent); flex: none; }

/* ══ player cards ════════════════════════════════════════════════════════ */
.board { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--board-min), 1fr)); gap: var(--gap); }

.pcard {
  position: relative; display: flex; flex-direction: column; gap: 13px;
  padding: var(--pad);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--shadow); overflow: hidden; width: 100%;
  transition: transform var(--t) var(--ease), border-color var(--t) var(--ease),
              box-shadow var(--t) var(--ease);
}
.pcard::before {
  /* the identity file paints this — a hairline of the player's rank colour */
  content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: var(--card-edge, transparent);
  opacity: .8; transition: opacity var(--t) var(--ease);
}
.pcard:hover, .pcard:focus-visible { transform: translateY(-3px); border-color: var(--line-2); box-shadow: var(--shadow-2); }
.pcard:hover::before { opacity: 1; }
.pcard:active { transform: translateY(-1px); }

.pcard .idrow { display: flex; align-items: center; gap: 12px; }
/* ── avatar ───────────────────────────────────────────────────────────────
   The initials sit underneath and the art covers them when it loads, so a
   failed image degrades to something identifiable rather than to a broken
   image glyph. .sm and .big are the leaderboard and hero sizes. */
.av {
  position: relative; display: grid; place-items: center; flex: none;
  width: 46px; height: 46px; border-radius: var(--r-sm); overflow: hidden;
  background: var(--sunk); border: 1px solid var(--line);
  transition: transform var(--t) var(--ease);
}
.av .avfb {
  font-family: var(--num); font-size: 14px; font-weight: 700;
  color: var(--ink-3); letter-spacing: -.02em; user-select: none; line-height: 1;
}
.av img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.av.sm { width: 22px; height: 22px; border-radius: var(--r-xs); }
.av.sm .avfb { font-size: 8.5px; }
.av.big { width: 72px; height: 72px; border-color: var(--line-2); }
.av.big .avfb { font-size: 22px; }
.pcard:hover .av { transform: scale(1.04); }
.pcard .nm { min-width: 0; flex: 1; }
.pcard .nm b { display: block; font-size: 1rem; font-weight: 650; letter-spacing: -.01em;
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcard .nm .rank { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.pcard .nm .rank img { width: 17px; height: 17px; flex: none; }
.pcard .nm .rank span { font-size: .76rem; color: var(--ink-2); }
.pcard .pos {
  position: absolute; top: 12px; right: 13px;
  font-family: var(--num); font-size: .7rem; font-weight: 700; color: var(--ink-3);
}

/* the metric row: the headline number is large, the rest are support */
.pcard .mets { display: grid; grid-template-columns: repeat(auto-fit, minmax(58px, 1fr)); gap: 10px; }
.pcard .met .k { font-size: 9px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); }
.pcard .met .v { font-family: var(--num); font-size: 1.04rem; font-weight: 700; margin-top: 1px; letter-spacing: -.02em; }
.pcard .met.lead .v { font-size: 1.3rem; color: var(--accent); }
.pcard .met .v.pos { color: var(--win); }
.pcard .met .v.neg { color: var(--bad); }

.pcard .foot { display: flex; align-items: center; gap: 10px; padding-top: 12px; border-top: 1px solid var(--line); }
.pcard .foot .when { margin-left: auto; font-size: .72rem; color: var(--ink-3); white-space: nowrap; }

/* a tiny sparkline of recent rating, drawn only when there is enough history */
.spark { height: 26px; width: 100%; overflow: visible; }

/* ══ leaderboard table ═══════════════════════════════════════════════════
   §45: a table IS the right visualisation for ranked comparison across many
   metrics. The cards answer "who is this"; the table answers "who is best at
   what", and no card grid does that as well. */
.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; border-radius: var(--r); border: 1px solid var(--line); }
table.lb { width: 100%; border-collapse: collapse; font-size: .84rem; background: var(--surface); }
table.lb th, table.lb td { padding: 9px 13px; text-align: right; white-space: nowrap; }
table.lb th:first-child, table.lb td:first-child,
table.lb th:nth-child(2), table.lb td:nth-child(2) { text-align: left; }
table.lb thead th {
  position: sticky; top: 0; z-index: 2; background: var(--raised);
  font-size: 9.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-3); border-bottom: 1px solid var(--line); cursor: pointer;
  transition: color var(--t-fast) var(--ease);
}
table.lb thead th:hover { color: var(--ink); }
table.lb thead th[aria-sort] { color: var(--accent); }
table.lb thead th .dir { display: inline-block; width: 9px; }
table.lb tbody tr { border-bottom: 1px solid var(--line); transition: background-color var(--t-fast) var(--ease); cursor: pointer; }
table.lb tbody tr:last-child { border-bottom: none; }
table.lb tbody tr:hover { background: var(--raised); }
table.lb td.num { font-family: var(--num); font-variant-numeric: tabular-nums; }
table.lb td .who { display: flex; align-items: center; gap: 9px; }
table.lb .best { color: var(--accent); font-weight: 700; }

/* ══ player detail ═══════════════════════════════════════════════════════ */
.detail { animation: slideUp var(--t-slow) var(--ease-out) both; }
.backbar { display: flex; align-items: center; gap: 10px; margin-bottom: var(--gap); }
.backbtn {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 14px;
  border-radius: 999px; border: 1px solid var(--line); font-size: .8rem; color: var(--ink-2);
  transition: all var(--t-fast) var(--ease);
}
.backbtn:hover { color: var(--ink); border-color: var(--line-2); background: var(--raised); }
.backbtn svg { width: 14px; height: 14px; }

.phero {
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
  padding: var(--pad); margin-bottom: var(--gap);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r);
  position: relative; overflow: hidden;
}
.phero .id { min-width: 0; }
.phero .id h2 { font-size: 1.6rem; font-weight: 700; letter-spacing: -.025em; line-height: 1.1; }
.phero .id .rank { display: flex; align-items: center; gap: 8px; margin-top: 5px; }
.phero .id .rank img { width: 24px; height: 24px; }
.phero .id .rank span { font-size: .88rem; color: var(--ink-2); }
.phero .hmets { display: flex; gap: 26px; margin-left: auto; flex-wrap: wrap; }
.phero .hmet .k { font-size: 9.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); }
.phero .hmet .v { font-family: var(--num); font-size: 1.62rem; font-weight: 700; letter-spacing: -.03em; margin-top: 2px; }
.phero .hmet .d { margin-top: 1px; }

/* ══ match rows ══════════════════════════════════════════════════════════ */
.matches { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; background: var(--surface); }
.match { width: 100%; display: flex; align-items: center; gap: 13px; padding: 11px 15px;
         border-bottom: 1px solid var(--line); transition: background-color var(--t-fast) var(--ease); }
.match:last-of-type { border-bottom: none; }
.match:hover { background: var(--raised); }
.match .res { width: 3px; align-self: stretch; border-radius: 2px; flex: none; background: var(--line-2); }
.match.won .res { background: var(--win); }
.match .agent { width: 34px; height: 34px; border-radius: var(--r-sm); flex: none; background: var(--sunk);
                display: grid; place-items: center; font-size: .66rem; font-weight: 700; color: var(--ink-3);
                overflow: hidden; }
.match .agent img { width: 100%; height: 100%; object-fit: cover; }
/* a remade game is neither a win nor a loss, so it is marked and not coloured */
.match.remake .res { background: var(--line-2); }
.match.remake .mi .l1 b { color: var(--ink-3); }
/* A draw needs its own edge too, for the same reason: the result bar is the
   first thing read, and a drawn game left on the default bar was
   indistinguishable from a defeat. Striped rather than solid, so it reads as
   "neither" at a glance and never as a quiet win. */
.match.draw .res {
  background: repeating-linear-gradient(
    135deg, var(--line-2) 0 3px, transparent 3px 6px);
}
.match.draw .mi .l1 b { color: var(--ink-2); }
.match .mi { min-width: 0; flex: 1; }
.match .mi .l1 { display: flex; align-items: baseline; gap: 8px; }
.match .mi .l1 b { font-size: .88rem; font-weight: 650; }
.match .mi .l1 .sc { font-family: var(--num); font-size: .78rem; color: var(--ink-3); }
.match .mi .l2 { font-size: .74rem; color: var(--ink-3); margin-top: 1px; }
.match .kda { font-family: var(--num); font-size: .86rem; font-weight: 650; flex: none; }
.match .kda i { color: var(--ink-3); font-style: normal; font-weight: 400; }
.match .num2 { font-family: var(--num); font-size: .82rem; text-align: right; min-width: 56px; flex: none; }
.match .when { font-size: .72rem; color: var(--ink-3); min-width: 62px; text-align: right; flex: none; }
.match .caret { color: var(--ink-3); transition: transform var(--t) var(--ease); flex: none; }
.match .caret svg { width: 14px; height: 14px; }
.match[aria-expanded="true"] .caret { transform: rotate(180deg); }
.mdetail { border-bottom: 1px solid var(--line); background: var(--sunk); overflow: hidden; }
.mdetail .inner { padding: 14px 16px 16px; display: grid;
                  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 13px; }
.mdetail .f .k { font-size: 9px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); }
.mdetail .f .v { font-family: var(--num); font-size: .92rem; font-weight: 650; margin-top: 2px; }

/* ══ charts ══════════════════════════════════════════════════════════════
   Every chart now carries explicit width/height attributes as well as a
   viewBox — a viewBox alone defines a coordinate system, not an intrinsic
   size, so inside a flex container the SVG had nothing to lay out and the
   radar collapsed to zero height while still being present in the DOM.
   max-width keeps them responsive without needing a resize observer. */
.chart { display: block; max-width: 100%; height: auto; }

/* ══ bars ════════════════════════════════════════════════════════════════ */
/* display:block matters — .bar is a <span> (it lives inside a <button> in some
   places) and an inline box ignores height entirely, so the track and its fill
   were laid out with zero height and the bars simply never appeared. */
.bar { display: block; height: 7px; border-radius: 99px; background: var(--sunk); overflow: hidden; }
.bar i { display: block; height: 100%; border-radius: 99px; width: 0;
         transition: width var(--t-slow) var(--ease-out); }
.barrow { display: flex; align-items: center; gap: 11px; padding: 6px 0; }
.barrow .lb { flex: 0 0 var(--lw, 92px); font-size: .8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.barrow .bw { flex: 1; min-width: 0; }
.barrow .vv { font-family: var(--num); font-size: .8rem; font-weight: 650; min-width: 52px; text-align: right; }

/* head-to-head: one bar, two ends, the winner marked with a glyph not a colour */
.h2h { padding: 11px 0; border-bottom: 1px solid var(--line); }
.h2h:last-child { border-bottom: none; }
.h2h .lab { display: flex; align-items: baseline; justify-content: center; gap: 8px; margin-bottom: 7px; }
.h2h .lab .k { font-size: 9.5px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; color: var(--ink-3); }
.h2h .track { display: flex; align-items: center; gap: 11px; }
.h2h .side { flex: 1; display: flex; align-items: center; gap: 9px; min-width: 0; }
.h2h .side.a { flex-direction: row-reverse; }
.h2h .side .v { font-family: var(--num); font-size: .96rem; font-weight: 700; min-width: 58px; }
.h2h .side.a .v { text-align: right; }
.h2h .side .t { flex: 1; height: 8px; border-radius: 99px; background: var(--sunk); overflow: hidden; display: flex; }
.h2h .side.a .t { justify-content: flex-end; }
.h2h .side .t i { display: block; height: 100%; width: 0; border-radius: 99px;
                  transition: width var(--t-slow) var(--ease-out); }
.h2h .win .v { color: var(--accent); }
.h2h .mark { font-size: 10px; font-weight: 800; color: var(--accent); }

/* ══ squad ═══════════════════════════════════════════════════════════════ */
.net { width: 100%; touch-action: pan-y; }
.net .edge { stroke: var(--line-2); transition: stroke var(--t-fast), stroke-width var(--t-fast), opacity var(--t-fast); cursor: pointer; }
.net .edge.hot { stroke: var(--accent); }
.net .edge.dim { opacity: .18; }
.net .node { cursor: pointer; }
.net .node circle { fill: var(--raised); stroke: var(--pc, var(--line-2)); stroke-width: 1.5;
                    transition: stroke var(--t-fast), fill var(--t-fast), r var(--t-fast); }
.net .node.hot circle { stroke: var(--accent); fill: var(--accent-soft); }
.net .node.dim { opacity: .3; }
.net text { font-family: var(--ui); font-size: 10.5px; fill: var(--ink-2); pointer-events: none; }
.net .node.hot text { fill: var(--ink); }

/* Same implicit-track trap as .grid above. A Lineups row is five names joined
   by ' + ' on a nowrap line; left uncapped it took the column 23px past the
   viewport on its own, which is enough to break the fixed footer. */
.pairs { display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; }
.pair { display: flex; align-items: center; gap: 12px; padding: 10px 13px; min-width: 0;
        border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface);
        transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); width: 100%; }
.pair:hover { border-color: var(--line-2); transform: translateX(2px); }
.pair .who { flex: 1; min-width: 0; font-size: .86rem; font-weight: 600;
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pair .g { font-family: var(--num); font-size: .76rem; color: var(--ink-3); flex: none; }
.pair .wr { font-family: var(--num); font-size: .96rem; font-weight: 700; flex: none; min-width: 52px; text-align: right; }
.pair .lift { font-family: var(--num); font-size: .76rem; font-weight: 700; min-width: 50px; text-align: right; flex: none; }

/* activity heatmap — day columns, week rows */
/* The scrollbar belongs here, at EVERY width, not in the phone block where it
   used to live. §49 wants controlled horizontal scroll on the chart and never
   on the page, and the calendar outruns its panel long before the viewport
   gets narrow: a year of history is 994px of columns, which spilled out of an
   804px panel at tablet width and took the document to 1042px. Capping the
   grid track (see .grid) stops the TRACK growing; it does not stop a child
   with `overflow: visible` painting past it and extending the page's scroll
   area. Only this does. */
.heatscroll { overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
.heat { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 1fr); gap: 3px; }
.heat .c { aspect-ratio: 1; border-radius: 3px; background: var(--sunk); transition: transform var(--t-fast) var(--ease); }
.heat .c[data-n]:hover { transform: scale(1.22); }
.heatlbl { display: grid; grid-template-rows: repeat(7, 1fr); gap: 3px; margin-right: 6px; }
.heatlbl span { font-size: 9px; color: var(--ink-3); display: grid; place-items: center; }

/* ══ compare ═════════════════════════════════════════════════════════════
   Grouped bars at every player count, including two. A diverging centre-out
   layout can only ever show a duel, so supporting three players meant either
   two layouts to keep honest or one that scales; this is the one that scales.

   The swatch is not decoration. It is the only thing tying a name in the
   picker to a shape on the radar and a bar in a row, and its absence is what
   made the old two-colour scheme unreadable. */
.cmppick { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.cmpchip {
  display: flex; align-items: center; gap: 9px; padding: 7px 13px 7px 9px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--surface);
  font-size: .86rem; font-weight: 600; color: var(--ink-2);
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.cmpchip:hover { border-color: var(--line-2); color: var(--ink); }
.cmpchip[aria-pressed="true"] { color: var(--ink); border-color: var(--pc, var(--line-2)); }
/* the dot is hollow until the player is picked, so "in" and "out" differ in
   shape as well as fill -- colour alone is not a state (§48) */
.cmpchip .dot {
  width: 13px; height: 13px; border-radius: 50%; flex: none;
  border: 2px solid var(--line-2); background: none;
}
.cmpchip[aria-pressed="true"] .dot { border-color: var(--pc); background: var(--pc); }
.cmpchip:disabled { opacity: .45; cursor: default; }
.cmpcount { text-align: center; font-size: .75rem; color: var(--ink-3); margin-top: 10px; }

.cmpmetric { padding: 11px 0; border-bottom: 1px solid var(--line); }
.cmpmetric:last-child { border-bottom: none; }
.cmpmetric > .lab { display: flex; align-items: baseline; gap: 8px; margin-bottom: 7px; }
.cmpmetric > .lab .k { font-size: .82rem; font-weight: 650; cursor: help; }
.cmpmetric > .lab .note { font-size: .7rem; color: var(--ink-3); }
.cmprow { display: flex; align-items: center; gap: 10px; padding: 2.5px 0; }
.cmprow .nm {
  flex: 0 0 var(--lw, 92px); min-width: 0; font-size: .78rem; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cmprow .bw { flex: 1; min-width: 0; }
.cmprow .vv { flex: none; min-width: 58px; text-align: right; font-size: .82rem; font-weight: 650; }
/* Sits where the bar would be when only one player is selected: there is no
   second value for a bar to be longer than, so the standing is spelled out. */
.cmprow .rank { font-size: .74rem; color: var(--ink-3); }
/* The leader is marked by weight and a ▲, not by hue -- hue is already spoken
   for by identity, and a page where the same green means both "Bob" and
   "better" teaches you to trust neither. */
.cmprow.lead .nm, .cmprow.lead .vv { color: var(--ink); font-weight: 750; }
.cmprow .mark { color: var(--ink-3); font-size: .7rem; }

.cmplegend { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; margin-top: 10px; }
.cmplegend span { display: flex; align-items: center; gap: 7px; font-size: .78rem; }
.cmplegend i { width: 10px; height: 10px; border-radius: 3px; flex: none; }

/* ══ tooltip ═════════════════════════════════════════════════════════════ */
.tip {
  position: fixed; z-index: 90; max-width: 260px; padding: 9px 12px;
  background: var(--raised); border: 1px solid var(--line-2); border-radius: var(--r-sm);
  box-shadow: var(--shadow-2); font-size: .78rem; line-height: 1.5; pointer-events: none;
  opacity: 0; transform: translateY(5px); transition: opacity var(--t-fast), transform var(--t-fast);
}
.tip.on { opacity: 1; transform: none; }
.tip b { color: var(--ink); font-weight: 700; }
.tip .t { font-size: 9.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
          color: var(--accent); margin-bottom: 3px; }
.tip .n { display: block; margin-top: 3px; color: var(--ink-3); }

/* ══ menu ════════════════════════════════════════════════════════════════ */
.menuwrap { position: relative; }
.menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 60; width: 268px;
  padding: 14px; border-radius: var(--r); background: var(--surface);
  border: 1px solid var(--line-2); box-shadow: var(--shadow-2);
  animation: pop var(--t) var(--ease-out) both;
}
.menu h4 { font-size: 9.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
           color: var(--ink-3); margin-bottom: 7px; }
.menu .mgroup + .mgroup { margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--line); }
.menu .mrow, .menu .mstats { display: flex; flex-wrap: wrap; gap: 5px; }
.menu .mrow button, .menu .mstats button {
  padding: 5px 11px; border-radius: 999px; border: 1px solid var(--line);
  font-size: .74rem; color: var(--ink-3); transition: all var(--t-fast) var(--ease);
}
.menu .mrow button:hover, .menu .mstats button:hover { color: var(--ink); border-color: var(--line-2); }
.menu .mrow button[aria-pressed="true"], .menu .mstats button[aria-pressed="true"] {
  color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft);
}
.menu .foot-note { font-size: .7rem; color: var(--ink-3); margin-top: 7px; }

/* ══ loading ═════════════════════════════════════════════════════════════
   §29: never the word "Loading". Skeletons in the SHAPE of what is coming, so
   the layout does not jump when the data lands. */
.sk { position: relative; overflow: hidden; background: var(--sunk); border-radius: var(--r-xs); }
.sk::after {
  content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--shimmer), transparent);
  animation: shimmer 1.35s infinite;
}
@keyframes shimmer { to { transform: translateX(100%); } }
.skcard { padding: var(--pad); border: 1px solid var(--line); border-radius: var(--r);
          background: var(--surface); display: flex; flex-direction: column; gap: 13px; }
.skrow { display: flex; align-items: center; gap: 12px; }

.progress { height: 2px; background: var(--sunk); border-radius: 99px; overflow: hidden; margin-bottom: var(--gap); }
.progress i { display: block; height: 100%; background: var(--accent);
              transition: width var(--t-slow) var(--ease-out); }

/* ══ states ══════════════════════════════════════════════════════════════ */
.empty { text-align: center; padding: 40px 22px; }
.empty .ic { width: 42px; height: 42px; margin: 0 auto 13px; border-radius: 12px;
             display: grid; place-items: center; background: var(--sunk); color: var(--ink-3); }
.empty .ic svg { width: 20px; height: 20px; }
.empty h3 { font-size: .94rem; font-weight: 650; margin-bottom: 4px; }
.empty p { font-size: .82rem; color: var(--ink-3); max-width: 34ch; margin: 0 auto 14px; line-height: 1.6; }
.btn { display: inline-flex; align-items: center; gap: 7px; padding: 8px 17px; border-radius: var(--r-sm);
       background: var(--accent); color: var(--on-accent); font-weight: 650; font-size: .82rem;
       transition: filter var(--t-fast) var(--ease), transform var(--t-fast) var(--ease); }
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: scale(.98); }
.btn.ghost { background: none; border: 1px solid var(--line); color: var(--ink-2); }
.btn.ghost:hover { color: var(--ink); border-color: var(--line-2); background: var(--raised); }

/* ══ motion ══════════════════════════════════════════════════════════════ */
@keyframes slideUp { from { opacity: 0; transform: translateY(12px); } }
@keyframes pop { from { opacity: 0; transform: translateY(-6px) scale(.98); } }
@keyframes fadeIn { from { opacity: 0; } }

/* §32 staggered reveal. The delay is capped: a twelfth card must not wait
   half a second, and nothing is un-clickable while it animates. */
.reveal > * { animation: slideUp var(--t-slow) var(--ease-out) both; }
.reveal > *:nth-child(1) { animation-delay: 0ms; }
.reveal > *:nth-child(2) { animation-delay: 34ms; }
.reveal > *:nth-child(3) { animation-delay: 68ms; }
.reveal > *:nth-child(4) { animation-delay: 102ms; }
.reveal > *:nth-child(5) { animation-delay: 136ms; }
.reveal > *:nth-child(6) { animation-delay: 170ms; }
.reveal > *:nth-child(7) { animation-delay: 190ms; }
.reveal > *:nth-child(n+8) { animation-delay: 210ms; }

.viewswap { animation: slideUp var(--t) var(--ease-out) both; }

/* ══ responsive ══════════════════════════════════════════════════════════
   §34: not a shrunken desktop. The nav becomes a bottom bar, the split
   collapses, comparisons stack, and the network graph is replaced by the
   ranked list rather than squeezed.

   THE BASE STATE COMES FIRST. This rule used to sit AFTER the 720px block,
   where — at identical specificity — it won the cascade and the bottom bar was
   hidden at every width. Nothing about the source looked wrong: both rules
   existed and both were correct in isolation. Only rendering the page caught
   it. A media query never beats a later rule of the same weight. */
.mobnav { display: none; }

@media (max-width: 1080px) {
  .split { grid-template-columns: minmax(0, 1fr); }
  .g3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  :root { --board-min: 100%; }
  .g2, .g3 { grid-template-columns: minmax(0, 1fr); }
  .shell { padding-bottom: 96px; }
  /* same reason as .mobnav below -- see the comment there */
  .top { padding: 10px 0; gap: 10px;
         background: var(--bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .brand h1 { font-size: .92rem; }
  .brand .sub { display: none; }
  .top .nav { display: none; }
  .phero { gap: 14px; }
  .phero .hmets { margin-left: 0; width: 100%; gap: 20px; }
  .phero .id h2 { font-size: 1.3rem; }
  .match .num2, .match .when { display: none; }

  /* Nine sort buttons in a row are 539px wide — they must wrap, or the whole
     page scrolls sideways on a phone. Caught by rendering at 390px, not by
     reading the CSS. */
  .segs { flex-wrap: wrap; }
  .toolbar { gap: 8px; }

  /* .heatscroll carries the scrollbar at every width now -- see the base rule.
     All a phone adds is a fixed, thumb-sized column instead of a squeezed one. */
  .heat { grid-auto-columns: 13px; }

  /* NO backdrop-filter on a phone, and the background is opaque to match.
     A backdrop filter re-rasterises everything painted behind it on every
     frame in which that content changes -- and on this page it changes
     constantly: .viewswap translates the WHOLE of #view on every tab press,
     countTo rewrites twenty counters per frame for 480ms, and the compare
     radar redraws for 900ms. Two full-width blur layers (this and .top) then
     re-snapshot the page sixty times a second, which on real phone GPUs shows
     up as a brief full-screen flash -- people read it as the page reloading.
     Desktop keeps the glass: it has the fill rate for it, and no one reported
     a flash there.

     The opaque background is not optional. --bg-blur is 82-85% alpha and only
     looks right WITH the blur behind it; dropped on its own it would let the
     board scroll through the bar unblurred. */
  .mobnav {
    display: grid; grid-template-columns: repeat(3, 1fr);
    position: fixed; left: 0; bottom: 0; z-index: 50;
    /* 100vw, not right:0. `left:0; right:0` resolves against the initial
       containing block, which grows with the document -- so the day something
       overflows sideways, this bar stretches with it and carries two of the
       three tabs off-screen where they cannot be pressed. That is precisely
       what the uncapped .grid track did on Squad. The width is pinned to the
       VIEWPORT so the tabs stay reachable even if that ever happens again;
       scripts/mobile-guard.mjs is what actually catches the overflow. */
    width: 100vw; max-width: 100%;
    background: var(--bg); backdrop-filter: none; -webkit-backdrop-filter: none;
    border-top: 1px solid var(--line);
    padding: 7px 6px calc(7px + env(safe-area-inset-bottom));
  }
  .mobnav button { display: flex; flex-direction: column; align-items: center; gap: 3px;
                   padding: 6px 2px; font-size: 9.5px; font-weight: 700; letter-spacing: .07em;
                   text-transform: uppercase; color: var(--ink-3); transition: color var(--t-fast); }
  .mobnav button svg { width: 18px; height: 18px; }
  .mobnav button[aria-selected="true"] { color: var(--accent); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}
/* the same switch, offered explicitly in the menu for people whose OS setting
   does not match what they want here */
[data-motion="off"] *, [data-motion="off"] *::before, [data-motion="off"] *::after {
  animation-duration: .001ms !important; animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
}

@media print { .top, .mobnav, .menu, .tip { display: none !important; } }

/* ── the refresh button while a run is in flight ──────────────────────────
   `busy` was being set by the script and styled by nothing, so pressing
   Refresh produced no visible change whatever — which is most of why the
   button read as broken. Three things say "working" here, because one is not
   enough: the control stops accepting presses, it dims, and its icon turns.

   A real refresh dispatches a GitHub Action and takes MINUTES, not the
   milliseconds a re-download takes. That is why this is a sustained state
   with its own wording beside it rather than a flicker. */
.iconbtn:disabled { cursor: default; opacity: .55; }
.iconbtn:disabled:hover { color: var(--ink-2); border-color: var(--line); background: none; }

.iconbtn.busy { color: var(--accent); border-color: var(--line-2); }
.iconbtn.busy svg { animation: spin 1.15s linear infinite; transform-origin: 50% 50%; }
@keyframes spin { to { transform: rotate(360deg); } }

/* The stamp carries the wording, so it has to stay readable while it says
   something longer than "updated 4 min ago". */
.stamp.working { color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  /* Simplified, not removed: with the spin gone the dimming and the wording
     are the only signals left, so the icon pulses instead of turning. */
  .iconbtn.busy svg { animation: pulse 1.8s ease-in-out infinite; }
  @keyframes pulse { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
}

/* The short wording is picked in app.js rather than clamped here: .stamp is
   nowrap, so a max-width would have overflowed rather than wrapped, and an
   ellipsis would have eaten the words "a few minutes" — the one part of the
   message worth keeping. */
