/* WOM Tracker.
 *
 * One stylesheet for every page. The palette itself is not here: it is
 * generated from wom/theme.py and written into :root by the template, so the
 * D3 charts and the page read the same colours from the same place.
 *
 * The order below is the cascade. A later layer beats an earlier one whatever
 * the selectors say, which is what keeps a page rule from having to out-
 * specify a component rule - or lose to it by an accident of source order.
 */
@layer reset, base, layout, components, pages, utilities;

/* ==================================================================== */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }

  body { margin: 0; }

  h1, h2, h3, h4, p { overflow-wrap: break-word; }

  img { max-width: 100%; height: auto; }

  /* Anything that moves, moves because it helps. For a viewer who has asked
     their system for less of that, none of it is load-bearing. */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: .01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: .01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

/* ==================================================================== */
@layer base {
  :root {
    /* The palette arrives from the template. These are the rest of the
       decisions: spacing, corners and type, named once. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --radius-sm: 5px;
    --radius: 7px;
    --radius-lg: 10px;
    --text-xs: 12px;
    --text-sm: 13px;
    --text-md: 14px;
    --font-ui: "Segoe UI", system-ui, -apple-system, sans-serif;
    --font-mono: Consolas, ui-monospace, monospace;
    /* Everything stacks below this: see the layout layer. */
    --sidebar: 232px;

    /* Renders the browser's own furniture - scrollbars, form controls, the
       space behind a dialog - in the same dark as the page. */
    color-scheme: dark;
  }

  body {
    background: var(--bg);
    color: var(--ink);
    font: 15px/1.5 var(--font-ui);
    -webkit-text-size-adjust: 100%;
    /* Nothing may push the page sideways: a chart or a table that cannot fit
       scrolls inside its own card instead. `clip` rather than `hidden`, which
       would make the body a scroll container and can strand a sticky child. */
    overflow-x: hidden;
    overflow-x: clip;
  }

  /* A visible ring for keyboard users, and none of the mouse-click halo that
     made the old rules reach for `outline: none`. */
  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 3px;
  }

  select, button {
    font: inherit; padding: 5px 9px; border-radius: var(--radius);
    border: 1px solid var(--line); background: var(--raised); color: var(--ink);
  }
  button { cursor: pointer; background: var(--accent); color: #fff; border: none; }
  input[type=checkbox] { accent-color: var(--accent); }
  input[type=color] {
    width: 44px; height: 28px; padding: 0; border-radius: var(--radius-sm);
    border: 1px solid var(--line); background: var(--raised);
  }
  textarea {
    width: 100%; font: var(--text-sm)/1.5 var(--font-mono);
    background: var(--raised); color: var(--ink); padding: 10px;
    border: 1px solid var(--line); border-radius: var(--radius-lg);
    resize: vertical;
  }
  code {
    background: var(--raised); padding: 1px 5px; border-radius: 4px;
    font-size: var(--text-xs);
  }

  table { border-collapse: collapse; width: 100%; font-size: var(--text-md); }
  th, td {
    text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--line);
  }
  th {
    color: var(--muted); font-weight: 600; font-size: var(--text-xs);
    text-transform: uppercase; letter-spacing: .03em;
  }
  /* The header has to be told as well, or it stays left over right-set figures. */
  th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
  td.name { white-space: nowrap; }
  td.dim { color: var(--muted); }
}

/* ==================================================================== */
@layer layout {
  header {
    display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap;
    padding: 14px var(--space-5);
    background: var(--card); border-bottom: 1px solid var(--line);
  }
  header h1 { font-size: 17px; margin: 0; }
  nav a {
    color: var(--muted); text-decoration: none; margin-right: 14px;
    font-weight: 500;
  }
  nav a.on, nav a:hover { color: var(--accent); }
  .status { margin-left: auto; color: var(--muted); font-size: var(--text-sm); }
  /* Hidden until live.js unhides it, and only on the pages whose content the
     server renders whole: there is nothing to redraw in place there, so the
     reader is offered the reload rather than handed one mid-sentence. Its own
     margin, because .status already claims the free space with an auto one
     and two of those would split it between them. */
  #refresh-now { margin-left: var(--space-3); }

  main {
    display: flex; gap: 18px; padding: 18px var(--space-5);
    align-items: flex-start;
  }
  /* The sidebar and the help box below it travel together, so the column
     holds the width and the stickiness and each card inside it is just a
     card. */
  .rail {
    flex: 0 0 var(--sidebar); position: sticky; top: 18px;
    display: flex; flex-direction: column; gap: var(--space-3);
  }
  .side {
    background: var(--card);
    border: 1px solid var(--line); border-radius: var(--radius-lg);
    padding: 14px;
  }
  .side-help {
    background: var(--card); border: 1px solid var(--line);
    border-radius: var(--radius-lg); padding: 10px 14px 12px;
    display: flex; flex-direction: column; gap: 3px;
    font-size: var(--text-xs); line-height: 1.5;
  }
  .side-help a { font-size: var(--text-sm); }
  .side h2 {
    font-size: var(--text-sm); text-transform: uppercase; letter-spacing: .04em;
    color: var(--muted); margin: 0 0 10px;
  }
  .side-head { display: flex; align-items: baseline; gap: var(--space-2); }
  .side-head h2 { margin-bottom: 6px; }
  .side select { width: 100%; }
  .side label {
    display: flex; align-items: center; gap: var(--space-2); padding: 3px 0;
    cursor: pointer;
  }
  /* min-width: 0 is what lets this column shrink at all: a flex item's floor
     is the width of its own contents, so one wide table in here would widen
     the page rather than scroll inside its own card. */
  .content { flex: 1 1 auto; min-width: 0; }

  footer {
    color: var(--muted); font-size: var(--text-xs);
    padding: 4px var(--space-5) 24px;
  }

  /* -- narrow screens -------------------------------------------------
     The desktop layout puts a fixed sidebar beside the charts, which on a
     phone leaves the charts a couple of hundred pixels and pushes the page
     into a sideways scroll. Below this width everything stacks, and the
     player picker becomes a compact strip above the cards rather than a
     column that fills the first screenful. */
  @media (max-width: 820px) {
    header { padding: 10px var(--space-3); gap: 4px 14px; }
    header h1 { font-size: 16px; width: 100%; }
    nav a { margin-right: var(--space-3); }
    .status { margin-left: 0; width: 100%; font-size: var(--text-xs); }
    /* align-items: flex-start is a horizontal rule once this is a column, and
       it shrinks every card to the width of its own text. Stretch instead. */
    main {
      flex-direction: column; align-items: stretch;
      gap: var(--space-3); padding: var(--space-3);
    }
    .rail {
      position: static; flex: 1 1 auto; width: 100%; gap: var(--space-3);
    }
    .side { width: 100%; padding: var(--space-3); }
    .side label {
      display: inline-flex; width: auto; margin-right: 14px; padding: 4px 0;
    }
    .side h2 { margin-bottom: 6px; }
    footer { padding: 4px var(--space-3) 20px; }
  }
}

/* ==================================================================== */
@layer components {

  /* The one page that is only words. Narrow, because a paragraph the width
     of a chart is hard to read back to the start of. */
  .prose { max-width: 62ch; }
  .prose h4 { margin: var(--space-4) 0 var(--space-2); }
  .prose p { margin: 0 0 var(--space-2); line-height: 1.6; }

  /* -- cards ---------------------------------------------------------- */
  .card {
    background: var(--card); border: 1px solid var(--line);
    border-radius: var(--radius-lg); padding: 14px var(--space-4);
    margin-bottom: var(--space-4);
  }
  .card h3 { margin: 0 0 2px; font-size: 15px; }
  .card h4 {
    margin: 18px 0 6px; font-size: var(--text-sm); color: var(--muted);
    text-transform: uppercase; letter-spacing: .04em;
  }
  .card p.hint { margin: 0; color: var(--muted); font-size: var(--text-sm); }
  /* The build line sits above the first card rather than inside one, so it
     styles itself: `.card p.hint` above cannot reach it. */
  p.hint.build {
    margin: 0 0 var(--space-3); color: var(--muted);
    font-size: var(--text-sm);
  }
  p.hint.build code { font-family: var(--font-mono); color: var(--ink); }
  .card-head {
    display: flex; align-items: flex-start; gap: var(--space-3);
    margin-bottom: var(--space-2);
  }
  .card-head .pick {
    margin-left: auto; color: var(--muted); font-size: var(--text-sm);
    white-space: nowrap;
  }
  .card-head .pick select { margin-left: 6px; }

  /* Two ways to read one card, both worth showing at once - a select would
     hide whichever is not chosen behind a click, and there are only ever two.
     Where a card has a dropdown too, that one owns the auto margin and this
     sits alongside it rather than fighting it for the same free space. */
  .card-head .modes {
    display: inline-flex; gap: 2px; margin-left: auto; padding: 2px;
    background: var(--raised); border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  .card-head .pick ~ .modes { margin-left: 0; }
  .card-head .modes .mode {
    padding: 4px 10px; border: none; border-radius: var(--radius-sm);
    background: none; color: var(--muted); font-size: var(--text-sm);
  }
  .card-head .modes .mode:hover { color: var(--ink); }
  .card-head .modes .mode[aria-pressed="true"] {
    background: var(--card); color: var(--ink);
  }

  /* The group's figures: three across, two down. Fixed rather than fitted -
     six tiles fanned across one row read as a strip of labels, and the point
     of the card is that each one is a headline. */
  .totals {
    display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr;
    gap: 1px; background: var(--line); border: 1px solid var(--line);
    border-radius: var(--radius); overflow: hidden;
  }
  .totals .total {
    display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
    padding: 16px 16px 14px; border: none; border-radius: 0; cursor: help;
    background: var(--card); color: var(--ink); text-align: left;
  }
  .totals .total:hover { background: var(--raised); }
  .totals .total-key {
    font-size: var(--text-sm); color: var(--muted);
    text-transform: uppercase; letter-spacing: .04em;
  }
  .totals .total-value {
    font: 600 26px/1.05 var(--font-mono); font-variant-numeric: tabular-nums;
  }
  .totals .total-note { font-size: var(--text-sm); color: var(--muted); }

  @media (max-width: 560px) {
    .totals { grid-template-columns: repeat(2, 1fr); }
    .totals .total { padding: 12px; }
    .totals .total-value { font-size: 21px; }
  }

  @media (max-width: 820px) {
    .card { padding: var(--space-3); border-radius: var(--space-2); }
    .card-head { flex-wrap: wrap; }
    .card-head .modes, .card-head .pick ~ .modes { margin: 4px 0 0; }
    /* Full width means its own line: left on one line with the word before
       it, a 100%-wide select starts where that word ends and hangs off the
       card by exactly that much. */
    .card-head .pick { margin-left: 0; width: 100%; white-space: normal; }
    .card-head .pick select { width: 100%; margin: 4px 0 0; }
  }

  /* -- small pieces --------------------------------------------------- */
  /* Not a button to look at, but a button to press: it sits inside a heading
     and must not shout louder than the names under it. */
  button.link {
    margin-left: auto; background: none; border: none; padding: 0;
    color: var(--accent); font-size: var(--text-xs); text-transform: uppercase;
    letter-spacing: .04em; font-weight: 600;
  }
  button.link:hover { text-decoration: underline; }
  button.clear {
    background: transparent; color: var(--muted); border: 1px solid var(--line);
    font-size: var(--space-4); line-height: 1; padding: 6px 10px;
    margin-bottom: 1px;
  }
  button.clear:hover { color: var(--ink); border-color: var(--accent); }
  button.quiet {
    background: var(--raised); color: var(--muted);
    border: 1px solid var(--line); margin-right: auto;
  }

  /* The one colour a template still sets inline, because it is data: each
     player picks their own. It arrives as --dot rather than as a background,
     so the shape stays the stylesheet's business. */
  .swatch {
    width: var(--space-3); height: var(--space-3); border-radius: 3px;
    flex: none; background: var(--dot, var(--muted));
  }
  /* Beside a name in a table cell, where there is no flex row to align it. */
  td .swatch, .standing .swatch {
    display: inline-block; margin-right: 6px;
  }

  .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
  .row .grow { margin-left: auto; }
  .live-note {
    margin: var(--space-3) 0 0; color: var(--muted); font-size: var(--text-xs);
  }
  .warn-note { margin: 0 0 var(--space-2); color: #d9a441; font-size: var(--text-xs); }
  .warn { color: #e5707e; margin: 0 0 var(--space-3); }
  .feed-icon {
    width: var(--space-5); height: var(--space-5); image-rendering: pixelated;
    vertical-align: middle;
  }
  .row-icon {
    width: 18px; height: 18px; image-rendering: pixelated;
    vertical-align: middle; margin-right: 7px;
  }
  .pickers { display: flex; flex-wrap: wrap; gap: 4px 18px; }
  .pickers .tick { margin: 0; }

  /* -- a box that scrolls only when it has to ------------------------- */
  /* Sideways when its contents do not fit, an ordinary box when they do -
     fit.js decides which. Without JavaScript it stays a scroller, which is
     the safe way round: a stray scrollbar beats a table with its right-hand
     columns cut off and no way to reach them. */
  .scroll { overflow-x: auto; }
  .scroll.fits { overflow-x: visible; }

  /* -- charts --------------------------------------------------------- */
  .plot { position: relative; min-height: 120px; }
  .plot svg { display: block; overflow: visible; }
  .plot .empty {
    color: var(--muted); text-align: center; padding: 44px var(--space-3);
  }
  .plot.loading::after {
    content: "drawing..."; display: block; color: var(--muted);
    text-align: center; padding: 44px var(--space-3); font-size: var(--text-sm);
  }
  .plot rect, .plot circle, .plot image { cursor: crosshair; }
  .plot .coverage {
    margin: 6px 0 0; color: var(--muted); font-size: var(--text-xs);
  }
  .plot .coverage .tip-dot {
    display: inline-block; width: var(--space-2); height: var(--space-2);
    border-radius: 2px; margin: 0 4px 0 var(--space-2);
  }

  .tip {
    position: absolute; z-index: 20; pointer-events: none; max-width: 320px;
    background: var(--raised); color: var(--ink); border: 1px solid var(--line);
    border-radius: var(--radius); padding: 7px 10px; font-size: var(--text-sm);
    line-height: 1.4; box-shadow: 0 6px 18px rgba(0, 0, 0, .45);
    transition: opacity .08s ease;
  }
  .tip .tip-sub {
    color: var(--muted); font-size: var(--text-xs); margin-top: 2px;
  }
  .tip .tip-dot {
    display: inline-block; width: var(--space-2); height: var(--space-2);
    border-radius: 2px; margin-right: 6px;
  }
  @media (max-width: 820px) {
    .tip { max-width: calc(100vw - 24px); }
  }

  /* -- written notes -------------------------------------------------- */
  /* The Maxing Leaderboard's answer for the window a recap covers, shown
     beside the prose. The recap judges on its own reading and the calendar
     judges on the rule; where they differ, the squares followed this. */
  .verdict {
    display: inline-flex; align-items: center; gap: 5px;
    margin-left: var(--space-2); padding: 1px var(--space-2);
    border: 1px solid var(--line); border-radius: 999px;
    font-size: var(--text-xs); white-space: nowrap;
  }
  .verdict.named { color: var(--dot, var(--ink)); border-color: currentColor; }
  .verdict.none { color: var(--muted); }
  /* Folded away inside an opened row: the day's skills are what the row was
     opened for, and a year of recaps above them would bury it. */
  .recaps { margin-bottom: var(--space-3); }

  /* -- disclosure: the summaries tree, the round-ups, the player rows -- */
  /* Plain <details>/<summary>, so expanding and collapsing works with no
     JavaScript at all. */
  .tree summary.node,
  .detail-body summary.node,
  .latest > summary {
    cursor: pointer; list-style: none; padding: 5px 4px;
    border-radius: var(--radius-sm); display: flex; align-items: center;
    gap: var(--space-2); user-select: none;
  }
  .tree summary.node::-webkit-details-marker,
  .detail-body summary.node::-webkit-details-marker,
  .latest > summary::-webkit-details-marker { display: none; }
  /* Drawn with borders rather than a glyph, so it depends on no font. */
  .tree summary.node::before,
  .detail-body summary.node::before,
  .latest > summary::before {
    content: ""; flex: none; width: 0; height: 0; margin: 0 2px;
    border-left: 5px solid var(--muted);
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform .12s ease;
  }
  .tree details[open] > summary.node::before,
  .detail-body details[open] > summary.node::before,
  .latest[open] > summary::before { transform: rotate(90deg); }
  .tree summary.node:hover,
  .detail-body summary.node:hover,
  .latest > summary:hover { background: var(--raised); }

  .tree summary.player { font-size: 15px; font-weight: 600; }
  .tree .folder > summary.node { color: var(--ink); font-weight: 500; }
  .tree .leaf > summary.node { color: var(--muted); font-weight: 400; }
  .tree .folder, .tree .leaf { margin-left: 14px; }
  .tree .count, .detail-body .count {
    margin-left: auto; color: var(--muted); font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
  }
  .tree .body {
    margin: 2px 0 10px 24px; padding-left: var(--space-3);
    border-left: 2px solid var(--line);
  }
  .tree .body p { margin: 0 0 9px; }
  .tree details.detail {
    padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);
  }
  .tree details.detail > summary.node { font-size: 15px; font-weight: 600; }

  /* A round-up is open on arrival - reading it is the point of the page -
     but it runs several paragraphs, and someone who has read today's wants
     the tree below it rather than a scroll. */
  .latest > summary h3 { display: inline; }
  .latest > summary .ago {
    margin-left: auto; color: var(--muted); font-size: var(--text-xs);
    white-space: nowrap;
  }
  .latest > summary { margin: -2px 0; }
  .latest[open] > summary { margin-bottom: 6px; }
  .jump { margin: -6px 0 var(--space-4) 2px; }
  .jump a { color: var(--muted); text-decoration: none; }
  .jump a:hover { color: var(--accent); }

  @media (max-width: 820px) {
    /* The tree indents four levels deep; at this width that is most of the
       screen, so tighten every step. */
    .tree .folder, .tree .leaf { margin-left: var(--space-2); }
    .tree .body { margin-left: var(--space-3); padding-left: var(--space-2); }
  }

  /* -- a row that doubles as the control for its own detail ----------- */
  /* Two tables use this shape - the roster on Players and the standings on
     Maxing - and a reader who has opened one expects the other to open the
     same way, so they are one set of rules rather than two that drift. */
  tr.player-row, tr.today-row { cursor: pointer; }
  tr.player-row:hover td, tr.player-row:focus td,
  tr.today-row:hover td, tr.today-row:focus td { background: var(--raised); }
  tr.player-row .twist, tr.today-row .twist {
    display: inline-block; width: 0; height: 0; margin-right: 7px;
    border-left: 5px solid var(--muted);
    border-top: 4px solid transparent; border-bottom: 4px solid transparent;
    transition: transform .12s ease; vertical-align: middle;
  }
  tr.player-row.open .twist, tr.today-row.open .twist { transform: rotate(90deg); }
  tr.detail-row > td { padding: 0 10px var(--space-3); }
  .detail-body { margin: 4px 0 6px 22px; }
  .detail-body > .folder { margin-left: 0; }
  .detail-table { font-size: var(--text-sm); }
  .detail-table td {
    padding: 3px var(--space-2); border-bottom: 1px solid var(--line);
  }
  .detail-table tr.moved td { color: var(--ink); }
  .detail-table tr:not(.moved) td { color: var(--muted); }
  .detail-table td.gain {
    color: var(--accent); font-variant-numeric: tabular-nums;
  }
  .detail-table td.name, .detail-table td.measure { white-space: nowrap; }
  /* A 99 reached today is what the day is actually won on, so it is marked
     rather than left to be inferred from the number beside it. */
  .detail-table .badge {
    margin-left: var(--space-2); padding: 1px 5px; border-radius: 3px;
    background: var(--accent); color: var(--bg);
    font-size: var(--text-xs); font-weight: 600; vertical-align: middle;
  }
  /* The twist sits in the rank column here rather than beside the name, so
     the column needs room for both it and a two-digit place. */
  .standing td.rank { white-space: nowrap; }
  .standing tr.today-row td.rank { width: auto; padding-right: var(--space-2); }

  @media (max-width: 820px) {
    th, td { padding: 5px 7px; }
    .detail-body { margin-left: 6px; }
    .detail-table { font-size: var(--text-xs); }
  }

  /* -- standings, on the Overview ------------------------------------- */
  /* Eight columns now it carries the same measures as the group tiles, which
     is more than a phone has room for. It scrolls inside its own card rather
     than widening the page - the same treatment every other wide table here
     gets, and the reason `body` sets `overflow-x: clip`. The header cell of
     the sorted column is what a reader needs to keep in view, so the name
     column travels with the scroll rather than being pinned. */
  .plot:has(> .standings) { overflow-x: auto; }
  .standings { font-size: var(--text-md); }
  .standings th { padding-top: 0; }
  .standings td { padding: 5px 10px; border-bottom: 1px solid var(--line); }
  .standings td.rank { color: var(--muted); width: var(--space-5); }
  .standings td.name { white-space: nowrap; }
  .standings tr.leader td { color: var(--ink); font-weight: 600; }
  .standings tr.leader td.rank { color: var(--accent); }

  @media (max-width: 820px) {
    /* Tighter cells and a smaller face buy back most of the three new
       columns before anything has to scroll at all. */
    .standings { font-size: var(--text-sm); }
    .standings th, .standings td { padding: 5px 6px; }
  }

  /* -- the Maxing Leaderboard on Round-ups ----------------------------
     Two months of squares, each the colour of whoever took that day, and
     beside them the day still running. */
  /* Says there is more to read without shouting about it. */
  .rules {
    cursor: help; border-bottom: 1px dotted var(--line); display: inline-block;
  }
  .months {
    display: flex; gap: 28px; flex-wrap: wrap; margin: var(--space-3) 0 4px;
  }
  /* The standings are their own card below the months now, not a panel
     wedged in beside them. As a flex item they could never be given a width:
     an item refuses to shrink below its content, so any basis small enough to
     leave the months alone was ignored and the page scrolled sideways
     instead. Given the full width there is nothing left to fight over. */
  .standing { min-width: 0; }
  /* Still sized to its contents rather than stretched across the card: six
     names and four short numbers spread over a thousand pixels read as four
     unrelated columns. */
  .standing table { font-size: var(--text-sm); width: auto; }
  /* The month stays under its measure rather than beside it - two lines of
     small text is a narrower column than one long line, and these figures
     are four characters wide. */
  .standing th { white-space: normal; line-height: 1.25; }
  .standing th .when {
    display: block; font-weight: 500; text-transform: none; letter-spacing: 0;
    opacity: .8;
  }
  .standing td { padding: 4px var(--space-2); }
  .standing tr:not(.moved) td { color: var(--muted); }
  .standing td.num { font-variant-numeric: tabular-nums; }
  .standing td.rank {
    color: var(--muted); width: 18px; padding-right: 0;
    font-variant-numeric: tabular-nums;
  }
  .standing tbody tr:first-child td.rank { color: var(--accent); }

  .month h4 { margin: 0 0 var(--space-2); font-size: var(--text-md); }
  /* Inherits the heading's colour, which is the month winner's own: the name
     and the month are saying the same thing, so they say it in one colour.
     Lighter and smaller keeps the month itself the thing you read first.
     (Not .won - that is the day squares, and this is not one.) */
  .month h4 .took {
    font-weight: 400; font-size: var(--text-xs); margin-left: 6px; opacity: .85;
  }
  .grid-days { display: grid; grid-template-columns: repeat(7, 30px); gap: 4px; }
  .grid-days .dow {
    font-size: 11px; color: var(--muted); text-align: center;
    padding-bottom: 2px;
  }
  .grid-days .pad { height: 30px; }
  .grid-days .day {
    height: 30px; border-radius: var(--radius-sm);
    background: var(--day, var(--raised));
    /* The date sits in the middle and the winner's mark in the corner, so
       the two are read as a square with a note on it rather than as two
       characters competing for the middle. */
    position: relative;
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; color: var(--ink); cursor: default;
    /* The number has to stay readable on six different player colours. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, .65);
  }
  /* Who took the day, said in something other than the colour. A reader who
     cannot separate two players' colours - and every reader on a phone, where
     there is no hover - has nothing else to go on otherwise. */
  .grid-days .day .mark {
    position: absolute; top: 1px; right: 2px;
    font-size: 8px; line-height: 1; font-weight: 700; letter-spacing: .02em;
    opacity: .9;
  }
  /* A hairline on the days that were won, so a player whose colour is dark -
     black is a colour someone picked - still reads as a filled square rather
     than as one nobody won. border-box keeps it the same size as the rest. */
  .grid-days .day.won { border: 1px solid rgba(255, 255, 255, .28); }
  /* Today is coloured for whoever leads it but has not been won: the day is
     hours from over and its last poll has not happened. Dashed and paler, so
     it reads as a race in progress rather than a result. */
  .grid-days .day.live {
    border-style: dashed; border-color: rgba(255, 255, 255, .75); opacity: .72;
  }
  /* A day that has not happened is not a day nobody won. */
  .grid-days .day.ahead {
    background: none; border: 1px dashed var(--line); color: var(--muted);
    text-shadow: none;
  }
  /* The days that have happened are links to the page as it stood that
     day, so the link look is taken off to leave the square. */
  .grid-days a.day { text-decoration: none; cursor: pointer; }
  .grid-days a.day:hover { filter: brightness(1.15); }
  /* The day the page is showing. A ring outside the square rather than a
     change to it, because the square's colour is the day's answer and must
     not be what moves. */
  .grid-days a.day[aria-current="date"] {
    outline: 2px solid var(--accent); outline-offset: 2px;
  }
  /* The way back from a picked day, where the table's heading is. */
  a.link.back-to-today {
    margin-left: auto; color: var(--accent); font-size: var(--text-xs);
    text-transform: uppercase; letter-spacing: .04em; font-weight: 600;
    text-decoration: none; white-space: nowrap;
  }
  a.link.back-to-today:hover { text-decoration: underline; }

  /* The calendar and the month's race side by side, wrapping rather than
     squeezing: the months are a fixed width of squares, and the table
     drops beneath them when there is not room for both. */
  .calendar-row {
    display: flex; flex-wrap: wrap; align-items: flex-start;
    gap: var(--space-3) 36px;
  }
  .calendar-row .months { flex: 0 0 auto; }
  .month-race {
    flex: 1 1 260px; min-width: 0; max-width: 420px;
    margin-top: var(--space-3);
  }
  .month-race h4 { margin: 0 0 4px; font-size: var(--text-md); }
  .month-race .hint { margin: 0 0 var(--space-2); }
  .month-race table { font-size: var(--text-sm); width: 100%; }
  .month-race td { padding: 4px var(--space-2); }
  .month-race td.name { white-space: nowrap; }
  .month-race td.num { font-variant-numeric: tabular-nums; }
  .month-race td.rank {
    color: var(--muted); width: 18px; padding-right: 0;
    font-variant-numeric: tabular-nums;
  }
  .month-race tr:not(.moved) td { color: var(--muted); }
  .month-race tbody tr:first-child.moved td.rank { color: var(--accent); }

  @media (max-width: 820px) {
    /* The day columns go fractional, so the two months shrink to share the
       width rather than one wrapping below the other. (The comment here used
       to say they stack. They do not, and did not - the squares are given a
       fraction of the row precisely so both months keep fitting.) */
    .months { gap: 18px; }
    .grid-days { grid-template-columns: repeat(7, 1fr); }
    .grid-days .day, .grid-days .pad { height: 34px; }
    /* Below the squares at this width, so it takes the whole row. */
    .month-race { max-width: none; }
    /* The squares are larger here, so the mark can be too - and this
       is the width where it is doing the most work, there being no
       hover on a phone to ask the square who won. */
    .grid-days .day .mark { font-size: 9px; }
  }

  /* -- forms ---------------------------------------------------------- */
  .field { display: block; margin: 0 0 10px; }
  .field > span {
    display: block; font-size: var(--text-sm); color: var(--muted);
    margin-bottom: 4px;
  }
  .field > span em { color: var(--accent); font-style: normal; }
  .field input, .field select {
    width: 100%; padding: 6px 9px; border-radius: var(--radius);
    border: 1px solid var(--line); background: var(--raised); color: var(--ink);
  }
  .field input:disabled { color: var(--muted); }
  /* A webhook URL, sitting in a table cell rather than under a label. Wide
     enough to read the whole thing without scrolling it, and monospaced so a
     transcribed one can be checked character by character. */
  .secret {
    width: 100%; min-width: 260px; padding: 5px 8px;
    border-radius: var(--radius); border: 1px solid var(--line);
    background: var(--raised); color: var(--ink);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
  }
  /* Whether an account is in game, as the plugin last reported. The palette
     has no red or green and does not need one here: the word carries the
     meaning and the weight carries the emphasis, so this stays readable for
     anyone who cannot separate the two colours a status badge would use. */
  .state { font-weight: 600; }
  .state.in-game { color: var(--accent); }
  .state.logged-in { color: var(--ink); }
  .state.logged-out { color: var(--muted); font-weight: 400; }

  /* The kind filter above the milestones feed. Laid out like the sidebar's
     pickers so it reads as the same sort of control. */
  .types { margin: 0 0 var(--space-2); gap: var(--space-2) 14px; }
  .types .tick { margin: 0; font-size: var(--text-sm); }

  /* Load more, under the feed. Centred and quiet: it is the end of the list
     rather than the point of the page, and it is absent whenever the whole
     list is already on screen. */
  .more { justify-content: center; margin: var(--space-2) 0 0; }
  .more:empty { margin: 0; }
  .load-more { padding: 6px 18px; border-radius: var(--radius); }

  /* The gallery. A grid that gives each screenshot room without a fixed
     column count, so it reflows on a phone rather than scrolling sideways. */
  .panel { margin: 0 0 var(--space-3); }
  .panel h4 { margin: 0 0 var(--space-2); }
  .shots {
    display: grid; gap: var(--space-2);
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  }
  .shots figure { margin: 0; }
  /* The button is only there to make the picture clickable, so it keeps none
     of a button's own appearance - but it keeps the focus ring, because that
     is how somebody using the keyboard knows where they are. */
  .shots .shot {
    display: block; width: 100%; padding: 0; border: 0; background: none;
    cursor: zoom-in; border-radius: var(--radius);
  }
  .shots img {
    width: 100%; height: auto; display: block; border-radius: var(--radius);
    border: 1px solid var(--line); background: var(--raised);
  }
  .shots .shot:hover img { border-color: var(--accent); }

  .viewer {
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--card); color: var(--ink);
    padding: var(--space-2); max-width: min(96vw, 1400px); max-height: 94vh;
  }
  .viewer::backdrop { background: rgba(0, 0, 0, 0.72); }
  .viewer img {
    display: block; max-width: 100%; max-height: 82vh; height: auto;
    border-radius: var(--radius);
  }
  .viewer p { margin: var(--space-2) 0 0; font-size: var(--text-sm); }
  .viewer .close {
    float: right; margin: 0 0 var(--space-2) var(--space-2);
    border: 1px solid var(--line); background: var(--raised); color: var(--ink);
    border-radius: var(--radius); cursor: pointer;
    font-size: var(--text-md); line-height: 1; padding: 3px 9px 5px;
  }
  .shots figcaption {
    font-size: var(--text-xs); color: var(--ink); margin-top: 4px;
    line-height: 1.5;
  }

  .tick {
    display: flex; gap: var(--space-2); align-items: center; margin: 0 0 10px;
  }
  /* Hangs under the field it undoes, rather than reading as its own setting. */
  .tick.sub { margin: -4px 0 14px; font-size: var(--text-xs); color: var(--muted); }

  /* A card that wants reading before the ones around it. */
  .card.attention { border-color: #d9a441; }

  .flash {
    background: var(--raised); border: 1px solid var(--line);
    border-left: 3px solid var(--accent); border-radius: 6px;
    padding: var(--space-2) var(--space-3); margin: 0 0 14px;
  }

  /* -- the export dialog ---------------------------------------------- */
  dialog {
    background: var(--card); color: var(--ink); border: 1px solid var(--line);
    border-radius: var(--radius-lg); padding: var(--space-4) 18px;
    max-width: 560px; width: calc(100% - 24px);
    box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
  }
  dialog::backdrop { background: rgba(0, 0, 0, .55); }
  dialog h3 { margin: 0 0 2px; font-size: 15px; }
  dialog h4 {
    margin: var(--space-4) 0 6px; font-size: var(--text-sm); color: var(--muted);
    text-transform: uppercase; letter-spacing: .04em;
  }
  .dialog-actions {
    display: flex; gap: var(--space-2); flex-wrap: wrap;
    margin-top: var(--space-4); padding-top: var(--space-3);
    border-top: 1px solid var(--line);
  }
  dialog details.columns { margin-top: 14px; }
  dialog details.columns summary {
    cursor: pointer; color: var(--muted); font-size: var(--text-sm);
  }
  @media (max-width: 820px) {
    dialog { padding: var(--space-3); }
    .dialog-actions button { flex: 1 1 auto; }
  }
}

/* ==================================================================== */
@layer pages {

  /* -- Admin. Deliberately plain: it is a form, not a dashboard. ------- */
  .job {
    margin-top: var(--space-3); border-top: 1px solid var(--line);
    padding-top: 10px;
  }
  .job-note { margin: 0; color: var(--muted); font-size: var(--text-sm); }
  .job.busy .job-note { color: var(--accent); }
  .job.failed .job-note { color: #e5707e; }
  .job-log {
    margin: 6px 0 0; max-height: 200px; overflow: auto; color: var(--muted);
    font: var(--text-xs)/1.5 var(--font-mono); white-space: pre-wrap;
  }

  /* -- the Data page ---------------------------------------------------
     A long table of every metric, sorted and filtered in the browser. */
  .toolbar {
    display: flex; gap: var(--space-3); align-items: flex-end; flex-wrap: wrap;
    margin: 4px 0 10px;
  }
  .toolbar .field { margin: 0; min-width: 180px; flex: 1 1 200px; }
  .toolbar .count { white-space: nowrap; margin-left: auto; align-self: end; }
  /* The dates sit at the far end of the filters and are narrower than a
     select: a yyyy-mm-dd input needs no more room than it needs. */
  .dates {
    display: flex; gap: var(--space-2); align-items: flex-end; margin-left: auto;
  }
  .toolbar .dates .field { min-width: 0; flex: 0 1 148px; }
  /* Held dates are outlined, so it is clear at a glance that the period
     picker in the sidebar is no longer what the table is answering. */
  .dates.locked .field input { border-color: var(--accent); }
  /* In the sidebar the two stack, From above To, and both fill the column so
     their edges line up. Side by side they set the sidebar's width, and every
     page then loses that width from its content. */
  .side .dates { display: block; margin-top: var(--space-2); }
  /* .side label is a flex row, which would sit each caption beside its input
     and start the two boxes at different x. Block puts the caption above, so
     From and To line up down the column. */
  .side .dates .field { display: block; margin: 0 0 var(--space-2); min-width: 0; }
  .side .dates .field input { width: 100%; }
  .side .dates .clear {
    display: block; margin-left: auto; padding: 3px 9px;
    font-size: var(--text-md);
  }

  /* Six players' worth of metrics is ~700 rows, which as a plain table is a
     20,000px page. It scrolls in its own box instead, so the search, the
     filters and the count stay on screen while you read down it. */
  .grid-scroll { max-height: 70vh; overflow: auto; }
  .grid { font-size: var(--text-sm); }
  /* Sticky, so the column you sorted by is still named 600 rows down. */
  .grid thead th {
    position: sticky; top: 0; z-index: 1; background: var(--card);
    cursor: pointer; user-select: none; white-space: nowrap;
    box-shadow: inset 0 -1px 0 var(--line);
  }
  .grid thead th:hover { color: var(--ink); }
  /* The arrow is drawn, not typed: it must not depend on a font having it. */
  .grid thead th.sorted { color: var(--accent); }
  .grid thead th.sorted::after {
    content: ""; display: inline-block; margin-left: 6px; vertical-align: middle;
    border-left: 4px solid transparent; border-right: 4px solid transparent;
    border-bottom: 5px solid currentColor;
  }
  .grid thead th.sorted.desc::after {
    border-bottom: 0; border-top: 5px solid currentColor;
  }
  .grid td { padding: 4px 10px; }
  .grid td.metric { white-space: nowrap; }
  .grid tr:not(.moved) td { color: var(--muted); }
  .grid td.gain { color: var(--accent); }
  .grid tbody tr:hover td { background: var(--raised); }

  @media (max-width: 820px) {
    /* Three selects will not sit side by side in 325px, and a half-width one
       cuts "Chambers Of Xeric Challenge Mode" to nothing. One per row. */
    .toolbar { gap: var(--space-2); }
    .toolbar .field { flex: 1 1 100%; }
    .toolbar .count { margin-left: 0; padding-bottom: 0; }
    /* From, To and the x share one row rather than taking three. */
    .dates { margin-left: 0; width: 100%; }
    .toolbar .dates .field { flex: 1 1 0; }
    /* Five columns in 325px: give back what the padding is taking, and let
       the metric wrap. A column is as wide as its widest cell, and "Chambers
       Of Xeric Challenge Mode" was pushing the figures off the side of the
       phone on every one of 666 rows. */
    .grid { font-size: var(--text-xs); }
    .grid th, .grid td { padding: 4px 6px; }
    .grid td.metric { white-space: normal; }
    .grid-scroll { max-height: 65vh; }
  }
}

/* ==================================================================== */
@layer utilities {
  /* Last, so nothing has to out-specify them. The hidden attribute in
     particular used to lose to a component's display: block. */
  [hidden] { display: none; }
  /* The handful of one-line decisions that used to be style attributes.
     Named for what they do, and kept few on purpose: a page reaching for a
     sixth of these is asking for a component instead. */
  /* Text in the player's own colour, where a name is the label rather than a
     row: a month's winner, a branch of the tree, a milestone's owner. A
     utility because it has to beat whatever the component says - `.card h4`
     is muted, and out-specifying it from the components layer is the sort of
     arms race these layers exist to end. */
  .named { color: var(--dot, inherit); }
  .grow { margin-left: auto; }
  .push-right { margin: 0 0 0 auto; }
  .accent { color: var(--accent); }
  .muted-text { color: var(--muted); }
  .center-card { max-width: 420px; margin: 40px auto; }
  .full { width: 100%; }
  .narrow { max-width: 200px; }
  .stack-top { margin-top: var(--space-2); }
  .stack-above { margin-top: var(--space-4); }
  .stack-left { margin-left: 10px; }
  .apply { margin: var(--space-3) 0 0; }
  /* The tree's own heading, and where the jump link lands. */
  .anchor-note { margin: 18px 0 var(--space-2); scroll-margin-top: var(--space-3); }

  @media (max-width: 820px) {
    /* Five columns do not fit 375px; keep the name, the measure and the gain. */
    .wide-only { display: none; }
  }
}
