/* public/css/site.css

   ============================================================================
   Design system
   ============================================================================

   This file is hand-written, has no build step and no framework. Everything
   below is organised so a future change can find the right lever instead of
   adding another one-off value.

   COLOUR
     --bg / --fg / --muted / --rule / --rule-strong / --control-border /
     --accent / --row-alt / --row-hover / --marker — defined once in `:root`,
     each as `light-dark(light-value, dark-value)`. Every other rule in this
     file reads colour only through these custom properties, so a rule is
     written once and works in both schemes. Which half applies follows the
     element's `color-scheme`, which is `light dark` by default (the operating
     system decides) and is pinned by `[data-theme]` on <html> once a visitor
     uses the theme toggle in the header — see public/js/theme.js.
     `--control-border` (not `--rule`) is used wherever a border is doing a
     control's job — a filter pill, a stat card, a form input — because
     WCAG 1.4.11 wants 3:1 there; `--rule` is decorative-only and is
     deliberately lower contrast (see test/lib/contrast.test.js). `--danger`
     and `--success` are the form-feedback red/green (error/success borders,
     the destructive-action button) — a distinct pair per scheme, like every
     other colour here, rather than a single hard-coded hex that stays too
     dark to read against a dark `--bg`.

   SPACING SCALE
     --space-1 … --space-8, a 4px-based scale (4/8/12/16/24/32/48/64px). Use
     one of these for every margin/padding/gap in this file instead of an
     ad-hoc rem value — it's what keeps the page's rhythm consistent instead
     of "close enough" everywhere.

   TYPE SCALE
     --fs-title / --fs-heading / --fs-subheading / --fs-body / --fs-small,
     paired with --fw-title / --fw-heading / --fw-subheading / --fw-body.
     --fs-title is the page's own name (one per page, the `<h1>`);
     --fs-heading marks off a major section (`<h2>`); --fs-subheading is a
     subdivision within a section (`<h3>`); --fs-body is running text and
     table cells; --fs-small is footnotes, captions and other small print.

   PAGE HEADER
     `.page-header` wraps a page's `<h1>`, its `.subtitle` line of context,
     and — when a page has them — page-level filters or actions, then draws
     one rule underneath to separate that block from the page body. Every
     public page opens with one.

   PANELS
     `.panel` groups a section heading with the table/content it introduces
     into one bordered block, so a long page reads as a stack of distinct
     units rather than a continuous scroll. A `.panel` does not change the
     heading it wraps — no class or id is added to any `<h2>` that a test
     pins literally (`<h2>Most matches</h2>`, `<h2>Cards</h2>`,
     `<h2>Penalties</h2>`, `<h2>Names by season</h2>`); the border and
     spacing come entirely from the wrapping element.
     `.panel-columns` lays a panel's content out as side-by-side columns
     that collapse to one on a narrow viewport, for a section whose content
     is a pair of short lists rather than one long one — the referee page's
     yellow and red card reasons.

   TABLES
     `.table-wrap` still exists purely for the horizontal scroll it enables
     on narrow viewports; tabular numerals, right-aligned numeric columns,
     a caption and scoped headers are unchanged. Layered on top: a visibly
     stronger header (bolder weight, a background tint, and — since the wrap
     only scrolls horizontally, not vertically — a `position: sticky` header
     that stays put while a long table's own rows scroll past it, no JS
     involved), a hover state on data rows, and a zebra stripe. Hover is
     declared after the stripe rule so it wins on both odd and even rows
     rather than the two fighting for the same pixels.

   FILTERS, PILLS, EMPTY STATES
     `.filters` is the one pattern for every filter/sort control on the site
     (referee index, match index, leaderboards, the position switch on the
     alternate referee page). `.empty` is the one pattern for "nothing to
     show here" — a bordered, centred note rather than a lone paragraph
     floating in blank space.

   ADMIN CHROME
     `.admin-shell` wraps the admin nav and every admin page's content
     (see admin/layout.njk). It draws one restrained signal — a coloured top
     rule — so an operator glancing at any admin screenshot can tell they are
     in the operator tools, not the public site, without the admin area
     becoming a visually different product. `.admin-nav` is the operator's
     primary navigation: the same `aria-current="page"` convention as the
     public nav, styled as a row of tabs rather than the public site's pill
     filters, because it is navigation, not a filter.

   FORMS (extended for admin)
     The public site's `.form` (single-column, 26rem, used by login and
     change-password) is unchanged. Admin adds what a data-entry form needs
     that a two-field auth form does not:
       - `fieldset`/`legend` group related controls with a border and a bold
         legend — used for every logical group (a radio choice, a form's
         "details" vs. its metadata, a table's crew positions).
       - `.field-row` (pre-existing) lays a set of fields out side by side,
         wrapping on narrow viewports; the input/select inside keep their own
         intrinsic width rather than stretching to fill the row.
       - Width utility classes `.w-tiny` / `.w-short` / `.w-medium` cap an
         input's width by what it holds (a match minute, a four-character
         club id, a person's given name) instead of letting every field claim
         the same full measure — the club id field is the clearest case: it
         is 1-4 characters and used to render exactly as wide as the club's
         full name field below it.
       - `.field-help` is muted explanatory text directly under a control —
         tighter than `.footnote`, which remains for page-level asides.
       - `.form` now also styles `email`/`number`/`date`/`file` inputs and
         `select`, not only `text`/`password` — admin forms use all of these
         and previously got bare, unstyled browser controls for anything
         past a text box.
     `.danger-zone` and `.btn-danger` mark destructive actions (delete a
     match, restore the database) so they read as unlike an ordinary save —
     see COLOUR's `--danger`.
   ============================================================================
*/

/* Each colour is stated once, as `light-dark(light-value, dark-value)`, rather than as a
   light block plus a dark `@media` block that have to be kept in step rule-by-rule. Which
   half is used follows the element's `color-scheme`, so the theme toggle switches every
   colour on the page by setting one property on <html> — see `[data-theme]` below and
   public/js/theme.js. `color-scheme` also tells the browser to render its own furniture —
   form controls, scrollbars, the canvas before the stylesheet lands — to match, which a
   `@media` query alone never did. */
:root {
  color-scheme: light dark;

  --bg: light-dark(#ffffff, #14171a);
  --fg: light-dark(#16191d, #e7eaee);
  --muted: light-dark(#5b6470, #9aa4b0);
  --rule: light-dark(#d8dde3, #2b3138);
  --rule-strong: light-dark(#b3bcc7, #3c444d);
  /* Dedicated border colour for interactive control boundaries (filter/sort pills, stat
     cards) — WCAG 1.4.11 wants 3:1 for these, which --rule (1.37:1 against --bg) does not
     come close to meeting. ~4.0:1 light, ~5.3:1 dark. Decorative table rules keep --rule. */
  --control-border: light-dark(#75808c, #838d99);
  --accent: light-dark(#14507d, #7db6e8);
  --row-alt: light-dark(#f6f8fa, #1a1e22);
  /* Distinct from --row-alt so a hovered row is never colour-identical to a striped one. */
  --row-hover: light-dark(#eaf1f8, #202a35);
  --marker: light-dark(#8a5a00, #e0aa4a);
  /* Form feedback: an invalid field/destructive action (--danger) and a saved one
     (--success). 6.54:1 and 6.28:1 against light --bg, 6.61:1 and 6.56:1 against dark —
     comfortably past the 4.5:1 .btn-danger's own text needs, since its label sits directly
     on this fill. The dark halves are lighter on purpose; reusing the light hexes there
     would sink both well under 3:1. */
  /* Figures the public site does not publish, shown greyed to a signed-in reader. A 55%
     grey, at the owner's direction: the first attempt used the lightest grey that still
     clears WCAG's 4.5:1 and did not read as distinctly grey enough to mark a whole class of
     figures apart. One value for both schemes rather than a light-dark pair — it is quieter
     than --fg either way, and a grey that is obviously grey is the point.

     3.36:1 against a white background, so it is under the 4.5:1 body-text minimum in light
     mode (5.35:1 in dark). Deliberate and scoped: nothing load-bearing is carried by the
     colour. The footer says in words what grey means, and the figures themselves are
     readable text either way. */
  --restricted: #8c8c8c;
  --danger: light-dark(#b3261e, #ff6f64);
  --success: light-dark(#1b6e3c, #4caf6d);
  --measure: 68rem;

  /* Spacing scale — 4px base. Use these, not ad-hoc rem values. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;

  /* Type scale. --fs-body matches the existing 15px base body copy. */
  --fs-title: 1.75rem;
  --fs-heading: 1.25rem;
  --fs-subheading: 1rem;
  --fs-body: 0.9375rem;
  --fs-small: 0.8125rem;
  --fw-title: 700;
  --fw-heading: 700;
  --fw-subheading: 600;
  --fw-body: 400;

  --radius: 6px;
}

/* The theme toggle's whole effect: one attribute on <html>, set before first paint by
   public/js/theme.js. Absent — the case for a visitor who has never touched the toggle,
   and for every visitor with JavaScript off — the `light dark` above stands and the
   operating system decides. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: var(--fs-body)/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

a { color: var(--accent); }
a:hover { text-decoration-thickness: 2px; }

/* A visible focus ring beyond the browser default, for keyboard navigation through the
   dense link-heavy tables this site is full of. Additive only — no rule anywhere in this
   file sets outline: none, so the platform default keeps working even where this is not
   supported. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, .linklike:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: var(--space-2);
  top: var(--space-2);
  z-index: 10;
  padding: var(--space-2) var(--space-3);
  background: var(--bg);
  border: 2px solid var(--accent);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ============================== Site chrome ============================== */

.site-header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  align-items: baseline;
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--space-4) var(--space-4) var(--space-3);
  border-bottom: 2px solid var(--rule-strong);
}
.site-title { margin: 0; font-size: 1.05rem; font-weight: 700; letter-spacing: -0.01em; }
.site-title a { color: inherit; text-decoration: none; }
.site-header nav ul { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0; padding: 0; list-style: none; }
.site-header nav a { text-decoration: none; color: var(--muted); }
.site-header nav a:hover { color: var(--fg); }
.site-header nav a[aria-current="page"] { color: var(--fg); font-weight: 600; }

.account { margin-left: auto; display: flex; gap: var(--space-3); align-items: baseline; color: var(--muted); font-size: var(--fs-small); }
.account form.inline { display: inline; margin: 0; }

/* The light/dark toggle. Icon only and unlabelled — it sits among text links and a second
   run of words there would read as another destination. Its name is on the button itself,
   set by theme.js, so a screen reader gets a sentence where a sighted reader gets a glyph.
   `align-self: center` because .account aligns on the baseline, which an icon has none of. */
.theme-toggle {
  align-self: center;
  display: flex; padding: var(--space-1);
  background: none; border: 0; border-radius: var(--radius);
  color: var(--muted); cursor: pointer;
}
.theme-toggle:hover { color: var(--fg); }

/* The button shows where pressing it goes, not where the page is: a moon in daylight, a
   sun at night. Three rules rather than one because CSS can read the chosen theme from the
   attribute but the unchosen one only from the media query. */
.theme-toggle .icon-sun { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun { display: block; }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: none; }
}
:root[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
.linklike { background: none; border: none; padding: 0; font: inherit; color: var(--accent); cursor: pointer; text-decoration: underline; }

main { max-width: var(--measure); margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-7); }

.site-footer {
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--space-4);
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-size: var(--fs-small);
}

/* ============================== Admin chrome ============================== */

/* One restrained, page-wide signal that this is the operator side of the site — a
   coloured top rule above the nav — rather than a different visual language. Wraps the
   admin nav and every admin page's own content (see admin/layout.njk). */
.admin-shell { border-top: 3px solid var(--accent); padding-top: var(--space-5); }

/* The operator's primary navigation. Same aria-current="page" convention as the public
   site nav and the same underlying list-of-links markup as `.filters`, but shaped like
   tabs — a bottom rule with the current section picked out by an accent underline —
   because this is navigation, not a filter over one page's content. */
.admin-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-5);
  margin: 0 0 var(--space-6);
  padding: 0 var(--space-1);
  border-bottom: 2px solid var(--rule-strong);
}
.admin-nav a {
  padding: var(--space-2) var(--space-1);
  border-bottom: 3px solid transparent;
  margin-bottom: -2px;
  text-decoration: none;
  color: var(--muted);
}
.admin-nav a:hover { color: var(--fg); }
.admin-nav a[aria-current="true"] { color: var(--fg); font-weight: 600; border-bottom-color: var(--accent); }

/* ============================== Forms ============================== */

.form { max-width: 26rem; }
/* The cap belongs to the single-column default; a gridded form sets its own. */
.form.form-grid { max-width: 76rem; }
.form label { display: block; font-weight: 600; margin-bottom: 0.2rem; }
.form input[type="text"], .form input[type="password"], .form input:not([type]),
.form input[type="email"], .form input[type="number"], .form input[type="date"],
.form input[type="file"], .form select {
  width: 100%; padding: var(--space-2) var(--space-3); border: 1px solid var(--control-border);
  border-radius: 4px; background: var(--bg); color: var(--fg); font: inherit;
}
.form p { margin: 0 0 var(--space-3); }
.form p:last-child { margin-bottom: 0; }
.form button {
  padding: var(--space-2) var(--space-4); border: 1px solid var(--accent); border-radius: 4px;
  background: var(--accent); color: var(--bg); font: inherit; font-weight: 600; cursor: pointer;
}
.form button:hover { filter: brightness(1.1); }
.form-error { padding: var(--space-3) var(--space-4); border-left: 4px solid var(--danger); background: var(--row-alt); border-radius: 0 4px 4px 0; }
.form-success { padding: var(--space-3) var(--space-4); border-left: 4px solid var(--success); background: var(--row-alt); border-radius: 0 4px 4px 0; }

/* A missing-data row saved or refused in place, without the page reloading.
 *
 * The same two colours as the messages above, applied to the row rather than to a banner at
 * the top of the page: with fifty rows on screen the feedback has to be where the operator
 * is looking. `is-saved` is removed again after a moment (public/js/gap-save.js) so a worked
 * table does not end up entirely green; `is-refused` stays until the row is saved, because
 * an unfixed row should not stop looking wrong. A left border rather than a background
 * fill: the row-striping underneath still has to read. */
tr.is-saved > * { box-shadow: inset 4px 0 0 -1px var(--success); }
tr.is-refused > * { box-shadow: inset 4px 0 0 -1px var(--danger); }

/* A form whose sections are independent enough to sit side by side.
 *
 * `.form` alone is one 26rem column, which is right for a login box and wrong for the
 * referee form: eight fieldsets, none of them wider than a name, stacked into a ribbon
 * down the left of a 1400px page with two thirds of it empty and the Save button two
 * screens below the first field.
 *
 * A grid rather than columns: `column-count` would break a fieldset across a column
 * boundary mid-radio-group. `auto-fit` with a 20rem floor means the count follows the
 * viewport — three across on a desktop, two on a laptop, one on a phone — with no
 * breakpoints to keep in step with anything. `align-items: start` stops a short fieldset
 * from stretching to the height of the tall one beside it.
 *
 * Only for a form that says so: the login and settings forms stay the single column they
 * were designed as. */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: var(--space-5); align-items: start; max-width: 76rem; }
.form-grid > fieldset { margin: 0; }
/* Full width, at the end of the last row: a control that acts on the whole form belongs
   under all of it rather than under whichever column it happened to land in. */
.form-grid > .form-actions { grid-column: 1 / -1; margin: 0; }

/* Two columns for a fieldset that holds paired fields. "Name parts" is the case: given and
   common-given belong side by side because one stands in for the other, and family and
   suffix because the suffix is an appendage of the family name — and two of these fields at
   their natural width do not fit one column, so in a single column all four stack and the
   fieldset grows to twice the height of anything beside it.
   Guarded by the media query, not left to `span 2` alone: in the single-column layout a
   narrow viewport falls back to, spanning two would conjure an implicit second column and
   push the form off the side of the screen. */
@media (min-width: 64rem) {
  .form-grid > .field-block-wide { grid-column: span 2; }
}

/* Groups related controls with a visible border and a bold legend — a radio choice
   (sort/alternate name), a form's own sub-section (a club's metadata, a match's crew).
   Unscoped (not `.form fieldset`) because the match form's fieldsets sit in a plain,
   unclassed <form> — see the Forms doc note in the header comment. */
fieldset { border: 1px solid var(--control-border); border-radius: var(--radius); padding: var(--space-4) var(--space-5) var(--space-5); margin: 0 0 var(--space-5); }
fieldset:last-of-type { margin-bottom: 0; }
legend { padding: 0 var(--space-2); font-size: var(--fs-subheading); font-weight: var(--fw-subheading); }

.field-row { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-end; }
.field-row p { margin: 0 0 var(--space-3); }
.field-row input, .field-row select,
.sides input { padding: 0.3rem 0.4rem; border: 1px solid var(--control-border); border-radius: 4px; background: var(--bg); color: var(--fg); }

/* The two sides of a match: one row each, one column per measure, so home and away line
   up as the pair of like things they are. The number columns are sized to the numbers they
   hold rather than left to `auto`, which would size them to the input's default width and
   leave each figure stranded at the left of a column far wider than itself. */
.sides { display: grid; grid-template-columns: max-content minmax(9rem, 20rem) 4.5rem 4.5rem; gap: var(--space-2) var(--space-4); align-items: center; justify-content: start; }
.sides p { margin: 0; }
.sides input { width: 100%; max-width: none; font: inherit; }
.sides-heading { font-size: var(--fs-small); color: var(--muted); }
.sides-side { font-weight: 600; }

/* Below the width where a club name and two number fields fit side by side, the grid
   collapses to one field per line. The decorative headings would then label nothing, so
   each field falls back to its own label — the one that was visually hidden above. */
@media (max-width: 34rem) {
  .sides { grid-template-columns: 1fr; }
  .sides-heading { display: none; }
  .sides .visually-hidden {
    position: static; width: auto; height: auto;
    margin: 0 0 0.2rem; overflow: visible; clip-path: none; white-space: normal;
    display: block; font-weight: 600;
  }
}
td input, td select { width: 100%; padding: 0.2rem 0.3rem; border: 1px solid var(--control-border); border-radius: 3px; background: var(--bg); color: var(--fg); font: inherit; }

/* Caps a control's width by what it actually holds, instead of every field claiming the
   same full measure — a four-character club id or a match minute does not need the width
   of a person's full name. A max-width, not a width, so each still shrinks further on a
   narrow viewport rather than overflowing it. */
.w-tiny { max-width: 4rem; }
.w-short { max-width: 6rem; }
.w-medium { max-width: 12rem; }

/* Muted explanatory text directly under one control — tighter than `.footnote`, which is
   for a page- or section-level aside instead. */
.field-help { margin: 0.2rem 0 0; color: var(--muted); font-size: var(--fs-small); }

/* The match a worklist row is about: a date, the two clubs, and the two places to watch the
   footage. It was one nowrap line and claimed a third of the table for three facts nobody
   edits, crowding the field the row exists to fill. Capped and allowed to wrap instead —
   the cell grows downwards, which is the cheap dimension on a page of short rows.

   The links are smaller again than the club names under `.field-help`, being the least of
   the three and the one a reader only looks at when they are about to use it. */
.match-cell { max-width: 14rem; white-space: normal; }

/* A cell whose parts go one to a line rather than running on: the match's date, clubs and
   links, and the player's name and club. Left inline they read as prose where they are
   separate facts, and the clubs wrapped mid-fixture — "2026-05-16 Orlando City SC v" /
   "Atlanta United FC". `.field-help` is inline elsewhere on purpose, so this is opt-in. */
.stacked-cell .field-help { display: block; }
.match-cell-links {
  display: block; margin: 0.15rem 0 0;
  font-size: 0.78rem; line-height: 1.35;
}
.match-cell-links a { color: var(--muted); }
.match-cell-links a:hover { color: var(--fg); }

/* A page-level note that is not an error, e.g. context on an admin form. Uses the same
   left-rule shape as .form-error/.form-success but neutral colouring. */
.notice { padding: var(--space-3) var(--space-4); border-left: 4px solid var(--control-border); background: var(--row-alt); border-radius: 0 4px 4px 0; color: var(--muted); }

/* A destructive action (delete a match, restore the database) reads as unlike an
   ordinary save: a fully-bordered red box rather than the neutral/left-rule shapes above,
   and a filled red button rather than the accent-coloured default. */
.danger-zone { padding: var(--space-4) var(--space-5); border: 2px solid var(--danger); border-radius: var(--radius); background: var(--row-alt); }
/* The `.form button` pairing (backup.njk's restore form carries class="form") outranks a
   bare `.btn-danger` on specificity alone, so it is repeated here rather than reached for
   with `!important`. */
.btn-danger, .form button.btn-danger { border-color: var(--danger); background: var(--danger); color: var(--bg); }
.btn-danger:hover { filter: brightness(1.1); }

/* ============================== Type & page header ============================== */

h1 { font-size: var(--fs-title); font-weight: var(--fw-title); margin: 0 0 var(--space-2); letter-spacing: -0.02em; line-height: 1.2; }
h2 { font-size: var(--fs-heading); font-weight: var(--fw-heading); margin: var(--space-6) 0 var(--space-3); padding-bottom: var(--space-1); border-bottom: 1px solid var(--rule); }
h3 { font-size: var(--fs-subheading); font-weight: var(--fw-subheading); margin: var(--space-4) 0 var(--space-2); }
.subtitle { margin: 0; color: var(--muted); font-size: var(--fs-body); }

/* Wraps a page's <h1> + .subtitle (+ optional filters/actions), then draws one rule
   under the whole block. Every public page opens with this. A heading or filter strip
   placed inside needs no class changes of its own — the wrapper supplies the spacing. */
.page-header {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-4);
  border-bottom: 2px solid var(--rule-strong);
}
.page-header h1 { margin-bottom: var(--space-2); }
.page-header .subtitle { margin-bottom: var(--space-3); }
.page-header .subtitle:last-child { margin-bottom: 0; }
.page-header .filters:last-child { margin-bottom: 0; }
/* A page-level action or link (e.g. "All {club} matches") sitting under the subtitle,
   before any filters. */
/* One line of outward links. `gap` rather than a separator character: two links reading
   "…mlssoccer.com → on FBref →" run together without one, and a bullet between them would
   be a third thing to read. */
.page-actions { margin: var(--space-3) 0 0; display: flex; flex-wrap: wrap; gap: var(--space-4); }

/* A short table of contents of in-page anchors, used on pages long enough that jumping
   beats scrolling (the referee page). Plain links to #fragment ids — no JS. */
.contents { margin: var(--space-4) 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); font-size: var(--fs-small); }
.contents a { text-decoration: none; }
.contents a:hover { text-decoration: underline; }

/* ============================== Panels ============================== */

/* Groups a section heading with its table/content into one visible block, so a long page
   reads as a stack of distinct units. Does not alter the heading it wraps. */
.panel {
  margin: var(--space-6) 0;
  padding: var(--space-4) var(--space-5) var(--space-5);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
}
.panel > h2:first-child { margin-top: 0; }
.panel > h2 + p, .panel > h2 + .subtitle { margin-top: 0; }
.panel + .panel { margin-top: var(--space-6); }

/* Two (or more) independent tables side by side inside one panel, for a section whose
   content is a pair of short, lopsided lists rather than one long one — the referee page's
   yellow and red card reasons. Stacked, such a pair costs the sum of both heights on a page
   that is already long, and leaves the shorter list a scroll away from the one it is meant
   to be read against; beside each other they cost the height of the taller alone.

   `auto-fit` with a floor rather than a fixed two-column track, so the columns collapse to
   one when the viewport cannot give each table a usable measure — no media query needed,
   and a section with three tables would behave sensibly without a new rule. `align-items:
   start` keeps a short table at the top of its column instead of stretching its last row
   down to meet a taller neighbour. */
.panel-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--space-3) var(--space-6);
  align-items: start;
}
/* A row label in half the panel's width is not the same problem as one in all of it:
   "Denying a goalscoring opportunity (handball)" is longer than a column here can be, and
   left `nowrap` it turned a table into something to scroll sideways at every viewport
   between the two-column and one-column layouts. It is prose and survives a line break, so
   it wraps. The figures beside it are single tokens and keep the site-wide `nowrap`. */
.panel-columns th.text { white-space: normal; }

/* Cards and Penalties, side by side where the screen has room.
 *
 * Stacked, the two panels put a screen between them and left a great deal of white to the
 * right of each: both are a pair of narrow columns of short lines, and a match's discipline
 * reads as one thing rather than two. `align-items: start` so a match with four bookings
 * and one penalty does not stretch the penalties panel to match.
 *
 * Each panel splits into two club columns of its own at 18rem apiece, so the pair needs
 * roughly twice that plus the gaps before it is worth doing — below which they go back to
 * one panel per row and split their own clubs as before.
 */
.panel-pair { display: grid; gap: 0; align-items: start; }
@media (min-width: 68rem) {
  .panel-pair { grid-template-columns: 1fr 1fr; gap: var(--space-5); }
  /* The panels carry their own bottom margin for the stacked case; side by side it would
     leave the shorter one hanging. */
  .panel-pair > .panel { margin-bottom: 0; height: 100%; }
}

/* A match's cards, grouped by club and then by player.
 *
 * Two nested lists rather than a table: the outer rows are people and the inner ones are
 * events, which is a hierarchy a table can only fake with a repeated first column — which
 * is what the flat card table was. `list-style: none` on both, because the bullet would be
 * a third mark competing with the colour chip that is already doing the work.
 */
.card-list, .card-entries, .event-list { list-style: none; margin: 0; padding: 0; }
.card-list > li { margin: 0 0 var(--space-3); }
.card-list > li:last-child { margin-bottom: 0; }
.card-player { margin: 0; font-weight: 600; }
.card-entries li, .event-list li { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; font-size: var(--fs-small); }

/* "No cards" or "No penalties" under a club that had none: a line in the list's own voice,
   not the dashed placeholder box `.empty` draws. That box is for a whole panel with nothing
   in it, and at the head of a column half a panel wide it made an absence louder than the
   events printed beside it. */
.empty-line { margin: 0; color: var(--muted); font-size: var(--fs-small); }

/* One flat list of events under a club heading, where the cards need two levels because
   several of theirs can belong to one player. A match's penalties are one apiece and want
   no grouping under a name — nobody wins two — so they take the card entries' line and
   skip the list of people above it. Spaced a little, being their own rows rather than
   sub-items of one. */
.event-list li + li { margin-top: var(--space-1); }

/* The penalty spot, drawn as a spot. Decorative, like the card chips beside it: the
   heading says these are penalties and every line reads without it. */
.pen-spot {
  flex: none; width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--muted);
}

/* The card itself, as a card: a small rectangle in the colour that was shown. Decorative —
   every entry carries the colour in words for a screen reader — so it is aria-hidden and
   the layout does not depend on it. `flex: none` keeps it from being squeezed to nothing
   when a long reason wraps beside it. */
.card-chip { flex: none; width: 0.6rem; height: 0.85rem; border-radius: 1px; border: 1px solid rgb(0 0 0 / 0.35); }
.card-chip-y { background: #f2c200; }
.card-chip-r { background: #d4322c; }

/* A dismissal, beside the name. Small and in the red the card was, so the eye finds it
   without it shouting over the name it qualifies. */
.card-off { font-size: var(--fs-small); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: light-dark(#b3231e, #ff8a84); }

/* A field whose content is code: the SQL console's query box, and the examples under it.
   Monospaced, because alignment carries meaning in a query and proportional type throws it
   away; and `white-space: pre` on the block so an indented join keeps its shape. */
.code, .code-block { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: var(--fs-small); }
.code { width: 100%; padding: var(--space-2) var(--space-3); border: 1px solid var(--control-border); border-radius: 4px; background: var(--bg); color: var(--fg); }
.code-block { margin: var(--space-2) 0 0; padding: var(--space-3); overflow-x: auto; background: var(--row-alt); border-radius: 4px; white-space: pre; }

/* A statistic's two readings in one card: the total, and the same figure per match in
   brackets after it. The bracketed halves are muted so the card still reads as one number
   at a glance, and the qualifier in the heading lines up with the rate under it. */
.stat-qualifier, .stat-rate { color: var(--muted); font-weight: 400; }
.stat-rate { font-size: var(--fs-small); }

/* ============================== Tables ============================== */

.table-wrap { overflow-x: auto; margin: var(--space-2) 0; }

table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
caption { text-align: left; color: var(--muted); padding-bottom: var(--space-2); font-size: var(--fs-small); }
th, td { padding: 0.3rem 0.55rem; text-align: right; white-space: nowrap; border-bottom: 1px solid var(--rule); }
th:first-child, td:first-child, th.text, td.text { text-align: left; }
/* A text column wraps; a numeric one does not. The `nowrap` above is there for figures —
   a token that must never be broken across lines — and applying it to prose made the widest
   tables wider than the panel holding them, so a reader at an ordinary laptop width had to
   scroll sideways to reach the last column. Club and referee names are the ones that push
   it: "Vancouver Whitecaps FC" claimed 190px of a 900px table for want of permission to
   take two lines. `.nowrap` opts a text cell back out, which the date column uses — an
   ISO date breaks after its hyphens, and half a date on each of two lines is worse than
   anything wrapping was meant to fix. */
th.text, td.text { white-space: normal; }
/* Written to out-specify `th.text` above rather than merely to follow it — a bare
   `.nowrap` loses to it, and the date would wrap after its hyphens anyway. */
.nowrap, th.nowrap, td.nowrap { white-space: nowrap; }
/* Headings wrap where data cells do not. Spelling the numeric columns out ("Opponent
   cautions" rather than "Cau opp") made the widest tables wider than the panel holding
   them, so a reader at an ordinary desktop width had to scroll sideways to reach columns
   that used to be in view. A heading is prose and survives a line break; the figure under
   it is a single token and must not be broken, so the `nowrap` above still governs the
   body. The rate headings are written "Cautions /&nbsp;match" for the same reason: left to
   break freely they put the slash alone on its own line in a squeezed table. */
thead th {
  white-space: normal;
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--row-alt);
  border-bottom: 2px solid var(--rule-strong);
  font-size: var(--fs-small);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}
thead th a { color: inherit; }
/* A row-labelling `<th scope="row">` in tbody carries the same weight and font as its
   `<td>` neighbours — the semantics are for assistive tech, not a visual emphasis cue. */
tbody th { font-weight: inherit; font-size: inherit; text-transform: none; letter-spacing: normal; color: inherit; }
tbody tr:nth-child(even) { background: var(--row-alt); }
/* Declared after the zebra rule so it wins the same-specificity tie on both odd and even
   rows, rather than a hovered even row keeping its stripe colour underneath the cursor. */
tbody tr:hover { background: var(--row-hover); }
tfoot td, tfoot th { border-bottom: none; color: var(--muted); font-size: var(--fs-small); white-space: normal; font-weight: 600; }
tfoot tr:first-child td, tfoot tr:first-child th { border-top: 2px solid var(--rule-strong); padding-top: var(--space-2); }

/* The per-position match count beside a crewmate's position, e.g. "AR1 (12)" — smaller
   and muted so the position itself (the useful-at-a-glance part) reads first. It sits
   inside the position's link, so it keeps its muted colour rather than inheriting the
   link's: the count is context for the position, not a second thing to click. */
.position-count { font-size: var(--fs-small); color: var(--muted); }

/* The substitution dagger. A <button>, so it is reachable by keyboard and takes focus on a
   tap — which is what makes the explanation available on a touch screen, where there is no
   hover to rely on. Stripped back to look like the character it is rather than a control:
   the dotted underline is the only affordance, and it is the conventional one for "this
   term explains itself". */
.marker {
  color: var(--marker); font-weight: 700; font: inherit; font-weight: 700;
  background: none; border: 0; padding: 0; cursor: help;
  text-decoration: underline dotted; text-underline-offset: 0.15em;
}
.marker:hover, .marker:focus-visible { text-decoration: underline solid; }

/* The explanation. Two of them are rendered once per page by the layout, one per kind of
   dagger; a match page's crew box adds one more beside each of its own daggers, because
   that explanation names the official and the minute and so differs per dagger.
   `position: fixed` is load-bearing rather than decorative: a dagger lives in a table cell
   inside `.table-wrap`, which is `overflow-x: auto`, and an element whose overflow is not
   visible on one axis clips the other too — so anything positioned relative to the dagger
   would be cut off at the edge of the table. Fixed positioning escapes that entirely, at
   the cost of the note appearing at the foot of the viewport rather than beside the
   symbol. For one sentence that always says the same thing, a predictable place is worth
   more than proximity, and it never covers the row being read. */
.marker-note {
  position: fixed;
  left: 50%; bottom: var(--space-4);
  transform: translateX(-50%) translateY(0.4rem);
  z-index: 20;
  max-width: min(44rem, calc(100vw - 2 * var(--space-4)));
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--bg);
  border: 1px solid var(--rule-strong);
  border-left: 4px solid var(--marker);
  border-radius: var(--radius);
  box-shadow: 0 2px 12px rgb(0 0 0 / 25%);
  color: var(--fg);
  font-size: var(--fs-small);
  opacity: 0; visibility: hidden;
  transition: opacity 120ms ease, transform 120ms ease, visibility 0s linear 120ms;
}

/* `:has()` is what lets a dagger anywhere on the page reveal a note that is not its
   sibling, without a line of JavaScript — every public page here ships none. Hover covers
   a mouse, focus-visible covers the keyboard, and plain focus covers a tap, which is the
   case a hover-only tooltip fails completely. A crew dagger's note *is* its next sibling,
   so it needs no `:has()` at all — which is also what allows one note per dagger, since a
   selector cannot otherwise tell hundreds of identical notes apart. */
body:has(.marker-aggregate:hover) #marker-note-aggregate,
body:has(.marker-aggregate:focus) #marker-note-aggregate,
body:has(.marker-match:hover) #marker-note-match,
body:has(.marker-match:focus) #marker-note-match,
.marker-crew:hover + .marker-note,
.marker-crew:focus + .marker-note {
  opacity: 1; visibility: visible;
  transform: translateX(-50%) translateY(0);
  transition: opacity 120ms ease, transform 120ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .marker-note { transition: opacity 1ms linear, visibility 0s linear 1ms; transform: translateX(-50%); }
  body:has(.marker-aggregate:hover) #marker-note-aggregate,
  body:has(.marker-aggregate:focus) #marker-note-aggregate,
  body:has(.marker-match:hover) #marker-note-match,
  body:has(.marker-match:focus) #marker-note-match,
  .marker-crew:hover + .marker-note,
  .marker-crew:focus + .marker-note { transform: translateX(-50%); }
}

.footnote { margin: var(--space-3) 0 0; color: var(--muted); font-size: var(--fs-small); }

/* ============================== Stat grid ============================== */

.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--space-3); margin: var(--space-4) 0; }
.stat { border: 1px solid var(--control-border); border-radius: var(--radius); padding: var(--space-3) var(--space-4); }
.stat dt { margin: 0; font-size: var(--fs-small); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.stat dd { margin: 0.15rem 0 0; font-size: 1.3rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* ============================== Filters, pager, empty states ============================== */

.filters { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: baseline; margin: var(--space-3) 0; }

/* One label and the control it names, as a single thing that wraps.
 *
 * The filter forms laid label and field out as siblings of the wrapping row above, so the
 * row broke wherever it ran out of width and left the words on one line with the box they
 * name on the next — "Referee:" over nothing, an unlabelled box beneath it. It was not a
 * narrow-screen problem: the match index orphaned a label at almost every width under
 * 1100px, and which one depended on where the wrap happened to fall. Pairing them makes the
 * pair the unit that wraps, and the row then reads at any width at all.
 *
 * `min-width: 0` on both, or a long field refuses to shrink and pushes the pair past the
 * edge of a phone instead of narrowing inside it. The width utilities (`.w-medium` and the
 * rest) are max-widths, so they cap without preventing that. */
.filter-field { display: flex; align-items: baseline; gap: var(--space-2); min-width: 0; }
.filter-field > label { flex: none; }
.filter-field > input, .filter-field > select, .filter-field > .combobox { min-width: 0; }
.filter-field > .combobox { flex: 1 1 auto; }
.filter-field > .combobox > input { width: 100%; }

/* On a phone, the label goes above its field and the field takes the width.
 *
 * Side by side they fit, but each field then starts wherever its own label ends — "Club:"
 * is short and "Crewmate:" is not — so four boxes sat at four different left edges down the
 * screen. Stacked, they share one, and it is the same shape `.form` gives every other form
 * in this application: label on its own line, control filling the measure below it. */
@media (max-width: 34rem) {
  form.filters > .filter-field {
    flex: 1 1 100%;
    flex-direction: column; align-items: stretch; gap: 0.2rem;
  }
  form.filters > .filter-field > label { font-weight: 600; }
  /* A descendant selector, not a child one: three of these fields have their input inside
     a `.combobox`, and a child rule reached only the fourth — leaving the select filling
     the measure and the three text boxes still capped by `.w-medium` two-thirds across it.
     `max-width: none` overrides that cap, which is there to stop a name field claiming the
     width of the page on a desktop and has nothing to say on a phone. */
  form.filters .filter-field input,
  form.filters .filter-field select { width: 100%; max-width: none; }
}
.filters a {
  text-decoration: none; padding: 0.15rem var(--space-3); border: 1px solid var(--control-border);
  border-radius: 999px; color: var(--muted); transition: background-color 0.1s ease, color 0.1s ease;
}
.filters a:hover { color: var(--fg); background: var(--row-alt); }

/* How many matches a pill leads to, inside the pill and quieter than its label. The label
   is what a reader is choosing between; the count is why they might choose it. */
.pill-count { color: var(--muted); font-size: var(--fs-small); }

/* The crewmate box's suggestion list. A hand-built listbox rather than a <datalist>,
   because a datalist option carries no second column and cannot be disabled, and both are
   the point here: each name comes with the number of matches it would find, and a name at
   nought is shown but not selectable. See public/js/crewmate-filter.js.

   Absolutely positioned under the field, so opening it does not push the filter row's other
   controls down the page. The wrapper is the positioning context and is inline, since it
   sits in a row of labels and inputs. */
.combobox { position: relative; display: inline-block; }
.suggestions {
  position: absolute; z-index: 20; top: 100%; left: 0; min-width: 100%;
  margin: 2px 0 0; padding: var(--space-1) 0;
  list-style: none;
  max-height: 16rem; overflow-y: auto;
  background: var(--bg); border: 1px solid var(--control-border); border-radius: 4px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
}
.suggestions [role="option"] {
  display: flex; justify-content: space-between; gap: var(--space-4);
  padding: 0.25rem var(--space-3);
  white-space: nowrap; cursor: pointer;
}
.suggestions [role="option"].is-active { background: var(--row-alt); }
.suggestion-count { color: var(--muted); font-size: var(--fs-small); }
/* Nobody this official has shared a match with under the filters in force. Offered, so a
   reader who typed the name in full is told rather than left wondering, and dead, because
   choosing it could only open an empty table. */
.suggestions [role="option"].is-unavailable { color: var(--rule-strong); cursor: default; }
.suggestions [role="option"].is-unavailable .suggestion-count { color: inherit; }
.filters a[aria-current="true"] { border-color: var(--accent); color: var(--fg); font-weight: 600; background: var(--row-alt); }

/* Not published to the public site — see src/lib/restricted.js. Applied to a whole cell,
   a whole stat card or a run of words; `!important` is deliberately not used, so a rule
   that sets colour more specifically still wins and this is a default rather than a
   guarantee. The tests assert the markup carries the class, not that a pixel is grey. */
.restricted, .restricted dt, .restricted dd { color: var(--restricted); }
/* Filter pills set their own colour at a higher specificity than a bare `.restricted`
   would, so the unpublished ones have to say so at that level too — including the active
   pill, which is otherwise emphasised back to --fg. Without these three the Home advantage
   row read exactly like the published rows above it. */
.filters.restricted > span,
.filters a.restricted,
.filters a.restricted[aria-current="true"] { color: var(--restricted); }

/* The leaderboard metric nav: one `.filters` row per subject. The rows are tightened
   against each other and their labels given a common column, so five rows read as one
   block of choices rather than as five unrelated filters stacked by accident. */
.metric-groups { margin: var(--space-3) 0; }
.metric-groups .filters { margin: 0 0 var(--space-2); }
.metric-groups .filters:last-child { margin-bottom: 0; }
.metric-groups .filters > span {
  flex: 0 0 auto; min-width: 9rem;
  color: var(--muted); font-size: var(--fs-small);
}

/* A filter row that is a form rather than a run of links — the match index's club and
   referee boxes, where 33 clubs and 690 officials are both past what pills can show and a
   type-ahead box is the control a reader already knows. The submit button is not optional
   decoration: these have to work with no JavaScript, so a typed value is submitted by
   hand. All three borrow the pill's own border and radius so the row still reads as one
   set of filters and not as a form that wandered onto the page. */
.filters input[type="text"], .filters select, .filters button {
  font: inherit;
  padding: 0.15rem var(--space-2);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
}
.filters input[type="text"] { max-width: 12rem; }
.filters button { padding: 0.15rem var(--space-3); border-radius: 999px; cursor: pointer; }
.filters button:hover { background: var(--row-alt); }

.pager { display: flex; gap: var(--space-4); align-items: baseline; margin: var(--space-4) 0; }
.pager .count { color: var(--muted); }

/* A deliberate empty state — a bordered, centred note rather than a lone paragraph
   floating in otherwise-blank space. */
.empty {
  margin: var(--space-4) 0;
  padding: var(--space-5);
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  color: var(--muted);
  text-align: center;
}

/* The share bar beside a card-minute band. An <svg> whose width attribute carries the
   percentage — see the note in referee.njk for why it cannot be an inline style — so the
   colour is on the rect and the element itself only has to sit on the text's baseline. */
.bar { display: inline-block; vertical-align: middle; }
.bar rect { fill: var(--accent); }

@media (max-width: 34rem) {
  th, td { padding: 0.3rem 0.4rem; }
  body { font-size: 14px; }
  .panel { padding: var(--space-3) var(--space-4) var(--space-4); }
}

/* ===================== Match page: facts and crew ===================== */

/* One club per line. A `<br>` would do it visually, but these are two separate names and
   a screen reader should get a pause between them, not a run-on. */
.match-title span { display: block; }

/* The match's numbers, one row per statistic and one column per club.

   Not inside `.table-wrap`: it is five rows of short figures and never needs to scroll,
   and the wrap's `overflow` would clip the crew note that can appear beside it. The
   site-wide table chrome — sticky header, zebra stripe, hover — is turned off too; those
   earn their keep on a hundred-row match log and only add noise to five rows read as a
   pair of columns. */
.comparison {
  width: auto; min-width: min(28rem, 100%);
  margin: var(--space-4) 0 0;
  border-collapse: collapse;
}
.comparison thead th {
  position: static; background: none;
  padding: 0 var(--space-4) var(--space-2);
  text-align: right; font-size: var(--fs-body); font-weight: var(--fw-subheading);
  white-space: normal; text-transform: none; letter-spacing: normal;
}
.comparison thead th:last-child { padding-right: 0; }
.comparison tbody tr { background: none; }
.comparison tbody tr:hover { background: none; }
/* The label column is the quiet one: a reader scans the figures and refers back to the
   label, not the other way round. */
.comparison tbody th {
  padding: var(--space-2) var(--space-5) var(--space-2) 0;
  text-align: left; font-weight: var(--fw-body); color: var(--muted);
  border-bottom: 1px solid var(--rule);
}
.comparison tbody td {
  padding: var(--space-2) var(--space-4);
  text-align: right; font-variant-numeric: tabular-nums; font-weight: 600;
  border-bottom: 1px solid var(--rule);
}
.comparison tbody td:last-child { padding-right: 0; }
.comparison tbody tr:last-child th, .comparison tbody tr:last-child td { border-bottom: 0; }

/* The match summary and the crew, side by side. The crew is four short lines; below the
   title at full width it cost a screenful to say so. `auto` on the second column lets the
   box be exactly as wide as the longest name rather than a fixed measure that is too
   narrow for "Levon Baladjanian" and too wide for "R Hall".

   The rule that normally closes a `.page-header` moves here, so it runs the full width
   under both columns instead of stopping where the left one ends. */
.match-top {
  display: grid;
  /* The crew column is a share of the page, not a content width: `auto` sized it to the
     longest name, which on a wide screen left a box far narrower than the space beside it.
     A band rather than one figure — never so narrow that names wrap, never so wide that it
     competes with the scoreline. */
  grid-template-columns: minmax(0, 1fr) minmax(40%, 50%);
  gap: var(--space-5) var(--space-6);
  align-items: start;
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-4);
  border-bottom: 2px solid var(--rule-strong);
}
.match-top > .page-header { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }

/* Fills the column the grid gives it; the width is decided there. */
.crew-panel { margin: 0; }
.crew-panel .footnote { max-width: none; }

/* One official per entry: the position chip, then the name and which appearance in that
   position this match was. */
.crew { list-style: none; margin: 0; padding: 0; }
.crew li {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: var(--space-1) 0;
  min-width: 0;
}
.crew li + li { border-top: 1px solid var(--rule); }

/* Fixed-width, so the names line up down the box rather than starting wherever "AR1" or
   "4th" happens to end. Not uppercased: these are the position's own spelling, and
   `text-transform` turned the fourth official into "4TH", which is not what it is called
   and read as an acronym. */
.crew-pos {
  flex: 0 0 2.5rem;
  color: var(--muted); font-size: var(--fs-small); font-weight: 600;
}
/* Name, any dagger, and the appearance line, stacked. The stack takes the width the chip
   leaves, so the appearance sentence begins under the name rather than under the chip. */
.crew-who { flex: 1 1 auto; min-width: 0; }
.crew-name { overflow-wrap: break-word; }

/* The appearance count as a sentence — "5th match as referee" — on its own line. A bare
   "5th" at the end of the name's line was ambiguous, and a phrase long enough to resolve
   the ambiguity does not fit beside a name in a column this narrow. The box is short of
   width, not of height, so the second line is the cheap dimension to spend. Nothing left
   to keep `white-space: nowrap` for, now that it is prose rather than a number. */
.crew-app {
  display: block;
  color: var(--muted); font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

/* The full table of appearance counts, in the disclosure below the crew box. Its figures
   are ordinals meant to be compared down a column, so they line up on their digits. */
.crew-table td { font-variant-numeric: tabular-nums; }
/* Where one competition's group of columns begins. Without it the nine figures on an
   overlapping match read as one run, and "which of these is the regular season" is a
   counting exercise. One line on almost every match; two on a July 2020 group-stage
   fixture, which was in two competitions at once.

   An inset shadow rather than `border-left`, which is what this was. In a table with
   `border-collapse: collapse` the browser resolves the borders between neighbouring cells
   into one set and paints them as a layer over the whole table — and opening a tooltip
   below, which makes one row taller, re-lays the table out without repainting that layer
   past the row that changed. The line then stops there and stays stopped, because closing
   the tooltip again dirties the same region and no more. The shadow belongs to the cell
   instead of to the table, so it is repainted whenever the cell is, and 1px inset on the
   left edge of a cell with no left border draws in exactly the same place. */
.crew-table .span-start { box-shadow: inset 1px 0 0 var(--rule-strong); }

/* An assistant referee's figure carries the one-touchline count on its tooltip, so it is
   marked as having something behind it — a dotted underline, the convention every other
   <abbr> on this site uses — and takes the help cursor rather than a text caret. */
.crew-table abbr { text-decoration: underline dotted; text-underline-offset: 0.2em; cursor: help; }
.crew-table abbr:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The same words the tooltip carries, shown under the figure when it is tapped or when
   Enter is pressed on it — a `title` is a hover affordance, and a phone has no hover. Under
   the figure rather than in a floating bubble because the table scrolls horizontally, and
   anything absolutely positioned inside a scrolling box is clipped at its edge.

   Hidden until public/js/tooltip.js adds the class. Without that file the span never
   appears and the tooltip behaves as it always did. */
.tip-text { display: none; color: var(--muted); font-size: var(--fs-small); font-weight: 400; }
.crew-table abbr.is-open .tip-text { display: block; }

/* Below the width where a scoreline and a name can share a line, the box goes back under
   the summary rather than being crushed into a column too narrow for either. */
@media (max-width: 52rem) {
  .match-top { grid-template-columns: 1fr; }
}

/* Home, score and away, read as one scoreline rather than three columns.
   
   They used to inherit the table's ordinary alignment — left, right, left — which put a
   wide gap between the home club and the score and none between the score and the away
   club, so "NE      6–1 CIN" read as two things rather than one. Pointing them inwards
   (right, centre, left) makes the score the middle of a unit.
   
   `width: 1%` is the shrink-to-fit idiom: the table is `width: 100%`, so its slack has to
   land somewhere, and it was landing on these three — HOME was several times the width of
   "NE". A declared width under the content width makes them take only what they need and
   sends the slack to the columns that can use it, the referee name and the date. */
.matchup-home, .matchup-score, .matchup-away { width: 1%; white-space: nowrap; }
.matchup-home { text-align: right; padding-right: var(--space-2); }
.matchup-score { text-align: center; padding-left: var(--space-2); padding-right: var(--space-2); }
.matchup-away { text-align: left; padding-left: var(--space-2); }

/* The totals / per-match switch above a by-season table. Shaped like the filter pills it
   sits among — same border, same radius — because it does the same job: choosing which of
   two views of one thing to look at. `aria-pressed` is both the state a screen reader
   announces and the hook this styles from, so the two cannot drift apart. */
.figures-switch button[aria-pressed="true"] {
  border-color: var(--accent); color: var(--fg); font-weight: 600; background: var(--row-alt);
}

/* Season/appearance pairs on the alternate referee page, flowed into columns.

   As a table this was a full-width panel eleven rows tall carrying a year and a one- or
   two-digit number per row — the pairs are so narrow that the columns were mostly
   whitespace, and two sections said between them only "how many, and when". Three or four
   across, it is three rows deep. The same reasoning as the match page's crew box: content
   this narrow should cost width, not a screenful of height.

   `auto-fill` rather than a fixed count, so it is four columns on a desktop and one on a
   phone without a media query. */
.season-counts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--space-1) var(--space-5);
  margin: var(--space-4) 0 0;
}
.season-counts > div {
  display: flex; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--rule);
}
.season-counts dt { color: var(--muted); }
.season-counts dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; }

/* A panel heading with an action beside it. The heading keeps the rule the `.panel` draws
   under every `<h2>`, so the action sits on that line rather than starting a row of its
   own — the point of putting it there is that it costs no height. Wraps under the heading
   on a narrow viewport, where two things on one line would collide. */
.panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap;
}
.panel-head h2 { flex: 1 1 auto; }
.panel-head-action { font-size: var(--fs-small); white-space: nowrap; }

/* An abbreviation in a table — a club's short form in a match list. No underline and no
   help cursor: the browser's default dotted rule under every one of these would draw more
   attention to the shortening than the shortening is worth, on a column where every cell
   carries one. The title attribute still gives the full name on hover and to assistive
   technology, which is the part that matters. */
abbr[title] { text-decoration: none; }

/* A collapsible block of filters. The browser does the collapsing; this only makes the
   summary look like the label it replaces rather than like a heading with a triangle.

   `list-style: none` plus the WebKit pseudo-element removes the native marker, because it
   sits at the far left where a reader is looking for the word "Season", not beside the
   value the way a control's affordance should. The chevron that replaces it turns with the
   state, which is the whole signal — and it is `aria-hidden`, since a `<summary>` already
   announces expanded/collapsed to assistive technology and a spoken "▾" would be noise. */
.disclosure { margin: var(--space-3) 0; }
.disclosure > summary {
  display: flex; align-items: baseline; gap: var(--space-2);
  width: fit-content; padding: var(--space-1) 0;
  cursor: pointer; list-style: none; color: var(--muted);
}
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary:hover { color: var(--fg); }
.disclosure > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The current season, named in the summary so collapsing the grid never hides which one is
   on — the one thing a collapse must not cost. */
.disclosure-value { color: var(--fg); font-weight: 600; }
.disclosure-chevron { font-size: var(--fs-small); transition: transform 0.12s ease; }
.disclosure[open] > summary .disclosure-chevron { transform: rotate(180deg); }
/* The grid brings its own top margin; inside a disclosure it would double the gap. */
.disclosure > .filters:first-of-type { margin-top: var(--space-1); }
/* A disclosure that is itself a panel — the match page's full appearance table. The summary
   is the panel's heading, so it takes the weight a heading would have rather than the muted
   grey of a control sitting inside one. */
.panel.disclosure { margin: 0 0 var(--space-5); }
.panel.disclosure > summary { color: var(--fg); font-weight: var(--fw-subheading); }

/* The season filter, as a decade grid. Still `.filters`, so the year links keep the pill
   styling every other filter row uses; only the layout changes from a wrapping flex run to
   a grid. One row per decade, ten columns for the final digit, and the columns are shared
   by every row — which is the entire point, since a year is found by looking down a column
   as much as along a row. */
.seasons-grid {
  display: grid;
  grid-template-columns: auto repeat(10, minmax(0, 1fr));
  gap: var(--space-1) var(--space-2);
  align-items: center;
  justify-items: stretch;
}
/* "All" has the first row to itself; the decades start on the next one. It used to share
   that row with a "Season:" label, which moved into the disclosure's summary when the grid
   became collapsible — the label had to go somewhere the reader can see while it is shut. */
.seasons-all { grid-column: 1 / -1; justify-self: start; }
.seasons-decade {
  grid-column: 1;
  color: var(--muted); font-size: var(--fs-small); font-weight: 600;
  padding-right: var(--space-2); white-space: nowrap;
}
.seasons-grid a { text-align: center; }
/* A season the selected round was never played in. Shaped exactly like the year buttons
   beside it — same padding, same radius, same box — so the grid's columns stay aligned and
   the difference reads as "not available" rather than as a hole. Quieter than --muted, and
   `cursor: default` because there is nothing to click. */
.seasons-unavailable {
  padding: 0.15rem var(--space-3);
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--rule-strong);
  text-align: center;
  cursor: default;
}

/* An empty cell, not a missing one: the 1990s begin at 1996, and the six columns before it
   have to stay open or every later decade would shift left and the alignment would be a
   lie. Not announced, because there is nothing there to announce. */
.seasons-gap { visibility: hidden; }

/* Ten year-columns will not fit a phone; fall back to the wrapping run it used to be. */
@media (max-width: 46rem) {
  .seasons-grid { display: flex; flex-wrap: wrap; gap: var(--space-2); }
  .seasons-decade { width: 100%; padding: var(--space-1) 0 0; }
  .seasons-gap { display: none; }
}
