/* ============================================================================
   quad-site.css — the one stylesheet for the four-player MWPA site.

   It is standalone: these pages do not load ../style.css, because the sibling
   dashboard's sheet is another agent's file and imports a web font.

   THE SYSTEM RULE, and everything below follows from it.

       INK IS EXACT.  HUE IS A PERSON.  A SPAN IS AN ESTIMATE.

   The win probability curve is measured to 2.0e-16; the four ratings are not
   known at all. Those are two different epistemic registers and they are drawn
   in two registers you cannot confuse. An exact path is a line in --ink. An
   exact magnitude is a solid bar hanging off the null. An estimate is a capped
   dimension line in a person's hue. Sign — the direction a probability moved —
   is the one thing that is allowed to colour an exact number, because CONTRACT
   rule 2 requires sign to be carried three ways and colour is one of the three.

   RULES THIS FILE IS HELD TO, stated so a later edit cannot quietly drop them.

   1. CONTRAST FLOOR. Text clears 4.5:1 against the worst surface it can land
      on. Marks that carry information clear 3:1 — which is why --rule (a
      hairline, structure) and --rule-strong (a gridline, information) are two
      tokens and not one. Checked by ../contrast.py against every token below,
      including all four player hues on both themes and on the composited wash.

   2. SIGN IS A COLOUR, PLAYER IS A HUE, AND THE TWO NEVER MIX. --up/--down
      only ever mean the direction a probability moved. The four player hues
      only ever mean which person a mark belongs to. Sign is carried three ways
      — the + or - the format spec prints, a position relative to the null, and
      colour. The ▲/▼ that used to lead the number is gone: it repeated the
      character already in front of it, in a second glyph the reader had to
      learn.

   3. FOUR HUES DO NOT SURVIVE DICHROMACY ON THEIR OWN. The palette is re-
      stepped so validate_palette.js passes all five checks on --pairs all in
      both themes (dark: normal 20.2, CVD 10.2, chroma 0.121; light: 18.9, 9.2,
      0.111), but at tritan it measures 10.2–11.2, which is legal and not
      comfortable. So every series also carries a marker shape, a stroke
      pattern, an end label and the player's initial on the largest marks. Do
      not remove any of them as redundant with the colour.

   4. HAIRLINES, NOT SHADOWS. Structure is 1px borders, radii stay at 2px.
      There is one transition and it is .18s ease. No shadow anywhere.

   5. TWO TYPE ROLES, and both of them arrive. Mono owns every number, label,
      axis tick and heading; a serif owns the argument prose. The three
      families this file used to declare were measured: two of them were never
      installed and rendered identically to the system sans, so the site was
      one voice pretending to be three.

   6. NOTHING LOADS OVER THE NETWORK. No @import, no CDN, no web font.

   7. NO MOTION CARRIES A FINDING. There is no load animation on any interval,
      rail or bar. A site whose result is a null must not dramatise its own
      uncertainty, and a reader who arrives mid-animation must never see a
      different number from one who arrives after it.
   ========================================================================= */

/* ---- Tokens --------------------------------------------------------------- */
:root {
  /* Dark is the default: this is a night-games dataset. It is dark by
     arithmetic, not by mood — four hues inside the dataviz lightness band
     L 0.48–0.67 must still clear 4.5:1 as TEXT, because these tokens set
     player names and end labels as well as stroking lines. At --field
     L 0.222 the worst hue clears 5.02:1; at L 0.28 the same hues fall to
     about 4.1 with no legal way to brighten them. */
  --ground: #0b1416;       /* the page */
  --field: #121d20;        /* the measurement surface: figures, tables, masthead */
  --well: #081011;         /* recessed rows, hover, insets */
  --ink: #e6ecec;          /* text, the null rule, and the curve. The exact register. */
  --quiet: #9aabad;        /* secondary text, axis labels, crosshair, non-focal marks */
  --rule: #334750;         /* every structural hairline. One token, replacing four. */
  --rule-strong: #5b737b;  /* every line that carries information: gridlines, axes, ticks */
  --up: #4ea86e;
  --down: #e2705a;
  --martin: #af841a;
  --snorlax: #00a1c6;
  --themarias: #a16dff;
  --trzzcko: #e35b8b;
  --wash-a: .105;          /* the one fill in the system; see --wash below */

  /* THE ONE SURFACE THAT IS THE SAME IN BOTH THEMES, and it is declared twice
     with the same literal on purpose: ../contrast.py guards it in both blocks,
     so the invariance is a thing that can fail rather than a comment. Riot's
     weapon art is a near-white silhouette on transparency. On the page surfaces
     it measures 6.93 median in dark and 1.53 in light — under the legibility
     floor, i.e. fainter than a table hairline. On this plate it is 6.99 in both,
     with all nineteen files clearing the 3:1 mark floor. The plate is a casing
     for one family of art, never a surface for text or for a number. */
  --art-plate: #111c1f;

  /* TEAM IS A SURFACE, NOT A HUE. The Team column comes out of the ten-player
     table and the round ledger, so team identity has to live somewhere else:
     one <tbody> per side, a group row naming it, and one of these two washes
     under the rows. Hue is not available — hue is a person on this site — so
     both washes are --well mixed into --field, which is to say they are the
     two surfaces contrast.py already guards, part of the way between each
     other. Every colour that can land on them therefore sits BETWEEN its
     field number and its well number, and both of those are checked. Measured:
     worst text 5.37 dark / 5.10 light (the hues, as names), worst information
     mark --rule-strong 3.66 dark / 3.84 light. Floors are 4.5 and 3.0. */
  --team-focal-wash: color-mix(in srgb, var(--well) 55%, var(--field));
  --team-other-wash: color-mix(in srgb, var(--well) 18%, var(--field));

  /* Chart vocabulary. The names are the reference component's so the two
     sites read as one language; the values are this palette and nothing else
     is imported. There is no area fill tinted toward a side any more: two
     tokens put a green swatch labelled "Red" on 30 of 65 match pages, which
     broke rule 2 on a page where the teams are literally named Red and Blue.
     One neutral --wash replaces both, and it means distance from the null. */
  --chart-line-primary: var(--ink);
  --chart-grid: var(--rule-strong);
  --chart-boundary: var(--rule-strong);
  --chart-crosshair: var(--quiet);
  --chart-tooltip-foreground: var(--ink);
  --chart-segment-line: var(--quiet);

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --serif: ui-serif, Charter, "Bitstream Charter", "Iowan Old Style", Georgia, serif;
  --w-body: 400;
  --w-strong: 600;
  /* Four sizes and one label size, against the twelve-plus-seven this file
     used to carry. */
  --fs-label: 11px;        /* THE ONE LABEL STYLE: mono, uppercase, +.13em */
  --fs-data: 13px;         /* table and rail data */
  --fs-body: 14.5px;       /* interface */
  --fs-prose: 16.5px;      /* the argument */
  --fs-head: 20px;         /* section heads */
  --fs-display: 34px;      /* the one headline sentence */

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 18px;
  --sp-5: 28px;
  --sp-6: 44px;
  --sp-7: 68px;

  --r: 2px;
  --maxw: 1180px;

  /* THE IMAGE HEIGHT ON THIS SITE, and there is one of it. 18px of art inside
     a 1px box is a 20px slot, and the slot fits inside the ledger row's
     existing 20.15px line box (13px x 1.55), so the row is 33px tall with
     portraits and 33px without them. A 20px portrait makes it 39px. The two
     pixels are the whole reason for the number.

     Three families ship and all three are 18px TALL, so no table row on this
     site is taller because of a picture than it was before pictures. Agent and
     rank art is square and 18px wide too. Weapon art is not square — Riot ships
     it 96px wide at 17 to 73px tall, a 1.3:1 to 5.7:1 spread — so it gets its
     own WIDTH and is contain-fitted into the box: build.py computes each file's
     rendered width and height from its own intrinsic size and writes both onto
     the element, so no weapon reflows and none is distorted. One box does hold
     them; it just is not a square. */
  --icon-art: 18px;
  --icon-slot: 20px;
  --weapon-art-w: 42px;
  --weapon-slot-w: 44px;

  /* THE CARD'S OWN ART SIZES, and they are the only place on this site where a
     picture is not 18px tall. Everywhere else the art rides inside a table row
     and the row's line box is the constraint. The card is not a row: it is the
     one object here built to be looked at, so the art ships at the size the
     files ACTUALLY ARE — meta.assets.agent.intrinsic_px is 40, and weapon art
     is 96 wide at 17 to 73 tall, contain-fitted into a slot 96 wide so the
     widest of them renders at 1:1 with no resampling at all. Contrast is a
     property of the pixels and not of the size, so ../contrast.py's numbers for
     both families carry across unchanged; the weapon plate comes with them,
     because that is what fixed 1.53 in the light theme. */
  --icon-card: 40px;
  --icon-card-slot: 42px;
  --weapon-card-w: 96px;
  --weapon-card-h: 36px;
  --weapon-card-slot-w: 98px;

  /* Rail geometry. Every rail on every page shares it, so the null rule lands
     at the same physical x on the front page, the player page and inside a
     table cell. That is the cost of the design and it is deliberate. */
}

:root[data-theme="light"] {
  --ground: #e7eceb;       /* pale cool grey-green — plotter stock, not cream */
  --field: #f5f9f8;
  --well: #dee4e3;
  --ink: #0d1719;
  --quiet: #4f5c5d;
  --rule: #a4b2b0;
  --rule-strong: #69797c;
  --up: #0f6a37;
  --down: #a83a22;
  --martin: #915405;
  --snorlax: #006999;
  --themarias: #7a2cd6;
  --trzzcko: #be0b67;
  --art-plate: #111c1f;    /* the same literal as the dark block, and that is the point */
  --wash-a: .085;
  /* The same mix, written out again rather than inherited, for the reason
     --art-plate is written out again: contrast.py's declared_tokens() reads one
     top-level block at a time, and a token that appears in only one of them
     cannot be guarded in the other. The expression is theme-aware on its own —
     --well is BELOW --field here and ABOVE it in the dark block, so the wash
     darkens here and lightens there, always toward the recessed surface. */
  --team-focal-wash: color-mix(in srgb, var(--well) 55%, var(--field));
  --team-other-wash: color-mix(in srgb, var(--well) 18%, var(--field));
}

/* The only fill in the system, and it carries no side semantics: it means
   distance from the null. Declared once, after both themes, because it is
   composed of two tokens that both move. contrast.py checks every mark that
   can land on it, at both alphas, on all three surfaces. */
:root { --wash: rgba(230, 236, 236, var(--wash-a)); }
:root[data-theme="light"] { --wash: rgba(13, 23, 25, var(--wash-a)); }

/* Rule 2. One class per player, setting a variable rather than a colour, so
   the same class drives a stroke in SVG and ink in HTML. */
.p-martin { --hue: var(--martin); }
.p-snorlax { --hue: var(--snorlax); }
.p-themarias { --hue: var(--themarias); }
.p-trzzcko { --hue: var(--trzzcko); }
.hue { color: var(--hue, currentColor); }

/* ---- Base ----------------------------------------------------------------- */
* { box-sizing: border-box; }
html, body { margin: 0; }

body {
  background: var(--ground);
  color: var(--ink);
  font-family: var(--mono);
  font-size: var(--fs-body);
  line-height: 1.55;
  font-weight: var(--w-body);
  font-variant-numeric: tabular-nums;
}
:root[data-theme="dark"] body { -webkit-font-smoothing: antialiased; }

/* There is no accent colour on this site. A link is --ink with a --rule
   underline that comes up to --ink on hover; focus is a 2px --ink ring.
   Interaction is weight, not hue — which also removes the third red the type
   audit found sitting at dE76 8.2 from a lost match. */
a { color: inherit; text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: 2px; text-decoration-thickness: 1px; }
a:hover { text-decoration-color: currentColor; }

button { font: inherit; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

code, kbd, samp { font-family: var(--mono); font-size: .94em; color: var(--quiet); }
hr { border: 0; border-top: 1px solid var(--rule); margin: var(--sp-5) 0; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--field); color: var(--ink);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--ink);
}
.skip:focus { left: var(--sp-3); top: var(--sp-3); }

.num, .n, .unit, .big, .ival-text, th, .lab, .chip,
.crumb, .scope, time { font-family: var(--mono); }
.unit { font-size: var(--fs-label); color: var(--quiet); white-space: nowrap; }
/* There is no .glyph rule any more. It set the size and the baseline of the
   ▲/▼, and nothing else ever went through that span: a measured zero is a
   middot inside `.num.zero` and an unmeasured value an em dash inside
   `.num.none`, both of which take the number's own type. */

/* THE ONE LABEL STYLE. It replaces the eight rules that used to spell this
   across four tracking values, two sizes, two weights and two colours. */
.lab, h4, thead th, .team-head th, .facts dt, .fig-data > summary {
  font-family: var(--mono);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--quiet);
}
/* A LABEL TAKES NO PROSE MEASURE. `p` caps at 66ch and `ch` is resolved against
   the ELEMENT's own size, so on a <p class="lab"> that is 66 x 6.62px = 437px
   against the 1144px rail it labels: `95.0% BCa interval, clustered on match ·
   axis -23.3% to +23.3%` broke after `to` and left the second bound alone on a
   line, at .13em tracking in uppercase. A label is as wide as the thing it
   labels. */
.lab { display: block; max-width: none; }

/* ---- Chrome --------------------------------------------------------------- */
.site-head {
  border-bottom: 1px solid var(--rule);
  background: var(--well);
  position: sticky; top: 0; z-index: 50;
  max-width: var(--maxw); margin: 0 auto; padding: 0 var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-4); min-height: 54px;
  flex-wrap: wrap;
}
.brand {
  font-family: var(--mono);
  font-size: var(--fs-body); font-weight: 700; letter-spacing: .04em;
  margin: 0; white-space: nowrap; font-variant-numeric: normal;
}
.brand a { text-decoration: none; }
/* THE WAY BACK OUT. This site moved up into /valorant/ when the dashboard that used to live
   there was retired, and that dashboard carried the site-wide nav band from /assets/hub.js.
   Importing hub.js here would buy the band at the cost of house rule 5 — it is an absolute
   /assets/ path, so every page would stop opening off disk. One link costs nothing and a broken
   link on file:// is a dead end rather than a broken page. Quiet, because it is a way out and
   not a section of this site. */
.brand .up { color: var(--quiet); font-weight: 400; }
.brand .up:hover { color: var(--ink); }
.brand-sep { color: var(--rule); margin: 0 .4em; }
.site-nav {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  font-family: var(--mono); font-size: var(--fs-data); flex: 1 1 auto;
}
.site-nav a { color: var(--quiet); text-decoration: none; }
.site-nav a:hover, .site-nav a[aria-current] { color: var(--ink); }

/* Two controls, one mechanism: an attribute on the root, a localStorage key, a
   button that says what it will do. The identity toggle is the second, and it
   is the argument for every image on the site rather than a convenience. */
.theme-toggle, .icons-toggle {
  background: none; border: 1px solid var(--rule); color: var(--quiet);
  font-family: var(--mono); font-size: var(--fs-data); line-height: 1;
  padding: 7px 10px; border-radius: var(--r); cursor: pointer; white-space: nowrap;
  transition: color .18s ease, border-color .18s ease;
}
.theme-toggle:hover, .icons-toggle:hover { color: var(--ink); border-color: var(--ink); }
.icons-toggle[aria-pressed="true"] { color: var(--ink); }

.page { max-width: var(--maxw); margin: 0 auto; padding: var(--sp-6) var(--sp-4) var(--sp-7); }

/* ---- Prose ---------------------------------------------------------------- */
/* The pairing is inverted from the usual on purpose: the NUMBERS are
   monospaced and the ARGUMENT is serif. The site's central claim — the
   interval covers zero, and here is why it cannot be read as excluding it —
   is a sentence, so sentences get the face with a voice and data gets the
   face with a grid. */
h1, h2, h3 { margin: 0; }
h1 {
  font-family: var(--serif); font-weight: 600;
  font-size: var(--fs-display); line-height: 1.16; letter-spacing: -.01em;
  margin: 0 0 var(--sp-3); max-width: 22ch; font-variant-numeric: normal;
}
h1 em { font-style: italic; }

/* The section rule, drawn ONCE. It used to be a border-top plus an ::after
   hairline 28px away, in two tokens dE 2.0 apart, with neither half visible. */
main section { margin-top: var(--sp-6); }
/* ...except the first one. .masthead is a section, so every page on the site
   opened with .page's own 44px of top padding AND 44px of section gap above the
   h1 — 88px of nothing before the first word, on all 74 pages. The gap belongs
   BETWEEN sections. */
main > section:first-child { margin-top: 0; }
main section > h2 {
  font-family: var(--mono);
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--quiet);
  margin: 0 0 var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--rule);
}
h3 { font-family: var(--mono); font-size: var(--fs-head); font-weight: 600; letter-spacing: -.01em; margin: 0 0 var(--sp-2); }
h4 { margin: 0 0 var(--sp-2); }

.lede, p, .note, .cav {
  font-family: var(--serif);
}
.lede { max-width: 62ch; margin: 0; font-size: var(--fs-prose); line-height: 1.5; color: var(--quiet); }
.lede b, .lede strong { color: var(--ink); font-weight: 600; }

.masthead { margin-bottom: var(--sp-6); }
.crumb { margin: 0 0 var(--sp-3); font-size: var(--fs-data); letter-spacing: .06em; font-family: var(--mono); }

p { margin: var(--sp-3) 0 0; max-width: 66ch; font-size: var(--fs-prose); line-height: 1.6; }
.note {
  color: var(--quiet); font-size: var(--fs-body); line-height: 1.6;
  max-width: 70ch; margin: var(--sp-3) 0 0;
}
.note strong { color: var(--ink); font-weight: 600; }

.cav {
  margin: var(--sp-4) 0 0; padding-left: var(--sp-3);
  border-left: 1px solid var(--rule);
  color: var(--quiet); font-size: var(--fs-body); line-height: 1.6; max-width: 72ch;
}

/* Season total, matches and rounds. A definition list, not a stat row. */
.facts { margin: var(--sp-4) 0 0; }
.facts > div {
  display: flex; flex-wrap: wrap; gap: 0 var(--sp-3); align-items: baseline;
  padding: var(--sp-1) 0; border-top: 1px solid var(--rule);
}
.facts > div:first-child { border-top: none; }
.facts dt { flex: none; min-width: 13ch; }
.facts dd { margin: 0; font-family: var(--mono); font-size: var(--fs-data); }

/* THE RAIL IS GONE. It drew a player's five ledger components as five capped dimension
   lines on one axis, and the five are a WALK: they start at the null and end at the
   headline, which a waterfall shows and a rail cannot. `.wf-*` below is what replaced it,
   and the null rule survives there — in --ink, at full strength, third instance. What is
   left in HTML is the ESTIMATE at cell scale (`.ival`, on a player's card) and the painted
   rule at zero inside a table (`td.z`, on the round ledger). */

/* ---- The interval, at cell scale and on a card ---------------------------- */
/* THE ESTIMATE, and after this round it is the only mark a table draws. The
   magnitude bar that used to sit beside every signed number is gone: the
   number is the number, and a bar restating it at a coarser precision was
   a second rendering of one fact. build.py positions the span and the point
   inline as percentages of one axis fixed for the whole site and already
   containing the null, so no cell's geometry is a function of another
   cell's value. */
.ival { position: relative; display: block; height: 16px; min-width: 88px; color: var(--hue, var(--quiet)); }
.ival::before { content: ""; position: absolute; left: 0; right: 0; top: 7.5px; height: 1px; background: var(--rule); }
.ival-span { position: absolute; top: 5px; height: 1px; background: currentColor; }
.ival-span::before, .ival-span::after {
  content: ""; position: absolute; top: -4px; width: 1px; height: 9px; background: currentColor;
}
.ival-span::before { left: 0; }
.ival-span::after { right: 0; }
.ival-point { position: absolute; top: 2px; height: 12px; width: 3px; margin-left: -1.5px; background: currentColor; }
.ival[data-clipped~="lo"] { border-left: 1px dotted var(--quiet); }
.ival[data-clipped~="hi"] { border-right: 1px dotted var(--quiet); }
.ival.none::before { background: repeating-linear-gradient(90deg, var(--rule) 0 3px, transparent 3px 6px); }


/* ---- Tables --------------------------------------------------------------- */
/* Wrapped in a focusable scroll region: a table that scrolls sideways and
   cannot be reached by keyboard is worse than one that wraps. */
.scroll, .table-wrap { overflow-x: auto; border: 1px solid var(--rule); border-radius: var(--r); }
.scroll:focus-visible, .table-wrap:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
table { width: 100%; border-collapse: collapse; background: var(--field); font-variant-numeric: tabular-nums; }

/* WORDS LEFT, NUMBERS RIGHT, and the markup has always said which is which:
   every numeric column on this site carries `.num` on its header and on every
   cell in it. The default was `right` for both, so the word columns were
   right-aligned against left-aligned headings — the ledger's names ran ragged
   under a left-aligned NAME, and on the round timeline `Round start` sat hard
   against `kill` with no left edge to scan. Checked before flipping it: across
   the 74 pages not one <td> without `.num` holds a number. `.l` still opts a
   cell left and now agrees with the default instead of fighting it. */
thead th {
  text-align: left; padding: var(--sp-3); border-bottom: 1px solid var(--rule);
  background: var(--well); white-space: nowrap;
}
thead th.num { text-align: right; }

tbody td {
  padding: 6px var(--sp-3); border-bottom: 1px solid var(--rule);
  text-align: left;
  font-family: var(--mono); font-size: var(--fs-data);
}
tbody td.num { text-align: right; }
/* nowrap on numeric cells only: a name or a duel-role list must be allowed to
   wrap rather than push a column off the right edge. */
tbody td .num, tbody td.n, tbody td time { white-space: nowrap; }
tbody td.l, tbody th[scope="row"] { text-align: left; font-weight: var(--w-body); }
tbody tr:last-child td { border-bottom: none; }
/* background-COLOR, never the `background` shorthand, and this is not a style
   preference. The null at zero is painted as a background-IMAGE on td.z three
   rules below; the shorthand resets background-image to none, and
   `tbody tr[data-focal] td` (0,1,3) outranks `td.z` (0,1,1), so ordering does
   not save it. Written as the shorthand, these three rules erased the one
   full-strength rule at the value that means NO CLAIM on exactly the rows
   belonging to the four people this site is about, and under the cursor on
   every other row. Anything added here paints a colour or it paints nothing. */
tbody tr:hover td { background-color: var(--well); }
/* The focal-row plate mixes toward TRANSPARENT, not toward --field. Over --field
   the two are the same colour to the byte; over a team wash they are not, and
   the opaque form painted the wash out from under every row belonging to the
   four — which on the round ledger is most of the rows, so the team wash would
   have survived only where nobody was looking. */
tbody tr.is-focal td, tbody tr[data-focal] td { background-color: color-mix(in srgb, var(--ink) 4%, transparent); }
tbody tr.is-focal:hover td, tbody tr[data-focal]:hover td { background-color: var(--well); }
tbody tr.absent td { color: var(--quiet); }

/* THE RULE AT ZERO, inside a table. The signed column is painted with a
   background gradient, so the null is one unbroken vertical running the full
   height of the table rather than a tick per cell. The cells' own bottom
   borders are cleared in this column so nothing interrupts it, and the header
   carries a bold 0 sitting exactly on it. This is the cheapest correct way to
   give a table the third sign channel, and it is the only device here that
   scales from four rows to ninety-nine unchanged. */
th.z, td.z {
  background-image: linear-gradient(to right,
    transparent calc(50% - .5px), var(--rule-strong) calc(50% - .5px),
    var(--rule-strong) calc(50% + .5px), transparent calc(50% + .5px));
  border-bottom-color: transparent;
}
th.z, td.z { text-align: center; width: 128px; padding-left: 0; padding-right: 0; }
th.z { color: var(--ink); font-size: var(--fs-data); }
tbody tr:hover td.z { background-color: var(--well); }

/* THE COLUMN GROUP. Eleven columns where two are the finding and five are the
   box score, so the reader is told which is which. The grouping is carried by
   a solid gutter cell rather than a border, because a border on a table that
   scrolls sideways slides out of view with the column it belongs to and the
   gutter does not. No colour, no rule, ten pixels. */
tr.group-head th {
  background: var(--field); border-bottom: 1px solid var(--rule);
  padding-bottom: var(--sp-1); padding-top: var(--sp-2);
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--quiet); text-align: left;
}
/* The gutter runs the full height of the table, so it is declared at a
   specificity that the focal-row and hover plates cannot outrank. A grouping
   device that breaks on four rows out of ten is not a grouping device. */
th.gutter { padding: 0; width: 10px; background: var(--well); }
tbody td.gutter,
tbody tr:hover td.gutter,
tbody tr[data-focal] td.gutter,
tbody tr.is-focal td.gutter { padding: 0; width: 10px; background-color: var(--well); border-bottom-color: transparent; }

/* ---- Team groups ----------------------------------------------------------
   The Team column is gone from the ten-player table and the round ledger. It
   was the same word, right-aligned, ten times, next to nine other columns that
   were all numbers — a fifth of the horizontal budget on a fact that changes
   twice down the page. It is now a <tbody> per side: the wash says where the
   break is at a glance and the group row SAYS THE NAME, so the fact is never in
   the wash alone. The wash goes on the tbody rather than on the cells because
   td backgrounds are already spoken for — the focal-row plate, the hover plate,
   the null gradient on td.z and the solid gutter all paint there, and a fifth
   claim on that property is how the gradient at zero got erased once already. */
.team-group { background-color: var(--team-other-wash); }
.team-group.is-focal { background-color: var(--team-focal-wash); }
/* The label row, not a second heading: it takes the one label style above and
   only geometry here. No wash of its own — it sits on its team's.
   The `tbody` prefix is load-bearing: `tbody th[scope="row"]` above sets
   font-weight 400 and outranks a bare `.team-head th`, which would have printed
   the one label style at body weight on this row and nowhere else.
   IT LOST ITS BOTTOM RULE, because that is what made it read as a second column
   header: the one label style on a full-width row with a 1px --rule under it,
   33px below a thead that is the one label style on a full-width row with a 1px
   --rule under it. A caption hangs above the rows it names and a header is
   fenced off from them, so the rule goes and the space above it doubles. */
tbody .team-head th {
  text-align: left; padding: var(--sp-4) var(--sp-3) var(--sp-2);
  border-bottom: none; background: none;
  font-weight: 600; white-space: nowrap;
}
/* tr:last-child is scoped to its own tbody, so the rule that clears the last
   row's border now fires once per team and the two sides ran together at the
   break. The team above a break keeps its bottom rule; only the last row of
   the last team loses one.
   THE BREAK IS A LINE, AND IT IS --rule-strong, because the wash cannot carry
   it. Both washes are mixes between --field and --well, and the whole distance
   between those two surfaces is a contrast ratio of 1.11: at 55% against 18%
   the step across the break measures 1.04, fainter than a zebra stripe and
   below anything a reader sees at a glance. The wash stays as the secondary
   cue and the edge is drawn in the token this sheet reserves for a line that
   carries information (3.66 dark, 3.84 light) — the same device
   .rail-row.is-scale uses to mark the break in the rail. It crosses the
   gutter: at (0,3,2) this outranks `tbody td.gutter`, so the line runs the full
   width of the table instead of stopping at the column group. */
.team-group:not(:last-child) tr:last-child td { border-bottom: 1px solid var(--rule-strong); }

/* AN AGENT PORTRAIT: identification beside a word that is always rendered,
   never an information mark. contrast.py measures 10 of 29 clearing 3:1 on
   the dark measurement surface and 26 of 29 on the light one -- disjoint
   sets, so no plate, filter or border rescues both ends. What clears the mark
   floor is the 1px --rule-strong BOX, at 3.42 dark and 4.28 light, which is
   why the slot reads as an object even when the picture inside it does not.
   The column is left-aligned so ten portraits form one scan column; right
   alignment would give them a ragged left edge, which is the opposite of the
   only reason to have them. */
/* ONE PLACEMENT FOR ALL THREE FAMILIES, set here and reused by .rank and
   .weapon: an 18px line box against 18px of art, so the picture and the word
   beside it centre on the same line and the word column's left edge runs
   straight down the table. Before this the containers inherited the body's
   1.55, which is 20.15px at 13px type against a 20px slot: every family found
   its own pixel of drift and the three of them found different ones. */
.agent { display: flex; align-items: center; justify-content: flex-start; gap: var(--sp-2); line-height: var(--icon-art); }
.agent-chip {
  flex: none; width: var(--icon-slot); height: var(--icon-slot);
  border: 1px solid var(--rule-strong); border-radius: var(--r);
  overflow: hidden; line-height: 0;
  display: flex; align-items: center; justify-content: center;
}
.agent-chip img { display: block; width: var(--icon-art); height: var(--icon-art); }

/* A RANK BADGE, AND IT IS NOT IN A BOX. The objection this slot answers, in
   full: Riot's tier art is hue-coded, and Gold sits OKLab ΔE 2.7–3.5 from
   --martin, a player's own hue. The site's rule is HUE IS A PERSON, so a
   coloured emblem is a collision -- IF the emblem is what tells you which rank.
   It never is here. The word ships in the same object, every time, and the
   badge is the recognition handle for it; house rule 4 already forbids colour
   alone. Two structural consequences, both enforced by where this class is
   allowed to appear: a badge is always inside .rank, which always contains the
   division word, and a badge never shares a cell with a mark in a player hue.
   THE BORDER IS GONE because it was answering a question this family never
   asked. contrast.py measures the art at 2.35 dark / 3.60 light median against
   the page surfaces, over the 1.70 hairline floor everywhere, so it reads on
   its own -- the agent portrait's 1px --rule-strong box exists because that
   family measures 10 of 29 clearing 3:1 in one theme and 26 of 29 in the other,
   and this one has no such split. What was left was a hairline square drawn
   around a picture that did not need one, on every row of every lobby table.
   The slot keeps its 18px WIDTH so the word column's left edge stays straight.
*/
.rank { display: inline-flex; align-items: center; gap: var(--sp-2); line-height: var(--icon-art); vertical-align: middle; }
.rank-chip {
  flex: none; width: var(--icon-art); height: var(--icon-art);
  overflow: hidden; line-height: 0;
  display: flex; align-items: center; justify-content: center;
}
.rank-chip img { display: block; width: var(--icon-art); height: var(--icon-art); }
/* THE EMPTY SLOT IS THE HONEST RENDERING OF "NO RANK ISSUED YET", which is 21.8%
   of this act's player-matches. It is not an em dash: the em dash on this site
   means NOT MEASURED and this is measured. It is not Riot's own string for the
   state either, because that string is the name of a QUEUE. So the slot is drawn
   and it is empty, in a dashed hairline rather than the solid --rule-strong of a
   badge that exists, beside the word `placement`. A reader sees a position on the
   ladder with nothing issued into it, which is exactly the fact. It is the one
   rank slot that is still drawn as a box, and now that a badge that EXISTS is
   not, the outline is doing the whole job on its own rather than differing from
   its neighbours by a dash pattern. */
.rank-chip.is-empty { border: 1px dashed var(--rule); border-radius: var(--r); }
/* The lobby range in a match masthead: two rungs, so two badges and two words,
   with the joining word between them at label weight. The placement count that
   may follow is a COUNT and gets no badge — a badge for the absence of a rank
   is the exact confusion cav.unrated_is_not_a_division exists to refuse. */
.rank-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); line-height: var(--icon-art); }
.rank-range .rank-to,
.rank-range .rank-also { color: var(--quiet); }

/* A WEAPON SILHOUETTE, and the one slot on this site with a fill in it. The art
   is near-white, so on the light theme it measures 1.53 against --field -- under
   the legibility floor. The plate is the fix and it does not move with the theme,
   so there is ONE number for this family (6.99) instead of a good one and a
   ghost. The box is wide rather than square because the art is: see --weapon-art-w.
   It ships in the `by weapon` breakdown, where every row is a DISTINCT weapon, and
   nowhere else -- the round ledger stays words, and quad/build.py carries the
   measurement that decided it. */
.weapon { display: flex; align-items: center; justify-content: flex-start; gap: var(--sp-2); line-height: var(--icon-art); }
.weapon-chip {
  flex: none; width: var(--weapon-slot-w); height: var(--icon-slot);
  border: 1px solid var(--rule-strong); border-radius: var(--r);
  background: var(--art-plate);
  overflow: hidden; line-height: 0;
  display: flex; align-items: center; justify-content: center;
}
.weapon-chip img { display: block; max-width: var(--weapon-art-w); max-height: var(--icon-art); }
/* `Other` is a grouping, not a weapon whose picture went missing, so it gets no box and no plate
   — only the width, which keeps the left edge of the word column straight. */
.weapon-chip.is-none { border-color: transparent; background: none; }

/* THE IDENTITY CONTROL, and the argument for every image on this site: flip it
   and not one fact leaves the page, because no picture on it carries a
   measurement. Every family is listed here, and a family that is not listed is a
   family that survives the switch — which is the test, so the list is the
   contract. Each of these three sits beside a word that is always rendered: the
   agent name, the division, the weapon name. */
:root[data-icons="off"] .agent-chip,
:root[data-icons="off"] .rank-chip,
:root[data-icons="off"] .weapon-chip { display: none; }
/* Inside the trajectory figure the badge is an SVG <image> and the empty slot an
   SVG <rect>, so both are hidden the same way and by the same attribute — one
   switch, not one per medium. The figure's LABEL GUTTER does not narrow when they
   go, because that geometry is arithmetic done at build time and there is no
   second layout pass; what is left is whitespace, and no fact is in it. */
:root[data-icons="off"] .rank-badge-svg,
:root[data-icons="off"] .fig .badge-box.is-empty { display: none; }

/* A DIVISION IS EIGHT CHARACTERS OF TYPE and that has not changed: the word is
   what encodes, on every surface, and the badge beside it is recognition.
   `placement` is a word, because the em dash on this site means NOT MEASURED and
   this is measured: no rank had been issued yet. */
.division { color: var(--ink); }
.division.is-placement { color: var(--quiet); border-bottom: 1px dotted var(--quiet); cursor: help; }

.focal { color: var(--hue, var(--ink)); font-weight: 600; }
/* THE CELL THAT HELD A LIST IS GONE, and with it the flex rule that spaced
   three hued chips inside one <td>. The front page's Focal players column is
   four COLUMNS now — one per player, empty where they did not play — so there
   is no list in a cell left to space, and the whitespace bug that rule fixed
   (`MartinLutherKing -2.09%SN0RLAX +6.92%`) cannot recur in a cell holding one
   number. */

/* THE CROSS-TAB. Four player columns, and the person is carried by the HEADING
   rather than by the cells: a cell is coloured by the SIGN of the number in it,
   which is the one thing this site never lets a hue mean. So the hue lives once
   per column, in the header, next to the name that encodes it — and the empty
   cells below a header get their meaning from the column they are in, which is
   the only thing an empty cell can be read against.

   The empty cell is genuinely empty: no glyph, no rule, no wash. A wash would
   be a value drawn for a match that was not played. */
table.idx-cross thead th .sort { color: var(--hue, inherit); }
/* WHERE AND WHEN ARE TWO COLUMNS. They used to be one unbreakable label — `Haven · 2026-08-06`
   — which made a scan for a date a scan through a map name. Neither may wrap: the table is
   already inside a horizontal scroller, which is the thing that absorbs a table too wide for
   the screen. The date is secondary to the map, which is the cell that names the match. */
table.idx-cross tbody td:nth-child(2),
table.idx-cross tbody td:nth-child(3) { white-space: nowrap; }
.idx-when { color: var(--quiet); }
.anon { color: var(--quiet); }
.team { font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .06em; }
/* The total row took none of the table's cell vocabulary — no padding, no data
   size, and `text-align` left at its initial value, which is `start` for a td
   and `center` for a th. On the round timeline that put the footer's 41.9% two
   hundred pixels left of the same figure in the 22 rows it totals, and centred
   `Round total · 22 nodes …` over the three columns it spans. A total that
   misses its own column is not a total. */
tfoot td, tfoot th {
  padding: 6px var(--sp-3); border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-data); font-weight: 600;
  text-align: left;
}
tfoot td.num, tfoot th.num { text-align: right; }
caption { caption-side: bottom; text-align: left; color: var(--quiet); font-size: var(--fs-body); font-family: var(--serif); padding-top: var(--sp-2); }

/* Rule 2: sign colour, and only on a value that has a direction. */
.pos { color: var(--up); }
.neg { color: var(--down); }
.zero, .na, .none { color: var(--quiet); }
/* A magnitude that rounds to zero at the field's own format is MEASURED. It
   prints as less-than the smallest number that format can write, with no sign
   character and no direction colour — never as an em dash, which is reserved for
   the unmeasured, and never as a direction claim it cannot support. */
.under { color: var(--quiet); }
.won { color: var(--up); }
.lost { color: var(--down); }
/* A draw is neither direction, so it takes neither direction's hue. It was unstyled:
   `match_result` started emitting this class for the act's one 15-15 match and nothing
   here was told, which left the one result word on the site that means "no winner"
   inheriting body colour and reading as an unfinished cell. The word is the fact either
   way -- `won`/`lost`/`drew` are printed, never coloured-only. */
.drew { color: var(--quiet); }

.thin, [data-thin="true"] { text-decoration: underline dotted var(--quiet); text-underline-offset: 3px; }

.chip {
  display: inline-block; font-family: var(--mono); font-size: var(--fs-label);
  letter-spacing: .06em; color: var(--quiet);
  border: 1px solid var(--rule); border-radius: var(--r); padding: 2px 7px;
}
button.chip { cursor: pointer; background: none; padding: 4px 9px; }
button.chip[aria-pressed="true"] { color: var(--ink); border-color: currentColor; }

/* The round ledger is 1,618px wide inside a scroller — it was already past the
   right edge — so letting the duel column wrap bought nothing and cost a lot:
   at 70px it broke "opening death · traded" over three lines and drove half
   the rows to 73px against 33px, which is what made the round sheet look
   ragged. The list stays on one line and the column takes the width it needs
   from a table that scrolls anyway. */
.duels { white-space: nowrap; }
.tag { font-family: var(--mono); font-size: var(--fs-label); color: var(--quiet); }
.tag + .tag { margin-left: var(--sp-1); }
.tag.none { cursor: help; }

.marker { font-family: var(--mono); font-size: var(--fs-label); color: var(--quiet); }

/* The sortable index. The whole row is the link and the link text says
   "Haven 13-8 · 25 Jun", so the address of a match is a sentence rather than
   a date in one column and a map in another. */
th[aria-sort] .sort-mark, th[data-sort] .sort-mark { color: var(--quiet); }
th[aria-sort="ascending"] .sort-mark::after { content: " \25B2"; }
th[aria-sort="descending"] .sort-mark::after { content: " \25BC"; }
th button.sort {
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
}
th button.sort:hover { color: var(--ink); }
.idx-filter { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: 0 0 var(--sp-3); }
.idx-count { font-family: var(--mono); font-size: var(--fs-label); color: var(--quiet); letter-spacing: .08em; text-transform: uppercase; }
tr[hidden] { display: none; }
/* background-COLOR, and the bar on the first cell only — this rule broke both
   halves of the warning forty lines up. Written as the shorthand it reset
   background-image at (0,2,2), outranking `td.z` at (0,1,1), so the j/k cursor
   erased the null gradient in the signed column on the one row the reader was
   looking at. And an inset on every td drew the 2px cursor bar six times across
   the row instead of once at its left edge. */
tbody tr.is-cursor td { background-color: var(--well); }
tbody tr.is-cursor td:first-child { box-shadow: inset 2px 0 0 var(--ink); }

/* ---- Figures, shared ------------------------------------------------------ */
.fig, .figure {
  border: 1px solid var(--rule);
  background: var(--field);
  border-radius: var(--r);
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  margin-top: var(--sp-4);
}
.fallback { margin: 0; color: var(--quiet); font-size: var(--fs-body); }
.fig .fallback { display: none; }
.fig-scroll { overflow-x: auto; }
.fig-scroll > svg { display: block; width: 100%; min-width: 720px; height: auto; }

.fig text { font-family: var(--mono); }
.fig .tick { fill: var(--quiet); font-size: 10.5px; }
.fig .axis-title { fill: var(--quiet); font-size: 10.5px; letter-spacing: .06em; }
/* Information, not decoration: a reader has to resolve a gridline to read a
   value off this figure, so it clears the 3:1 mark floor. */
.fig .gridline { stroke: var(--rule-strong); stroke-width: 1; stroke-dasharray: 4 4; }
.fig .axis { stroke: var(--rule-strong); stroke-width: 1; }
/* The null, in the tracker's unit. Drawn last on this figure and heavier than
   any series, because nothing on it can be hidden by it — the traces cross it
   at a point, not along it. */
.fig .axis-zero { stroke: var(--ink); stroke-width: 2.5; stroke-dasharray: none; }
.fig .hit { fill: transparent; cursor: pointer; }
.fig .empty { fill: var(--quiet); font-size: 12px; }

.fig-readout {
  font-family: var(--mono); font-size: var(--fs-data); color: var(--quiet);
  margin: var(--sp-3) 0 0; min-height: 2.6em; max-width: none;
}
.fig-readout b { color: var(--ink); font-weight: 600; }

.fig-data { margin-top: var(--sp-3); border-top: 1px solid var(--rule); padding-top: var(--sp-2); }
.fig-data > summary { cursor: pointer; list-style-position: outside; }
.fig-data > summary:hover { color: var(--ink); }
/* The only .fig-data left on the site is the match figure's node table. The
   ninety-nine-link list this also styled went with the tracker's dots. */
.fig-data .table-wrap { margin-top: var(--sp-3); }

/* ---- The rank trajectory --------------------------------------------------
   TWO PLOTS, ONE X AXIS, TWO Y SCALES — never one plot with two scales, which
   is the dual-axis anti-pattern. They share the match order and nothing else:
   own gridlines, own titles, own units, a gutter between them.

   The upper panel is the third instance of "a line in --ink is an exact path",
   in a third unit. The lower panel is the same per-match magnitude the rest of
   the site draws, in the player's own hue, against the same null. The reader
   is meant to notice that one rises and the other does not; the caption under
   it prints the correlation so the rise can never be read as a second opinion
   on the rating.

   THE PLACEMENT REGION HAS NO AXIS. Not a faint one, not a dashed box: no
   path, no gridline, no tick, because there was no rank to have. Its right
   edge is one dashed vertical and the emptiness is labelled. */
.fig-track { position: relative; }
.fig-track svg { display: block; width: 100%; height: auto; }
.fig-track svg:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.fig .paneltitle { fill: var(--quiet); font-size: 10.5px; letter-spacing: .13em; }
.fig .axlab { fill: var(--quiet); font-size: 10.5px; letter-spacing: .06em; }
.fig .axlab.is-null { fill: var(--ink); font-weight: 600; }
/* Solid, not dashed: a gridline is information and the only dash on this
   figure is the placement boundary, which is a threshold. */
.fig .grid { stroke: var(--rule-strong); stroke-width: 1; }
.fig .boundary { stroke: var(--quiet); stroke-width: 1; stroke-dasharray: 3 3; }
/* A step path on an ordinal axis lies ALONG its own gridlines by construction,
   so 2px of --ink over 1px of --rule-strong merges into one line and the path
   disappears for long stretches. The casing is the panel colour and breaks the
   gridline exactly where the path crosses it. */
.fig .track-case { fill: none; stroke: var(--field); stroke-width: 6; stroke-linejoin: miter; }
.fig .track { fill: none; stroke: var(--ink); stroke-width: 2; stroke-linejoin: miter; stroke-linecap: butt; }
.fig .track-dot { fill: var(--ink); }
.fig .bar { stroke: var(--hue, var(--quiet)); stroke-width: 3; }
.fig .null { stroke: var(--ink); stroke-width: 3; }
.fig .cross { stroke: var(--chart-crosshair); stroke-width: 1; stroke-dasharray: 3 3; }
.fig .hitbar { fill: transparent; }
/* THE ONLY RANK BOX LEFT ON THE SITE, and it is the one that has nothing in it.
   A division badge that EXISTS is bare in both media: .rank-chip dropped its
   rule and the figure stopped emitting a rect at all, so the badge measures
   --icon-art in SVG and --icon-art in HTML and sits one --sp-2 from its word in
   both. What is drawn here is the placement prefix: a rung with nothing issued
   into it, dashed and in --rule, exactly as .rank-chip.is-empty is in HTML, so
   the two say the same thing in the two media. Its hairline is inside the 18,
   which is what border-box does to the HTML one. It switches off with the rest
   of the badge column, because the fact is in the words beside it and an empty
   box is not a fact on its own. */
.fig .badge-box.is-empty { fill: none; stroke: var(--rule); stroke-width: 1; stroke-dasharray: 3 2; }

.fig-track-readout {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4);
  font-family: var(--mono); font-size: var(--fs-data); color: var(--quiet);
  margin: var(--sp-3) 0 0; max-width: none; min-height: 2.4em;
}
.fig-track-readout b { color: var(--ink); font-weight: 600; }

/* ---- The tracker ---------------------------------------------------------- */
.fig-legend { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0 0 var(--sp-3); }
.legend-key {
  display: flex; align-items: center; gap: var(--sp-2);
  background: none; border: 1px solid var(--rule); border-radius: var(--r);
  padding: 5px 10px 5px 7px; cursor: pointer; color: var(--quiet);
  transition: border-color .18s ease, color .18s ease;
}
.legend-key:hover { border-color: var(--ink); color: var(--ink); }
.legend-key[aria-pressed="true"] { border-color: currentColor; color: var(--ink); }
.legend-key .swatch { width: 26px; height: 14px; flex: none; display: block; }
.legend-key .swatch .cum { stroke: var(--hue, var(--quiet)); stroke-width: 2; }
.legend-key .swatch .dot { fill: var(--hue, var(--quiet)); }
.legend-key.p-themarias .swatch .cum { stroke-dasharray: 5 3; }
.legend-key.p-trzzcko .swatch .cum { stroke-dasharray: 1.5 3; }
.legend-key .who { font-weight: var(--w-strong); color: var(--hue, var(--ink)); font-size: var(--fs-data); }
.legend-key .exposure { font-family: var(--mono); font-size: var(--fs-label); color: var(--quiet); }

.fig-legend.is-static { margin-bottom: var(--sp-3); }
.legend-key.is-static { cursor: default; border-color: var(--rule); }
.legend-key.is-static:hover { border-color: var(--rule); color: var(--quiet); }
.legend-key.is-static .swatch { overflow: visible; }
/* The match figure's per-player keys, beside the three that say what a KIND of
   segment means. The swatch is drawn by the same class as the mark it stands
   for — `.wpfig .wp-act` already strokes it in the key's own --hue — so all
   this adds is a weight that fits a 26x14 box: 4px of stroke in fourteen is a
   third of the swatch. The initial rides in it exactly as it does on the plot,
   because that pairing is the thing the key has to teach. */
.legend-key .swatch .wp-act { stroke-width: 3; }

.tracker .series .cum {
  fill: none; stroke: var(--hue, var(--quiet));
  stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round;
}
.tracker .p-themarias .cum { stroke-dasharray: 5 3; }
.tracker .p-trzzcko .cum { stroke-dasharray: 1.5 3; }
.tracker .leader { stroke: var(--hue, var(--quiet)); stroke-width: 1; stroke-dasharray: 2 2; }
.tracker .end-name { fill: var(--hue, var(--ink)); font-size: 11px; font-weight: 600; }
.tracker .end-val { font-size: 11px; fill: var(--quiet); }
.tracker .end-val.pos { fill: var(--up); }
.tracker .end-val.neg { fill: var(--down); }
/* A player with no match yet at the pointer's position has no running mean; the label says
   NOT MEASURED rather than borrowing a number from that player's future. */
.tracker .end-val.none { fill: var(--quiet); }
/* One nearest-index crosshair band across the plot height, replacing the 99
   individual 12px hit circles — 87 of which had a neighbour inside 24px and
   every one of which was tabindex="-1". Every match is still addressable, in
   the sortable index below and in the list under the figure. */
.tracker .band { fill: var(--ink); fill-opacity: .07; }
.tracker .cross-v { stroke: var(--chart-crosshair); stroke-width: 1; stroke-dasharray: 3 3; }

/* THE READOUT, AS A TABLE. Four players at three quantities each was eleven middots on one
   wrapping line; a reader comparing two of them was comparing two substrings of a paragraph.
   The table is not in a `.scroll` and takes none of the site's table chrome — it is a readout
   that happens to have columns, so it sits on the figure's own surface with no border and no
   header wash, and only the alignment is borrowed. It holds its height at four rows so the
   figure does not jump as the pointer crosses a match nobody played. */
.tracker-read { min-height: 10.2em; }
.tracker-rest { margin: 0; max-width: 70ch; font-family: var(--serif); font-size: var(--fs-body); }
.tracker-tbl { width: auto; min-width: min(100%, 460px); background: none; }
.tracker-tbl caption {
  caption-side: top; padding: 0 0 var(--sp-2); font-family: var(--mono);
  font-size: var(--fs-data); color: var(--ink); font-weight: 600;
}
.tracker-tbl th, .tracker-tbl td {
  padding: 3px var(--sp-3) 3px 0; border-bottom: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-data); background: none;
  white-space: nowrap;
}
.tracker-tbl thead th {
  padding-top: 0; background: none; border-bottom: 1px solid var(--rule);
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--quiet);
}
.tracker-tbl .n { text-align: right; }
.tracker-tbl .l { text-align: left; }
.tracker-tbl tbody th { font-weight: 600; color: var(--hue, var(--ink)); }
.tracker-tbl tbody tr:last-child th, .tracker-tbl tbody tr:last-child td { border-bottom: none; }
.tracker-tbl tbody tr:hover th, .tracker-tbl tbody tr:hover td { background: none; }

.fig-tracker[data-only="martin"] .series:not(.p-martin),
.fig-tracker[data-only="snorlax"] .series:not(.p-snorlax),
.fig-tracker[data-only="themarias"] .series:not(.p-themarias),
.fig-tracker[data-only="trzzcko"] .series:not(.p-trzzcko) { opacity: .22; }

/* APPEND to quad-site.css, after the tracker block and before "---- The match
   figure ----". Every new class is od- prefixed. No new colour literal: the
   panels ink a cell by OPACITY on the player's own --hue. */

/* ---- Offense against defense ----------------------------------------------
   ONE POINT PER PLAYER-MATCH ON A SQUARE, SYMMETRIC PLANE, then that same plane
   binned four a side, once per player. Both halves are the same unit, so the box
   is square: that is what makes the x = -y guide a true 45 degrees and each
   quadrant a true quarter.

   THE DENSITY IS A FIELD, NOT A GRID. It is drawn under the marks on the same
   plane, so the shape of a season and the match that made it are one figure. */
.od-plot > svg { display: block; width: 100%; max-width: 680px; height: auto; margin: 0 auto; }
.od-plot > svg:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* The pointer appears only while a mark is live, which is exactly when a click
   opens that match. */
.od-scatter.is-live { cursor: pointer; }

/* Structure, not a datum: the locus of a match worth nothing is a property of
   the plane, so it is --rule, a hairline, and under every mark. */
.od-fig .od-guide { stroke: var(--rule); stroke-width: 1; stroke-dasharray: 5 4; }
.od-fig .od-guide-lab { fill: var(--quiet); font-size: 10.5px; }
.od-fig .od-quad { fill: var(--quiet); font-size: 10.5px; letter-spacing: .06em; }
/* A CASING, the same device `.track-case` uses on the trajectory: these four words sit in the
   corners of a plot whose corners hold real marks, and a datum crossing a letter costs the
   reader both. `paint-order: stroke` puts the panel colour under the type without moving it,
   so nothing is hidden — a mark under a label is still drawn, with a hairline of air round the
   glyph it would otherwise share a pixel with. */
.od-fig .od-quad text, .od-fig .od-guide-lab {
  paint-order: stroke; stroke: var(--field); stroke-width: 3px; stroke-linejoin: round;
}
/* The hairline is the panel colour and it is what keeps 151 marks separable
   where they overplot. Shape carries the series as well as hue — rule 3. */
.od-fig .od-series .od-dot {
  fill: var(--hue, var(--quiet)); stroke: var(--field); stroke-width: .8;
}
.od-fig .od-focus { fill: none; stroke: var(--ink); stroke-width: 1.5; }

.od-fig[data-only="martin"] .od-series:not(.p-martin),
.od-fig[data-only="snorlax"] .od-series:not(.p-snorlax),
.od-fig[data-only="themarias"] .od-series:not(.p-themarias),
.od-fig[data-only="trzzcko"] .od-series:not(.p-trzzcko) { opacity: .22; }

/* ---- the density blob ------------------------------------------------------
   THE FOUR BINNED PANELS ARE GONE and this is what does their job, on the plane
   the marks are already on. Each match is a radial gradient in that player's
   hue, transparent at its edge, and overlapping splats accumulate — so the ink
   IS the density and the fade IS the probability falling to nothing. No cell,
   no contour level, no threshold.

   THE HUE COMES FROM THE CLASS ON THE GRADIENT, not from a literal, so all four
   fields move with the theme and contrast.py has nothing new to guard: the
   stops set opacity only. The ceiling is 0.36 at the centre of one splat, which
   is under the tone the panels topped out at, because these overlap and a
   season can stack a dozen of them in the same place.

   IT IS UNDER EVERYTHING. The blob layer is drawn before the marks and before
   the two null rules, so no field ever paints over a datum, a gridline or the
   value that means no claim. */
.od-fig .od-blob circle { pointer-events: none; }
.od-fig .od-kernel stop { stop-color: var(--hue, var(--quiet)); }

.od-read {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-1) var(--sp-4); min-height: 5.4em;
}
.od-q { white-space: nowrap; }
.od-k { color: var(--quiet); }
/* Weight only. The sign colour belongs to .pos/.neg/.zero, which are declared
   above this and would lose to a colour set here. */
.od-v { font-weight: 600; }

/* Four players at two quantities is a comparison down a column, not a sentence
   with middots in it — the same argument .tracker-tbl is built on. */
.od-tbl { width: auto; min-width: min(100%, 420px); background: none; }
.od-tbl caption {
  caption-side: top; padding: 0 0 var(--sp-2); font-family: var(--mono);
  font-size: var(--fs-data); color: var(--ink); font-weight: 600;
}
.od-tbl th, .od-tbl td {
  padding: 3px var(--sp-3) 3px 0; border-bottom: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-data); background: none;
  white-space: nowrap;
}
.od-tbl thead th {
  padding-top: 0; background: none; border-bottom: 1px solid var(--rule);
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .13em;
  text-transform: uppercase; color: var(--quiet);
}
.od-tbl .n { text-align: right; }
.od-tbl .l { text-align: left; }
.od-tbl tbody th { font-weight: 600; color: var(--hue, var(--ink)); }
.od-tbl tbody tr:last-child th, .od-tbl tbody tr:last-child td { border-bottom: none; }
.od-tbl tbody tr:hover th, .od-tbl tbody tr:hover td { background: none; }

/* ---- The match figure ----------------------------------------------------- */
/* ONE CONTINUOUS WIN PROBABILITY CURVE. Every vertical is one credited action
   and the height of that vertical is exactly that action's match impact:
   dP/dq is the round's leverage, measured to 3.6e-11 in the payload. The
   chart and the metric are the same object, which is why nothing here may
   round a corner off it. */
.fig-match .wp-head { margin: 0 0 var(--sp-4); }
.wp-head-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.wp-head-top .lab { display: inline; }
.wp-when { font-family: var(--mono); font-size: var(--fs-label); color: var(--quiet); letter-spacing: .06em; white-space: nowrap; }
.wp-teams { margin-top: var(--sp-2); }
.wp-team {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: baseline; gap: 0 var(--sp-3);
  padding: var(--sp-2) 0; border-top: 1px solid var(--rule);
}
.wp-team:first-child { border-top: none; }
/* The two live percentages are the figure's headline and were set two steps below the
   section head that names them. --fs-display for both, because the pair is only readable as
   a comparison if it is one size; the trailing side still drops to --quiet at --w-body,
   which is where the sign lives. */
.wp-name { font-family: var(--mono); font-size: var(--fs-body); font-weight: 600; letter-spacing: .04em; overflow-wrap: anywhere; }
.wp-score { font-family: var(--mono); font-size: var(--fs-head); color: var(--quiet); }
.wp-pct { font-family: var(--mono); font-size: var(--fs-display); font-weight: 600; letter-spacing: -.01em; min-width: 6.5ch; text-align: right; }
.wp-pct.ahead { color: var(--ink); }
.wp-pct.behind, .wp-pct.level { color: var(--quiet); font-weight: 400; }

.wp-plot { position: relative; margin-top: var(--sp-3); }
/* 860, up from the shared 720. Every radius, stroke width and font size in this figure is
   authored in viewBox units, and at 720 they all rendered at 0.75 — an 11px initial arrived
   as 8px. At 860 nothing is drawn smaller than it was designed; the container already
   scrolls and is labelled as doing so. The tracker keeps the shared floor. */
.wp-plot .fig-scroll > svg { min-width: 860px; }
.wpfig { display: block; }
.wpfig .tick { fill: var(--quiet); }
.wpfig .gridline { stroke: var(--rule-strong); stroke-width: 1; stroke-dasharray: 4 4; }
/* THE NULL, second instance: 50.0% match win probability, the point at which
   we cannot say who wins. It is the heaviest horizontal on the figure — and
   it is drawn UNDER the curve, not over it. Drawn last it would erase the
   data it is judging at exactly the moment the match was closest. */
.wpfig .even { stroke: var(--ink); stroke-width: 2.5; stroke-dasharray: none; }
/* THE CHIP THAT NAMED IT IS GONE, and the rule is not. `THE NULL · 50.0%` was
   type printed over the data on all 68 match pages to label the heaviest,
   longest horizontal on a figure whose y axis is already ticked in per cent —
   the reader who needs to be told what 50% means is not helped by being told it
   in the middle of the plot. The rule keeps every line above it unchanged. */
/* Boundaries are 7px ticks at the axis, not 24 to 32 full-height rules. Those
   turned the plot into a barcode over its own data. */
/* 4px at every boundary, 9px where the axis prints a number. All 25 at 7px was a picket
   fence competing with its own data; the labelled tick is the one the eye lands on, and in
   time mode that tick is a minute. */
.wpfig .bound { stroke: var(--rule-strong); stroke-width: 1; }
.wpfig .bound.wpx-major { stroke: var(--quiet); }
.wpfig .switch { stroke: var(--chart-segment-line); stroke-width: 1.5; }
.wpfig .switch-lab { fill: var(--quiet); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
.wpfig .ot-bracket { stroke: var(--rule-strong); stroke-width: 1; fill: none; }

/* The one fill in the system. It carries no side meaning: it means distance
   from the null, which is the thing the figure is about. */
.wpfig .area-wash { fill: var(--wash); }
.wpfig .wp-band { fill: var(--ink); fill-opacity: .06; }

.wpfig .wp-line {
  fill: none; stroke: var(--chart-line-primary); stroke-width: 2.5;
  stroke-linejoin: miter; stroke-linecap: butt;
}
/* THE PART OF THE LINE ONE OF THE FOUR MOVED. An action's vertical IS its
   match impact — the chart and the metric are the same object — so when the
   actor is one of the four, that vertical is drawn again in their hue on top of
   the ink line. It is heavier than .wp-line rather than lighter because it is
   the thing being claimed: the reader asks "who moved this match" and the
   answer is now readable off the curve without hunting for a 5px disc. It
   overprints the ink exactly, so the line's path is unchanged; only its colour
   is, and only where a hue means a person. */
.wpfig .wp-act {
  fill: none; stroke: var(--hue, var(--ink)); stroke-width: 4;
  stroke-linejoin: miter; stroke-linecap: butt;
}
.wpfig .wp-free { fill: none; stroke: var(--chart-segment-line); stroke-width: 2; stroke-dasharray: 6 4; stroke-linecap: butt; }
.wpfig .wp-econ { stroke-dasharray: 6 4; stroke-width: 2; }
.wpfig .wp-term { stroke-dasharray: 1.5 2.5; stroke-width: 1.4; stroke-linecap: round; }
.wpfig .wp-close { stroke-dasharray: none; stroke-width: 1.4; stroke-linecap: butt; }

/* AN ACTION'S MARKER. Radius is |dp| against a fixed axis from the payload. TWO levels, not
   three: a filled hue mark for one of the four, a hollow neutral ring for anyone else past the
   payload's gate, and nothing at all for the rest — the vertical in the line is already their
   mark, and 145 two-pixel ticks a match at three units apart was a carpet drawn over the
   finding.

   THE STROKE IS A HALO, NOT AN OUTLINE. `paint-order: stroke` puts the field colour UNDER the
   fill, so the mark keeps its full radius and gains a 1.2-unit gap against whatever it grazes
   — the same trick `.fig .track-case` uses to break a gridline. */
.wpfig .ev {
  fill: var(--hue, var(--quiet)); stroke: var(--field); stroke-width: 2.4;
  paint-order: stroke;
}
.wpfig .ev.no-actor, .wpfig .ev.other { fill: none; stroke: var(--quiet); stroke-width: 1.8; }
.wpfig .ev.over { stroke: var(--chart-line-primary); }
/* The per-player initial inside the disc: the figure's only per-player
   secondary channel, and the palette measures 10.2 at deutan, which is legal
   only because a channel like this exists. A channel nobody can read is not a
   channel — at 7px the cap height was 4.9px and the owner could not tell an M
   from an S. At 9px it is 6.3px, which is why the marker radii went up with it:
   a disc has to reach r 4.4 to case a glyph that tall, and the figure now draws
   the hue-carrying markers past that. */
/* 9px gave a 6.3px cap inside a 9px mark and the owner could not read it; 11px in a mark
   that is now at least 13 across can be read at arm's length, which is the whole point. */
.wpfig .ev-init { fill: var(--field); font-size: 11px; font-weight: 700; letter-spacing: 0; }

/* THE RISER. At the selected action the vertical move is drawn as an
   engineering dimension line — serifs at both ends, the value in a box on a
   leader — and that value is the number the ledger row below credits to the
   last digit. Everywhere else a chart is a view of a metric and this would be
   decoration; here the chart and the metric are the same object, and this is
   the only mark that says so. */
.wpfig .riser { stroke: var(--ink); stroke-width: 1; }
.wpfig .riser-serif { stroke: var(--ink); stroke-width: 1.4; }
.wpfig .riser-box { fill: var(--field); stroke: var(--ink); stroke-width: 1; }
.wpfig .riser-val { fill: var(--ink); font-size: 11px; font-weight: 600; }

.wpfig .cross-v { stroke: var(--chart-crosshair); stroke-width: 1; stroke-dasharray: 3 3; }
.wpfig .cross-dot { fill: none; stroke: var(--ink); stroke-width: 1.5; }

.wp-tip {
  position: absolute; z-index: 3; pointer-events: none;
  min-width: 168px; max-width: 280px;
  background: var(--well); color: var(--chart-tooltip-foreground);
  border: 1px solid var(--rule); border-radius: var(--r);
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--mono); font-size: var(--fs-label); line-height: 1.5;
}
.wp-tip-head { color: var(--quiet); }
.wp-tip-head b { color: var(--chart-tooltip-foreground); font-weight: 600; }
.wp-tip-what { margin-top: 2px; color: var(--chart-tooltip-foreground); }
.wp-tip-who { margin-top: 2px; color: var(--quiet); overflow-wrap: anywhere; }
.wp-tip-rows { margin: var(--sp-2) 0 0; }
.wp-tip-rows > div { display: flex; justify-content: space-between; gap: var(--sp-3); border-top: 1px solid var(--rule); padding: 2px 0; }
.wp-tip-rows dt { color: var(--quiet); }
.wp-tip-rows dd { margin: 0; }

/* ---- Round tabs ----------------------------------------------------------- */
.round-strip, .round-tabs {
  display: flex; flex-wrap: wrap; gap: var(--sp-1);
  margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--rule);
}
.round-tabs button, .round-tab {
  display: inline-block; min-width: 30px; text-align: center;
  background: none; cursor: pointer; text-decoration: none;
  border: 1px solid var(--rule); border-radius: var(--r);
  color: var(--quiet); font-family: var(--mono); font-size: var(--fs-data);
  padding: 4px 7px; line-height: 1.2;
  transition: color .18s ease, border-color .18s ease;
}
.round-tabs button:hover, .round-tab:hover { color: var(--ink); border-color: var(--ink); }
.round-panel { margin-top: var(--sp-5); }
.round-tab.is-on, .round-tabs .is-on, .round-tabs [aria-selected="true"],
.round-strip [aria-pressed="true"], .round-strip [aria-current="true"] {
  color: var(--ground); background: var(--ink); border-color: var(--ink);
}
/* ---- The round timeline ---------------------------------------------------
   The win probability curve for one round, as rows: what happened, in order,
   and what each thing was worth. It takes the whole table vocabulary from the
   element selectors — there is not one cell rule here — because it is the same
   ledger as .round-ledger read down a different axis, and a second spelling of
   a table is how two tables stop agreeing.

   The one thing it needs that the ledger does not: THREE OF THE FOUR KINDS
   BELONG TO NOBODY. A round start is the economy, a clock row is time running
   out on a side, a terminal is the gap between the model and the result that
   actually landed. Only an action has an actor, and only an action is a thing
   one of the four did — so the other three go to --quiet, by the same device
   `tbody tr.absent` already uses. The signed numbers keep their own colour:
   the row is unattributed, not unmeasured. */
/* Two scroll regions in one panel now — the ledger and the timeline — and the
   table inside each is a border-collapse table whose own margin does nothing.
   The gap belongs to the regions. */
.round-panel .scroll + .scroll { margin-top: var(--sp-4); }
/* AND EACH ONE SAYS WHICH IT IS. Two bordered grids stacked 18px apart, both
   opening on the one label style over --well, read as one table with a seam in
   it. The name each needs is already in the markup and already correct — the
   region's aria-label, `Round 1, in order` and `Round 1 ledger` — and the only
   reader who was not being told was the one looking at it. So it is printed,
   from the attribute rather than from a second copy that could drift, on a
   title bar in the table's own --field above the --well header row: bar, then
   header, then body, three surfaces deep, which is the hierarchy the panel was
   missing. It stands down if a table ever ships a real <caption>, so this and
   build.py's caption argument cannot both fire. `sticky` keeps the bar at the
   left edge of the 1,618px ledger while it scrolls under it. */
.round-panel .scroll[aria-label]:not(:has(caption))::before {
  content: attr(aria-label);
  display: block; position: sticky; left: 0;
  padding: var(--sp-2) var(--sp-3);
  background: var(--field); border-bottom: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--quiet);
}
.round-timeline tr[data-kind="round_start"] td,
.round-timeline tr[data-kind="clock"] td,
.round-timeline tr[data-kind="terminal"] td { color: var(--quiet); }

.round-outcome { font-size: var(--fs-data); font-weight: 600; margin-left: var(--sp-2); }

/* ---- Lateral navigation between match pages -------------------------------
   Every match page and, until this round, zero links between any two of
   them: a reader working through an eight-match evening returned to a
   hundred-row index each time and re-found their place. Two links and a
   position. No thumbnail curve — at 68x20 in --quiet a win probability curve
   is a hairline scribble at 1x, and the link label already says which match
   it is. */
.match-rail {
  display: flex; align-items: baseline; gap: var(--sp-3);
  margin: var(--sp-3) 0 0; padding-top: var(--sp-3);
  border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-data);
}
.match-rail .where { flex: none; order: 2; color: var(--quiet); font-size: var(--fs-label); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.match-rail .step { flex: 1 1 0; min-width: 0; color: var(--quiet); text-decoration-color: color-mix(in srgb, currentColor 45%, transparent); }
.match-rail .step:hover { color: var(--ink); }
.match-rail .step.prev { order: 1; }
.match-rail .step.next { order: 3; text-align: right; }
.match-rail .step.is-end { color: var(--rule); cursor: default; text-decoration: none; }

/* ---- Top plays ------------------------------------------------------------ */
.plays { list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.play { border-top: 1px solid var(--rule); padding: var(--sp-3) 0; }
.play:first-child { border-top: none; }
.play-what { margin: 0; font-family: var(--mono); font-size: var(--fs-data); }
.play-num { margin: var(--sp-1) 0 0; font-family: var(--mono); font-size: var(--fs-head); }
/* The paragraph of context under each swing is gone; what is left is the score
   entering the round, which is the one thing the number above it is read against. */
.play-at { color: var(--quiet); font-family: var(--mono); font-size: var(--fs-label);
  letter-spacing: .08em; text-transform: uppercase; }

/* ---- Player page ---------------------------------------------------------- */
.headline { margin-top: var(--sp-4); }
.big { font-family: var(--mono); font-size: var(--fs-display); font-weight: 600; letter-spacing: -.01em; line-height: 1.1; margin: 0; }
.big .unit { display: block; font-size: var(--fs-label); letter-spacing: .13em; text-transform: uppercase; margin-top: var(--sp-1); }
.ival-text { font-size: var(--fs-label); color: var(--quiet); margin-top: var(--sp-1); font-family: var(--mono); }
.breakdown-block { margin-top: var(--sp-5); }

/* ============================================================================
   THE CARD.

   One structural claim, and every rule below serves it: THERE ARE TWO KINDS OF
   NUMBER ON THIS PAGE AND THEY ARE NOT THE SAME KIND OF THING. On the left an
   estimate — impact per match, with its span on the payload's fixed axis and
   the null running through it. On the right counts — kills, deaths, assists,
   rounds. A kill happened; an impact per match is a guess with a width.

   The device that says so is ONE HAIRLINE, the site's own --rule, with a word
   at the top of each side. That is the whole ornament. There is no shadow, no
   radius above 2px, no gradient and no second colour: the card is made of the
   same four tokens every table on this site is made of, and what makes it a
   card is the arrangement, not a treatment.

   THE RIGHT-HAND SIDE HAS NO AXIS ON IT. Not a bar, not a span, not a rule at
   zero. An axis is the vocabulary of the left, and putting a kill count on one
   would be the exact category error the divider exists to prevent.
   ========================================================================= */
.card {
  margin-top: var(--sp-4);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--field);
}
/* The hue is a 3px stripe along the top edge and it is the only decoration on
   the object. Hue is a person: this says whose card it is, in the one channel
   the site reserves for that, and it carries no measurement — the name is
   directly under it, so nothing here is encoded by colour alone. */
.card::before {
  content: ""; display: block; height: 3px;
  background: var(--hue, var(--rule-strong));
}
.card-face { display: grid; grid-template-columns: 1fr 1fr; }
.card-reg-est, .card-reg-cnt { padding: var(--sp-4); min-width: 0; }
.card-reg-cnt { border-left: 1px solid var(--rule); }

/* The two words that name the registers. Same label style as every heading on
   the site, so they read as structure rather than as a badge. */
.card-reg {
  font-family: var(--mono); font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--quiet);
  margin: 0 0 var(--sp-3);
}
/* --quiet, not --rule-strong. This is TEXT — "9 more" is a fact about how many
   agents are not on the card — and --rule-strong is guarded at the 3:1 MARK
   floor, where it measures 3.42 dark and 4.28 light against --field. Both are
   under 4.5. --quiet is the token that carries secondary text on this site and
   is guarded as text: 7.20 and 6.55. */
.card-more { color: var(--quiet); letter-spacing: .06em; }
/* THE INTERVAL IS OFF THE CARD, and so are the span, the axis under it and the sentence
   beside it — three renderings of one width on the one object built to be glanced at. The
   width is still exact under the waterfall and in every breakdown table on the page. */

/* THE THREE STRONGEST CUTS, one per dimension, and the one figure on this card that is not
   per match — which is why the label above them names its unit. The dimension is the quiet
   column and the cell's own name is the loud one: the reader is scanning for `Haven`, not for
   the word `Map`. A thin cell keeps the dotted underline the table below gives it. */
.pc-best { list-style: none; margin: 0; padding: 0; }
.pc-best li {
  display: grid; grid-template-columns: 5.5em 1fr auto;
  align-items: baseline; gap: var(--sp-2);
  padding: 3px 0; border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: var(--fs-data);
}
.pc-best li:first-child { border-top: none; }
.pc-best-d { color: var(--quiet); font-size: var(--fs-label); letter-spacing: .1em; text-transform: uppercase; }
.pc-best-k { color: var(--ink); font-weight: var(--w-strong); overflow-wrap: anywhere; }
.pc-best-k.thin { text-decoration: underline dotted var(--quiet); text-underline-offset: 3px; }

/* THE SEASON, IN ONE LINE, and it is the raw datum rather than the running mean the front
   page draws. The card had no sense of the spread behind its big number; this is that spread,
   with no axis and no ticks because the only value it claims is the null under it. Every one
   of these matches is printed exactly in the table below. */
.pc-spark { padding: var(--sp-4) var(--sp-5); border-top: 1px solid var(--rule); }
.pc-spark .lab { margin: 0 0 var(--sp-2); }
.pc-spark svg { display: block; width: 100%; max-width: 420px; height: auto; }
.pc-spark-line { fill: none; stroke: var(--hue, var(--quiet)); stroke-width: 1.4; stroke-linejoin: round; }
/* The null, at the weight it has everywhere else on this site relative to its figure. */
.pc-spark-null { stroke: var(--ink); stroke-width: 1.2; }
.pc-spark-end { fill: var(--hue, var(--quiet)); stroke: var(--field); stroke-width: 1; }

/* THE SCOREBOARD LINE. 15 / 15 / 5 is how this game writes a match on its own
   scoreboard, so the act is written the same way and the per-match qualifier is
   said once underneath instead of three times beside. Three stat rows would
   have been the obvious answer and this is faster to read, because it is the
   form the reader already owns. */
.kda { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0; margin: 0; font-family: var(--mono); }
.kda-part { display: flex; flex-direction: column; }
.kda-n { font-size: var(--fs-display); font-weight: 600; letter-spacing: -.01em; line-height: 1.1; }
.kda-k { font-size: var(--fs-label); letter-spacing: .13em; text-transform: uppercase; color: var(--quiet); margin-top: var(--sp-1); }
.kda-slash { font-size: var(--fs-head); color: var(--rule-strong); align-self: flex-start; padding: 0 var(--sp-2); line-height: 1.5; }
.card-per { margin: var(--sp-3) 0 0; font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .06em; color: var(--quiet); }

/* The identity strip: agents and guns, with the art at its own intrinsic size.
   It is under both registers because it belongs to neither — it is not an
   estimate and it is not the box score, it is who this person plays as. */
.card-strip {
  display: grid; grid-template-columns: 1fr 1fr;
  border-top: 1px solid var(--rule);
}
.card-strip-block { padding: var(--sp-4); min-width: 0; align-content: start; }
.card-strip-block + .card-strip-block { border-left: 1px solid var(--rule); }
.card-art { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-4); }
.card-art li { display: flex; flex-direction: column; gap: var(--sp-1); }
.card-art-n { font-family: var(--mono); font-size: var(--fs-data); color: var(--quiet); }
.card-art-n .unit { text-transform: uppercase; letter-spacing: .1em; }
.agent.is-card, .weapon.is-card { line-height: 1.2; font-family: var(--mono); font-size: var(--fs-data); }
.agent-chip.is-card { width: var(--icon-card-slot); height: var(--icon-card-slot); }
.agent-chip.is-card img { width: var(--icon-card); height: var(--icon-card); }
.weapon-chip.is-card { width: var(--weapon-card-slot-w); height: calc(var(--weapon-card-h) + 2px); }
.weapon-chip.is-card img { max-width: var(--weapon-card-w); max-height: var(--weapon-card-h); }

/* One column below the point where two 34px numbers stop fitting side by side.
   The hairline becomes horizontal and keeps doing the same job. */
@media (max-width: 760px) {
  .card-face, .card-strip { grid-template-columns: 1fr; }
  .card-reg-cnt, .card-strip-block + .card-strip-block {
    border-left: none; border-top: 1px solid var(--rule);
  }
}

/* ---- The ledger waterfall -------------------------------------------------
   FIVE MOVES AND ONE ESTIMATE, in one figure, which is the only place rule 2's
   two vocabularies are allowed to meet. A floating bar is a move and takes sign
   colour; the total is a person's estimate, takes their hue, and wears the
   interval that says the sum is not known. Nothing here is hue AND sign, and
   the difference between the two registers is the whole reading.

   Every value is set in the figure, so nothing is hover-only; the titles are
   the same facts as a sentence. Geometry is build.py's, this file is colour,
   type and weight — the same division the trajectory above it runs on. */
/* The null, in the ledger's unit, and the same weight it has in every other
   instance: drawn OVER the bars it judges, because a rule this heavy under a
   solid bar is a rule the reader never sees cross anything. */
.fig-wf .wf-null { stroke: var(--ink); stroke-width: 3; }
/* WHERE THIS ROW STANDS AMONG THE FOUR. A sort order on four overlapping intervals, so it
   is set at the sub size in --quiet under the figure it ranks — never at the measurement's
   own weight, and never as a badge. */
.fig-wf .wf-rank { fill: var(--quiet); font-size: 10.5px; letter-spacing: .06em; }
/* Information, not decoration: the connector carries the arithmetic and the
   separator carries the summary row, so both clear the 3:1 mark floor. */
.fig-wf .wf-link, .fig-wf .wf-sep { stroke: var(--rule-strong); stroke-width: 1; }
.fig-wf .wf-bar.pos { fill: var(--up); }
.fig-wf .wf-bar.neg { fill: var(--down); }
.fig-wf .wf-bar.zero, .fig-wf .wf-bar.none { fill: var(--quiet); }
.fig-wf .wf-total { fill: var(--hue, var(--quiet)); }
/* AN ESTIMATE: the capped dimension line `.rail-iv` was, at the same cap height,
   under the bar rather than through it. Two claims about one number stay two
   marks; a line drawn through a solid bar of its own colour fuses them. */
.fig-wf .wf-iv, .fig-wf .wf-cap { stroke: var(--hue, var(--quiet)); stroke-width: 1; }
.fig-wf .wf-label { fill: var(--ink); font-size: 12.5px; letter-spacing: .06em; }
.fig-wf .wf-sub, .fig-wf .wf-note { fill: var(--quiet); font-size: 10.5px; letter-spacing: .06em; }
/* A number takes sign colour here as it does in every table on this site: the
   hue on this figure belongs to the bar, never to the digits. */
.fig-wf .wf-val { fill: var(--quiet); font-size: 12.5px; letter-spacing: .06em; }
.fig-wf .wf-val.pos { fill: var(--up); }
.fig-wf .wf-val.neg { fill: var(--down); }
.fig-wf .wf-hit { fill: transparent; }

/* ============================================================================
   THE TWO STRIPS OF RESULTS ON THE CARD.

   Two paragraphs came off the card and a strip of figures went under each
   register in the space they left. The two strips are deliberately THE SAME
   SHAPE, because what tells them apart is the only thing this object is about:
   every figure on the left prints a sign and was estimated, every figure on the
   right was counted and cannot print one. Still no axis, no span and no rule at
   zero anywhere on the right-hand side.
   ========================================================================= */
.pc-strip {
  margin-top: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--rule);
}
.pc-strip .lab { margin: 0 0 var(--sp-2); }
.pc-tally {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5);
}
.pc-tally > li { display: flex; flex-direction: column; }
/* --fs-head and not --fs-display. These are the SECOND figure in their register
   and a strip that matched the big number would be a second headline: same
   face, same weight, same negative tracking, one step down. */
.pc-n {
  font-family: var(--mono); font-size: var(--fs-head); font-weight: var(--w-strong);
  letter-spacing: -.01em; line-height: 1.2;
}
.pc-k {
  font-family: var(--mono); font-size: var(--fs-label); letter-spacing: .13em;
  text-transform: uppercase; color: var(--quiet); margin-top: var(--sp-1);
}
/* MORE AIR IN THE ONE OBJECT THE FOUR OF THEM ACTUALLY LOOK AT. --sp-4 was the
   padding of a card with four figures on its face; there are nine now. */
.card-reg-est, .card-reg-cnt, .card-strip-block { padding: var(--sp-5); }
/* ============================================================================
   THE SIX TABLES A PLAYER PAGE IS MADE OF: five breakdowns and the synergy.

   Scoped to .bt and to nothing else. The match pages share every bare element
   selector above this, and a sixty-row round ledger wants the opposite of what
   a three-row breakdown wants.

   THE z COLUMN CAME OFF BOTH OF THEM. A drawn interval at cell scale restated,
   at lower precision and on a fixed axis the page never names, the two numbers
   already printed beside it. The uncertainty stayed — it is a column of numbers
   and it was always the numbers doing the work.

   Which leaves six tables of pure figure, so the hierarchy has to be type.
   Three registers, keyed off the header class that already travels down its own
   column: the finding, the width of the finding, and the exposure that explains
   the width. A reader scanning one column down the page is scanning .bt-find.
   ========================================================================= */
.bt-block { margin-top: var(--sp-6); }
.bt-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); margin: 0 0 var(--sp-3);
}
.bt-head h3 { margin: 0; }
/* The row count. It was a sentence under every one of these five headings
   telling the reader how many rows were about to follow. */
.bt-count {
  margin: 0; font-family: var(--mono); font-size: var(--fs-label);
  font-weight: var(--w-strong); letter-spacing: .13em; text-transform: uppercase;
  color: var(--quiet); white-space: nowrap;
}

/* WHOSE TABLE THIS IS, in the one channel this site reserves for a person, and
   it carries no measurement: the page is that person's page and their name is
   at the top of it. The same 2px edge the card wears, so the card and the six
   tables under it read as one object. `p-<short>` was already on these tables —
   it fed the hue to the interval bars that just came off, and this is what it
   feeds now. */
.bt thead th {
  border-top: 2px solid var(--hue, var(--rule-strong));
  border-bottom: 1px solid var(--rule-strong);
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
}
/* 6px of vertical padding is right for a sixty-row ledger and wrong for a
   three-row breakdown, and that is the whole of what "these do not pop" was. */
.bt tbody td { padding: var(--sp-3); font-size: var(--fs-body); }

/* The column that names the row: full ink, the one weight the rest of the table
   is not, and a hairline down its right edge so the names read as a spine
   rather than as the first of six numbers. */
.bt th.bt-key, .bt td.bt-key { border-right: 1px solid var(--rule); }
.bt tbody td.bt-key { color: var(--ink); font-weight: var(--w-strong); }
/* THE FINDING, and it is the largest figure in the table. Nothing sets a colour
   here on purpose: .pos and .neg colour their own spans and outrank inherited
   colour, so the third sign channel survives the promotion. */
.bt tbody td.bt-find { font-size: var(--fs-prose); font-weight: var(--w-strong); }
/* Its width, one step down and in full ink. The bar went; the estimate did not,
   and a quiet interval under a loud point estimate would be this site arguing
   against itself. */
.bt tbody td.bt-ival { font-size: var(--fs-data); }
/* The exposure and the match count: the answer to "why is that interval so
   wide", which is not itself a finding. */
.bt tbody td.bt-aside { font-size: var(--fs-data); color: var(--quiet); }

/* ---- The round panel ------------------------------------------------------
   A round is two objects and they answer two questions. .round-timeline is
   WHAT HAPPENED, in order. .round-ledger is WHAT EACH OF TEN PEOPLE WAS WORTH.
   The aria-labels have always said so; the layout now does too.

   The ledger was fourteen columns and 1,618px inside a horizontal scroller,
   with five columns of small signed percentages the reader was expected to add
   in their head against a sixth. It is eight columns and 1,142px, which fits
   the 1,144px measure with nothing to scroll. The five parts did not go into a
   tooltip — they are printed under the bar that draws them, in the same order,
   at the same precision they always had. */
.rl-meta {
  font-family: var(--mono); font-size: var(--fs-data); color: var(--quiet);
  /* A LABEL STRIP TAKES NO PROSE MEASURE, the same exemption .lab has: it is a
     <p>, so the 66ch cap broke four labelled numbers over two lines. */
  max-width: none;
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-5);
  margin: var(--sp-3) 0 var(--sp-4);
}
.rl-fact { white-space: nowrap; }
.rl-key {
  font-family: var(--mono); font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; color: var(--quiet);
  margin-right: var(--sp-2);
}
.rl-key[title] { cursor: help; }
.rl-meta .rl-val { color: var(--ink); }

/* ---- The ledger row -------------------------------------------------------
   Roomier on purpose: two lines in the name cell, a bar and its six numbers in
   the track cell. 6px of padding was sized for a one-line row. */
.round-ledger tbody td { padding: var(--sp-3); vertical-align: top; }
.round-ledger thead th, .round-ledger tr.group-head th { white-space: normal; }
.round-ledger .rl-who { display: block; }
/* The duel roles wrap now. The rule that stopped them was written for a
   fourteen-column table where one 73px row made the sheet look ragged; this
   cell is two lines tall on every row it appears in. */
.round-ledger .duels { display: block; margin-top: 3px; white-space: normal; }
.round-ledger .tag + .tag { margin-left: 0; }
.round-ledger .tag { display: inline-block; margin-right: var(--sp-2); }

/* THE TRACK. It is a `z` column, so the null at zero is the same unbroken
   painted vertical every signed column on this site gets, and the header's bold
   0 sits exactly on it. The cell keeps zero horizontal padding — that is what
   makes the bar's own 50% land on the painted rule — so the numbers under the
   bar carry their own inset instead. */
.round-ledger th.z, .round-ledger td.z { width: 40%; min-width: 316px; }
.round-ledger td.z { text-align: left; padding: var(--sp-3) 0; }
.round-ledger .rl-parts { display: block; padding: var(--sp-2) var(--sp-3) 0; }

/* THE STACK. Positives right of the null, negatives left, in a fixed order.
   Nothing here identifies a segment by colour: --up/--down are the direction a
   probability moved and the four hues are people, so the only channels left are
   position in the stack and height. The two half-height segments are the two
   the payload itself calls bookkeeping — the alive clock, split equally across
   the survivors, and the centring term against the rest of the round. Full
   height is what the player did. Both take currentColor from the sign class the
   renderer put on them, which is the same --up/--down pair contrast.py already
   guards on the field, the well and both team washes. */
.rl-bar { position: relative; display: block; height: 20px; }
.rl-seg { position: absolute; top: 5px; height: 10px; background: currentColor; }
.rl-seg.is-book { top: 7px; height: 6px; }
/* THE NET, and it is a third mark because a two-armed stack does not show its
   own total: MWPA is the difference between the arms, not the end of either.
   Taller than the segment band at both ends, so it never vanishes into an arm
   of its own sign colour. Same geometry and the same job as .ival-point. */
.rl-net { position: absolute; top: 0; height: 20px; width: 3px; margin-left: -1.5px; background: currentColor; }
/* A stack that ran past the pinned axis, flagged at the end it ran off — the
   same dotted edge .ival uses, for the same reason. */
.rl-bar[data-clipped~="lo"] { border-left: 1px dotted var(--quiet); }
.rl-bar[data-clipped~="hi"] { border-right: 1px dotted var(--quiet); }

/* The six numbers, in the order the bar stacks them. This is the key: there is
   no legend, because a legend for six segments that no colour may name is a
   second thing to learn, and the numbers have to be printed anyway. */
.rl-part { display: inline-block; margin-right: var(--sp-4); white-space: nowrap; }
.rl-part .rl-key { margin-right: var(--sp-1); }
.rl-part .num { font-size: var(--fs-label); }

/* ---- The round timeline ---------------------------------------------------
   Unchanged in shape: five columns, the round's own curve read down, and the
   footer that accounts for the clock nodes too small for `dp` to print.

   Two things are new. The victim rides in the actor's cell, so a kill row says
   the whole event instead of half of it. And ONLY AN ACTION IS SOMETHING
   SOMEBODY DID — that was carried by --quiet on the other three kinds and by
   nothing else, so an action row now also carries a 2px inset rule at its left
   edge, the same device `tr.is-cursor` uses, one token quieter. It is an inset
   rule and not a shadow: nothing here casts. */
.round-timeline thead th { white-space: normal; }
.round-timeline tbody tr[data-kind="action"] td:first-child { box-shadow: inset 2px 0 0 var(--rule-strong); }
/* The spaces are in the text, not in the padding, so the cell extracts as
   `MLK → Anonymous#3da6d8`. */
.rl-vs { color: var(--quiet); }

/* The match figure's player key draws BOTH channels the plot uses — the hued
   vertical and the marker with its initial in it. The 26x14 box the rule
   swatches use cannot hold a mark at plot size, so this one is 34x20 and the
   mark inside it is the same radius the plot draws. */
.legend-key .swatch.wpx-key { width: 34px; height: 20px; }
.legend-key .swatch.wpx-key .wp-act { stroke-width: 4.5; }

/* THE CONTROL BAR. What the line means on the left, what x measures on the
   right, one strip. The toggle is a .chip pair and nothing more: the site
   already spells "either on or off" that way, and a second spelling is how two
   controls stop agreeing. It changes an axis, never a value. */
.wpx-bar {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-3) var(--sp-4); flex-wrap: wrap; margin: 0 0 var(--sp-3);
}
.wpx-bar .fig-legend { margin: 0; flex: 1 1 22em; }
.wpx-modes { display: flex; gap: var(--sp-1); flex: none; }
.wpx-mode { white-space: nowrap; }

/* ---- Footer --------------------------------------------------------------- */
.site-foot {
  max-width: var(--maxw); margin: 0 auto;
  padding: var(--sp-4) var(--sp-4) var(--sp-6);
  border-top: 1px solid var(--rule);
  color: var(--quiet); font-size: var(--fs-body); line-height: 1.7;
}
.site-foot p { margin: 0 0 var(--sp-2); max-width: 78ch; font-size: var(--fs-body); }
.site-foot code { word-break: break-all; }

/* ---- Responsive ----------------------------------------------------------- */
/* The rail keeps its three columns at every width. It has to: the null is a
   child of the track column, and a rail that reflowed into a stack would be a
   rail with no continuous line through it — which is the whole argument. So
   the columns narrow and the interval text moves under the name, and the
   front page's finding is never a drag gesture. */
@media (max-width: 900px) {
  :root { --fs-display: 27px; --fs-head: 17px; }
}
@media (max-width: 640px) {
  :root {
    --fs-display: 23px; --fs-prose: 15.5px; --fs-data: 12px; --fs-label: 10px;
  }
  .page { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  .site-head, .site-foot { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  .fig, .figure { padding: var(--sp-3); }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

@media print {
  .site-head, .theme-toggle, .crumb, .round-strip, .round-tabs, .fig-legend,
  .wp-tip, .idx-filter { display: none; }
  body { background: #fff; color: #000; }
  .fig, .figure, .scroll, .table-wrap { break-inside: avoid; }
  .fig-data[open] > summary { list-style: none; }
}

/* Round panels are emitted expanded so the page is whole without the script.
   `js-tabs` is set by quad-app.js the moment it takes over. */
.js-tabs .round-panel { display: none; }
.js-tabs .round-panel.is-on { display: block; }

@media print {
  .js-tabs .round-panel { display: block; }
}
