/* ==========================================================================
   Folding stats — design system

   Dark by default. This is a site people leave open on a second monitor and
   scan columns of numbers on, so the surface stays dark and quiet and the data
   carries the only saturation on the page.

   Colours are the validated reference data-viz palette, unmodified: chart
   surface #1a1a19, page plane #0d0d0d, series slots in fixed order. Chart hues
   are never used for UI chrome and UI chrome never borrows a series hue, so a
   colour on this page always means the same thing.
   ========================================================================== */

:root {
  color-scheme: dark;

  /* Surfaces, layered rather than outlined. Depth comes from the step between
     planes; borders are hairlines that only separate, never decorate. */
  --plane: #0d0d0d;
  --surface: #1a1a19;
  --surface-raised: #232322;
  --surface-hover: #272725;

  --ink: #ffffff;
  --ink-secondary: #c3c2b7;
  --ink-muted: #898781;

  --line: rgba(255, 255, 255, 0.09);
  --line-strong: #383835;
  --grid: #2c2c2a;

  /* Series slots, fixed order, never cycled. */
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #c98500;
  --series-5: #d55181;
  --series-6: #008300;
  --series-7: #9085e9;
  --series-8: #e66767;

  --accent: var(--series-1);
  --accent-quiet: rgba(57, 135, 229, 0.14);

  /* Status is reserved: never a series, always paired with a glyph or label. */
  --good: #0ca30c;
  --warning: #fab219;
  --critical: #d03b3b;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --radius: 10px;
  --radius-sm: 6px;

  /* One spacing scale. Every gap on the page is a step on it. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  --header-h: 56px;
  --wide: 1320px;
}

:root[data-theme="light"] {
  color-scheme: light;
  --plane: #f9f9f7;
  --surface: #fcfcfb;
  --surface-raised: #ffffff;
  --surface-hover: #f2f1ee;
  --ink: #0b0b0b;
  --ink-secondary: #52514e;
  --ink-muted: #898781;
  --line: rgba(11, 11, 11, 0.10);
  --line-strong: #c3c2b7;
  --grid: #e1e0d9;
  --series-1: #2a78d6;
  --series-2: #eb6834;
  --series-3: #1baf7a;
  --series-4: #eda100;
  --series-5: #e87ba4;
  --series-6: #008300;
  --series-7: #4a3aa7;
  --series-8: #e34948;
  --accent-quiet: rgba(42, 120, 214, 0.12);
  --good: #006300;
}

* { box-sizing: border-box; }

/* body must be free to grow past the viewport. With height:100% its box is capped
   at one screen, and a sticky element cannot hold position outside its containing
   block — so the header scrolled away while the sticky table heading went on
   reserving space for it, leaving an empty band the height of a header that was no
   longer there. min-height keeps whatever the 100% was for without the cap. */
html { height: 100%; }
body { min-height: 100%; }

body {
  margin: 0;
  background: var(--plane);
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: inherit; text-decoration: none; }
a:hover { color: var(--accent); }

/* Every number on this page belongs to a column that should line up. */
.num, td.num, th.num { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- header --- */

.header {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: var(--s5);
  padding: 0 var(--s5);
  background: color-mix(in srgb, var(--plane) 82%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  font-weight: 620;
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.brand-mark { color: var(--accent); font-size: 16px; }

/* The two icon buttons are one group. Left as siblings of the nav and the search they
   inherited the header's section gap, which spent width implying they were unrelated
   to each other — and header width is the scarcest thing on this page. */
.header-actions { display: flex; align-items: center; gap: var(--s2); flex: none; }

.nav { display: flex; gap: var(--s1); }
.nav a {
  padding: 6px var(--s3);
  border-radius: var(--radius-sm);
  color: var(--ink-secondary);
  font-weight: 500;
  transition: background 120ms ease, color 120ms ease;
}
.nav a:hover { background: var(--surface-hover); color: var(--ink); }
.nav a[aria-current="page"] { background: var(--accent-quiet); color: var(--accent); }

/* Search lives in the header, not a sidebar: it is the primary way people find
   themselves, and burying it below navigation is the main thing wrong with the
   site this replaces. */
.search {
  position: relative;
  flex: 1 1 auto;
  max-width: 460px;
  margin-left: auto;
}
.search input {
  width: 100%;
  height: 34px;
  padding: 0 var(--s3) 0 34px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit;
  outline: none;
  transition: border-color 120ms ease, background 120ms ease;
}
.search input::placeholder { color: var(--ink-muted); }
.search input:focus { border-color: var(--accent); background: var(--surface-raised); }
.search svg {
  position: absolute;
  left: 10px; top: 9px;
  width: 16px; height: 16px;
  color: var(--ink-muted);
  pointer-events: none;
}

.search-results {
  position: absolute;
  top: calc(100% + 6px);
  left: 0; right: 0;
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  overflow: hidden;
  max-height: 60vh;
  overflow-y: auto;
}
.search-results:empty { display: none; }
.search-result {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.search-result:last-child { border-bottom: 0; }
.search-result:hover, .search-result.active { background: var(--surface-hover); }
.search-result .kind {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  min-width: 44px;
}
.search-result .val { margin-left: auto; color: var(--ink-secondary); font-variant-numeric: tabular-nums; }
.search-result .exact {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: var(--accent-quiet);
  padding: 1px 6px;
  border-radius: 999px;
}
.search-empty { padding: var(--s4); color: var(--ink-muted); }

/* Freshness is part of the data, not decoration: the numbers are a snapshot and
   the page says how old it is and when the next one lands. */
.freshness {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: 12px;
  color: var(--ink-muted);
  white-space: nowrap;
}
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--good); flex: none; }
.dot.stale { background: var(--warning); }

a.icon-btn { color: var(--ink-secondary); text-decoration: none; }
a.icon-btn:hover { color: var(--ink); }

/* The countdown is now the whole visible indicator, beside a status dot. Tabular
   digits stop the header shifting sideways every second as glyph widths change.

   No left divider any more: it separated the countdown from "Updated 30m ago", and
   with that moved into the tooltip there is nothing on the other side of it. */
.countdown {
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}
.countdown:empty { display: none; }
.countdown.checking { color: var(--ink-secondary); }
.countdown.overdue { color: var(--warning); }

/* A refreshed page should be noticeable without being disruptive: one short wash
   over the content, not a skeleton flash. */
@keyframes data-refreshed {
  from { background: var(--accent-quiet); }
  to   { background: transparent; }
}
main.refreshed { animation: data-refreshed 900ms ease-out; }

/* Swapping a leaderboard tab or page keeps the old rows on screen until the new ones
   are ready, so the only thing signalling that a click landed is this. Short and
   shallow on purpose: deep enough to read as "working", nowhere near a blank.
   A cached response resolves inside one frame and never paints it at all. */
main { transition: opacity 90ms ease-out; }
main.swapping { opacity: 0.55; }

@media (prefers-reduced-motion: reduce) {
  main { transition: none; }
  main.refreshed { animation: none; }
}

.icon-btn {
  display: grid;
  flex: none;
  place-items: center;
  width: 34px; height: 34px;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink-secondary);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.icon-btn:hover { background: var(--surface-hover); color: var(--ink); }

/* ----------------------------------------------------------------- page --- */

main { max-width: var(--wide); margin: 0 auto; padding: var(--s6) var(--s5) var(--s7); }

.page-head { margin-bottom: var(--s5); }
.page-title {
  margin: 0;
  font-size: 26px;
  font-weight: 640;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.page-sub { margin: var(--s2) 0 0; color: var(--ink-muted); }
.eyebrow {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  margin-bottom: var(--s2);
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.card-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
  /* A chart card carries two segmented controls next to its title. That is wider
     than a phone, and without wrapping the row grows the card past the viewport and
     the whole page scrolls sideways. */
  flex-wrap: wrap;
}
.card-title { font-weight: 600; font-size: 14px; }
.card-body { padding: var(--s5); }

/* ------------------------------------------------------------ my folding --- */

/* A label/value list, for facts that are read rather than compared. A table would
   line the values up in a column nobody scans down; these are read one row at a
   time, so they wrap and each row stands alone. */
.kv { display: grid; gap: var(--s2); }
.kv-row {
  display: flex;
  gap: var(--s4);
  align-items: baseline;
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
.kv-row:last-child { border-bottom: 0; padding-bottom: 0; }
.kv-k { color: var(--ink-muted); font-size: 12px; min-width: 92px; flex: none; }
.kv-v { color: var(--ink); overflow-wrap: anywhere; }

/* A link that is really a button. Used where an action has to sit inside prose or a
   dense row without the weight of a button beside it. */
.linkish {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 13px;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.linkish:hover { filter: brightness(1.15); }
.add-machine { font-size: 13px; }
.dot-sep { color: var(--ink-muted); }
.code-field {
  width: min(30rem, 100%);
  padding: 6px var(--s3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font: inherit;
  font-family: var(--mono);
  font-size: 12px;
}
.code-field:focus { outline: none; border-color: var(--accent); }

/* One row per machine. A fleet is scanned, not read: the eye goes down the left edge
   looking for a dot that is not green, so the name and the state sit there and the
   output — the least urgent thing — sits furthest right. */
.machines { display: grid; gap: var(--s3); }
.machine {
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.mach-head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s2); }
.mach-name { font-weight: 620; }
.mach-rate { margin-left: auto; color: var(--ink-secondary); font-variant-numeric: tabular-nums; }
.mach-head .fold-actions { margin-left: var(--s3); }
.machine .bar-legend { font-size: 12px; }

/* Status and actions, above everything. The two commonest reasons to open this page
   are "is it still folding" and "make it stop", so they sit one glance apart. */
.fold-bar {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.fold-status { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.fold-state { font-weight: 620; font-size: 15px; }
.fold-actions { display: flex; gap: var(--s2); margin-left: auto; flex-wrap: wrap; }
.dot.good { background: var(--good); }
.dot.warn { background: var(--warning); }
.dot.idle { background: var(--ink-muted); }

/* The site's first real button. Deliberately not the segmented control the charts use:
   there, picking a segment changes what you are looking at, and here it changes what
   the computer is doing. Reusing one shape for both would make a destructive-adjacent
   action look like a view toggle. */
.btn {
  padding: 6px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface-raised);
  color: var(--ink-secondary);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.btn:hover { background: var(--surface-hover); color: var(--ink); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Only a state that wants fixing gets a primary. While it is folding, nothing needs
   doing and nothing should look like it does. */
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 560;
}
.btn-primary:hover { background: var(--accent); color: #fff; filter: brightness(1.08); }

.hero-row { display: flex; align-items: baseline; gap: var(--s4); margin-bottom: var(--s3); }
.hero-num { font-size: 34px; font-weight: 640; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.hero-side { margin-left: auto; color: var(--ink-secondary); font-variant-numeric: tabular-nums; }
.fold-meta { margin: var(--s4) 0 0; color: var(--ink-muted); font-size: 12px; overflow-wrap: anywhere; }
/* The specification: read once, then never again. */
.fold-spec { margin: 0; color: var(--ink-muted); font-size: 12px; overflow-wrap: anywhere; }

/* Work unit progress. The bar is the one place on this site where a number is worth
   drawing rather than printing: "61.4%" and a bar that is nearly two-thirds full say
   the same thing, but only one of them is readable at a glance from across a room,
   which is how a folding box is actually watched. */
.bar {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-hover);
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 400ms ease;
}
.bar-legend {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  margin-top: var(--s2);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) { .bar-fill { transition: none; } }

/* Paragraphs keep the browser's 1em margins, so every card holding prose cancelled
   them by hand: padding-bottom:0 on the body, margin-top:0 on the first paragraph,
   margin-bottom:0 on the last, repeated at twenty call sites. What that produced was
   a box padded 24px along the top and nothing along the bottom — the text sat on the
   border, and it read as though the card had been cut off mid-sentence. Collapsing
   the outer margins here restores symmetric padding everywhere at once, and a card
   that wants a different bottom gap can still say so. */
.card-body > :first-child { margin-top: 0; }
.card-body > :last-child { margin-bottom: 0; }

/* A link inside a paragraph has to look like one.
   The global rule is `color: inherit; text-decoration: none`, which is right for a
   leaderboard — a column of donor names is obviously clickable from context, and
   painting it accent-blue would be noise. In prose it is wrong: the link becomes
   indistinguishable from the sentence around it, revealed only on hover, which a
   touchscreen does not have. Anchors carrying a class keep their own styling. */
.card-body a:not([class]) {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Same reasoning: the footer mixes plain spans with links, so with no underline the
   two are the same text at the same weight in the same colour. */
footer a { text-decoration: underline; text-underline-offset: 2px; }

.section { margin-bottom: var(--s6); }
.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.section-title { font-size: 16px; font-weight: 620; letter-spacing: -0.01em; }
.section-link { margin-left: auto; color: var(--ink-muted); font-size: 13px; }
.section-link:hover { color: var(--accent); }

/* ----------------------------------------------------------- stat tiles --- */

/* A single number is a stat tile, not a chart. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
/* The basis switch for the Percentile tile. It sits above the grid because a tile is
   180px at its narrowest, and a two-way control inside one would crowd out the figure
   it qualifies. Right-aligned so it reads as a control over the block rather than as a
   heading for it. */
.stats-bar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--s3);
}
.stat { background: var(--surface); padding: var(--s4) var(--s5); }
.stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-muted);
  display: flex;
  align-items: center;
  gap: 6px;
}
.stat-value {
  margin-top: var(--s2);
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.15;
}
.stat-sub { margin-top: 2px; font-size: 12px; color: var(--ink-muted); font-variant-numeric: tabular-nums; }

.hero-value {
  font-size: 40px;
  font-weight: 660;
  letter-spacing: -0.03em;
  line-height: 1.05;
}

.delta { font-variant-numeric: tabular-nums; }
.delta.up { color: var(--good); }
.delta.down { color: var(--critical); }
.delta.flat { color: var(--ink-muted); }

/* In a stat tile the movement is the emphasis and the window is context, so only the
   arrow and count take the delta colour. */
.stat-move { color: var(--ink-muted); }
.stat-move .delta { font-weight: 600; }

/* The reader's own row in a rivals table. It travels inside the ranking rather than
   being pulled out above it, because the whole point of the view is seeing where you
   sit between the two people either side of you. A left rule and a raised surface
   locate it without making it shout. */
.rival-self > td { background: var(--surface-raised); }
.rival-self > td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
.rival-you {
  margin-left: var(--s2);
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: var(--accent-quiet);
  vertical-align: 1px;
}

/* Paging a rivals card keeps its rows on screen while the next page loads, the same
   trade the leaderboard tabs make: a skeleton inside a card collapses it and springs
   it back, which is a flicker in exchange for nothing. */
.card .swapping { transition: opacity 90ms ease-out; opacity: 0.55; }

/* Direction of an overtake. Never colour alone: the arrow says which way it goes and
   the text says when, so this reads the same in greyscale. */
.overtake { white-space: nowrap; font-variant-numeric: tabular-nums; }
.overtake .gain { color: var(--good); }
.overtake .loss { color: var(--critical); }
.overtake .muted { color: var(--ink-muted); }

/* Sortable column headings. These replaced a row of period tabs: once every numeric
   column can be ordered by, a separate control for four of them is a second way to say
   the same thing — and the one that silently omitted the column most people rank by.

   The whole heading is the target rather than a small glyph inside it, and the caret
   marks the active column so the ordering is never inferred from the numbers alone. */
.data th.sortable { padding: 0; }
.data th.sortable button {
  width: 100%;
  padding: 10px var(--s3);
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  font-weight: inherit;
  text-align: right;
  cursor: pointer;
  white-space: nowrap;
  border-radius: var(--radius-sm);
}
.data th.sortable button:hover { background: var(--surface-hover); color: var(--ink); }
.data th.sortable.active button { color: var(--ink); }
.sort-caret { margin-left: 4px; font-size: 10px; vertical-align: 1px; }

/* The column currently ordered by, emphasised so the ordering is legible as an
   ordering rather than looking arbitrary next to the lifetime total. */
.data td.metric { color: var(--ink); font-weight: 600; }

/* --------------------------------------------------------------- tables --- */

/* Deliberately NOT a scroll container on wide screens.
   `overflow-x: auto` makes this the nearest scrolling ancestor, which re-anchors
   the sticky header to the wrapper instead of the page — parking it 56px down
   inside the card and covering the first row completely. Horizontal scrolling is
   only needed on narrow viewports, so it is scoped there and the header goes
   static in exchange. */
.table-wrap { overflow-x: visible; }

table.data {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
table.data th {
  position: sticky;
  /* The header wraps to two or three rows on narrow windows, so its height is not a
     constant. --header-now is measured from the element itself; a fixed offset here
     tucks the sticky row underneath the header, which is invisible until someone
     scrolls a long table on a laptop. */
  top: var(--header-now, var(--header-h));
  z-index: 1;
  background: var(--surface);
  text-align: right;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
table.data th:first-child, table.data th.left { text-align: left; }
table.data td {
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  text-align: right;
  white-space: nowrap;
}
table.data td:first-child, table.data td.left { text-align: left; }
table.data tbody tr { transition: background 90ms ease; }
table.data tbody tr:hover { background: var(--surface-hover); }
table.data tbody tr:last-child td { border-bottom: 0; }

/* Prose inside a data table. The nowrap above is right for a leaderboard — a number
   broken across lines is unreadable, and every other table here is numbers — but
   wrong for a column of sentences, which is what the tool list on /agents is. Opt in
   per cell rather than relaxing the default. */
table.data td.wrap, table.data th.wrap { white-space: normal; }
table.data td.wrap { min-width: 24ch; }

.rank { color: var(--ink-muted); font-size: 13px; width: 1%; }
.name-cell {
  max-width: 34ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
}
/* Donor names are unvalidated upstream text and legitimately contain tabs and
   newlines; render them literally rather than letting whitespace collapse. */
.name-raw { white-space: pre; }

.muted { color: var(--ink-muted); }
.dim td { color: var(--ink-muted); }
.dim .name-cell { font-weight: 400; }

/* Production heat, from the aggregate bracket the API reports.
   ---------------------------------------------------------------------------
   The name itself carries the tier, so these are TEXT colours and are held to the
   WCAG text gate of 4.5:1 against their own surface — not the 2:1 that a decorative
   swatch beside the name would have needed. That gate is what sets both ends of
   each ramp, and it is why neither runs all the way to a pale or a near-black step.

   Each theme gets its own steps rather than one set reused, because the anchor
   flips. On dark, heat climbs toward light, so the hottest names are also the
   brightest and emphasis points the same way as magnitude. On light the ramp has to
   deepen instead, or the hot end would burn out against the page.

   The steps are sampled from inferno, the perceptually-uniform heat map, rather than
   picked by eye. It spans hues on purpose — that is what makes it read as heat — and
   it is not a rainbow: the thing that separates the two is that inferno is monotone
   in lightness, so the order survives greyscale, print and colour-blindness, while a
   rainbow's lightness wanders and the order does not. Lightness carries the ranking;
   hue only makes neighbouring steps easier to tell apart.

   Validated in both themes: every step >= 4.5:1, lightness monotone, adjacent OKLCH
   ΔL >= 0.06 so neighbouring tiers are separable. Both ramps are trimmed at both
   ends — the bright end short of white, the dark end well above black — because
   inferno's extremes lose all chroma and stop looking like a colour at all.

   Colour is never the only encoding: points_per_day_7d_avg, the figure the tier is
   derived from, is a column in the same row. */
.tier-name { font-weight: 500; }

/* The streak flame climbs the same ramp. Its colour is decoration — the day count sits
   beside it in the same tile — so it is never the only encoding, and inheriting the
   validated text steps means it clears the text gate rather than the looser one a
   decorative mark would have been held to. */
.streak { display: inline-flex; align-items: center; gap: var(--s2); }
.flame { width: 0.85em; height: 0.85em; flex: none; }

/* Idle. Not part of the ramp — no production is not the coldest heat, it is the
   absence of any, so it takes the muted ink token and recedes. */
.tier-0 { color: var(--ink-muted); }

.tier-1 { color: #de5437; }
.tier-2 { color: #f0701f; }
.tier-3 { color: #f99109; }
.tier-4 { color: #fcb217; }
.tier-5 { color: #f8d446; }
.tier-6 { color: #fbef82; }

:root[data-theme="light"] .tier-1 { color: #ce4447; }
:root[data-theme="light"] .tier-2 { color: #b1325a; }
:root[data-theme="light"] .tier-3 { color: #8f2468; }
:root[data-theme="light"] .tier-4 { color: #6b1c6d; }
:root[data-theme="light"] .tier-5 { color: #4c0d6a; }
:root[data-theme="light"] .tier-6 { color: #2c0d56; }

/* Forced-colours and print drop author colours entirely, taking the whole encoding
   with them. The per-day column still carries it, which is the reason that column
   is not optional. */
@media (forced-colors: active) {
  .tier-name { forced-color-adjust: none; color: LinkText; }
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  border: 1px solid var(--line);
  color: var(--ink-secondary);
  white-space: nowrap;
}
.badge.warn { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 35%, transparent); }

/* ------------------------------------------------------------ pagination --- */

.pager {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s4) var(--s5);
  border-top: 1px solid var(--line);
}
.pager button {
  height: 30px;
  min-width: 30px;
  padding: 0 var(--s3);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink-secondary);
  font: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.pager button:hover:not(:disabled) { background: var(--surface-hover); color: var(--ink); }
.pager button[aria-current="true"] { background: var(--accent-quiet); border-color: transparent; color: var(--accent); }
.pager button:disabled { opacity: 0.35; cursor: default; }
.pager .count { margin-left: auto; color: var(--ink-muted); font-size: 12px; font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------- chart --- */

.chart { width: 100%; height: 300px; }
/* An empty chart should not leave a 300px hole in the page. */
.chart.is-empty { height: 0; }
.chart-note {
  padding: var(--s2) var(--s5) var(--s3);
  font-size: 11px;
  color: var(--ink-muted);
}
.chart-empty { padding: var(--s6) var(--s5); text-align: center; color: var(--ink-muted); }
.chart-toolbar {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
}
.seg {
  display: inline-flex;
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.seg button {
  padding: 5px 11px;
  background: none;
  border: 0;
  border-right: 1px solid var(--line);
  color: var(--ink-muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.seg button:last-child { border-right: 0; }
.seg button:hover { background: var(--surface-hover); color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--accent-quiet); color: var(--accent); }

/* A segmented control long enough to outgrow the page: seven client names do not fit
   a phone. It scrolls sideways rather than wrapping, the same choice the nav makes for
   the same reason — wrapping would break the pill into ragged rows and the rounded
   clip and per-button dividers only read correctly on one line. */
.tab-strip { max-width: 100%; overflow: hidden; }
.seg-tabs {
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.seg-tabs::-webkit-scrollbar { display: none; }
.seg-tabs button { white-space: nowrap; flex: none; }

/* Legend is always present for two or more series, so identity is never carried
   by colour alone. */
.legend { display: flex; flex-wrap: wrap; gap: var(--s4); padding: var(--s3) var(--s5) 0; }
.legend-item {
  display: flex; align-items: center; gap: 7px;
  font-size: 12px; color: var(--ink-secondary);
  /* A button when it toggles, a div when it does not; the reset keeps the two
     looking identical so the legend does not change shape between charts. */
  background: none; border: 0; padding: 0; font-family: inherit; line-height: inherit;
}
button.legend-item { cursor: pointer; }
button.legend-item:hover { color: var(--ink); }
/* Switched off: the row stays legible and keeps its position, so the way back is
   where the reader left it. The swatch hollows out rather than vanishing — an empty
   outline still says which colour it was. */
.legend-item.off { color: var(--ink-muted); text-decoration: line-through; }
.legend-item.off .swatch { background: none !important; box-shadow: inset 0 0 0 1px var(--ink-muted); }
.swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }

/* The crosshair, restyled.
 *
 * uPlot ships it as `1px dashed #607D8B` from its own stylesheet — one hard-coded
 * blue-grey for both themes, and a hairline dashed border landing on fractional
 * device pixels, which is the kind of thing browsers disagree about rendering at all.
 * Reported invisible in Safari.
 *
 * So it is ours: solid rather than dashed, 2px so no rounding can drop it, and in a
 * theme variable rather than one hex for both.
 *
 * --ink-muted, chosen by measuring rather than by name: it clears 3:1 against both
 * surfaces (4.85:1 dark, 3.50:1 light) without the 9.7:1 of --ink-secondary, which
 * would put a crosshair louder than the data it points at. --line-strong was the
 * obvious-sounding pick and is 1.48:1 — fainter than the uPlot default it replaces.
 *
 * It matters more than it looks: the hover dots were removed on the grounds that this
 * line already shows the position, so if it does not draw there is nothing left
 * pointing at anything.
 */
.u-cursor-x {
  /* Not `.u-hz .u-cursor-x` like uPlot's own rule. Its height and its border both
     come from that descendant selector and nowhere else, so if the orientation class
     is ever missing the element still exists — positioned, zero-sized, borderless,
     and invisible. Setting both here means the line does not depend on a class we do
     not control. */
  height: 100% !important;
  border-right: 2px solid var(--ink-muted) !important;
}
/* The horizontal half is off in cursor(); belt and braces if that ever changes. */
.u-cursor-y { border-bottom: 0 !important; }

/* The plot responds to the pointer, so the pointer should say so. uPlot sets no
   cursor style on the hit area, which leaves an arrow over a chart that has a
   crosshair and a tooltip waiting behind it. */
.u-over { cursor: crosshair; }

.u-tooltip {
  position: absolute;
  z-index: 20;
  pointer-events: none;
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  padding: var(--s2) var(--s3);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  transform: translate(-50%, calc(-100% - 12px));
  opacity: 0;
  transition: opacity 90ms ease;
}
.u-tooltip.on { opacity: 1; }
.u-tooltip .t-date { color: var(--ink-muted); margin-bottom: 3px; }
.u-tooltip .t-row { display: flex; align-items: center; gap: 7px; }
.u-tooltip .t-val { margin-left: auto; font-weight: 600; }
.u-tooltip .t-none { color: var(--ink-muted); }

/* ----------------------------------------------------------------- tabs --- */

.tabs {
  display: flex;
  gap: var(--s1);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  padding: var(--s3) var(--s4);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--ink-muted);
  font: inherit;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
.tab .tab-val { color: var(--ink-muted); font-size: 12px; margin-left: 6px; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- states --- */

.skeleton {
  background: linear-gradient(90deg, var(--surface) 25%, var(--surface-hover) 37%, var(--surface) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.3s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }

.empty, .error {
  padding: var(--s7) var(--s5);
  text-align: center;
  color: var(--ink-muted);
}
.error { color: var(--critical); }

.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-left: 2px solid var(--warning);
  border-radius: var(--radius-sm);
  color: var(--ink-secondary);
  font-size: 13px;
}

/* minmax(0, …) rather than 1fr: a grid item's default min-width is auto, which is
   min-content, so a wide table refuses to shrink and pushes the whole page into
   horizontal scroll instead of scrolling inside its own wrapper. */
.grid-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--s5); }
.breadcrumb { display: flex; align-items: center; gap: var(--s2); color: var(--ink-muted); font-size: 13px; margin-bottom: var(--s3); }

footer {
  max-width: var(--wide);
  margin: 0 auto;
  padding: var(--s5);
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: 12px;
  display: flex;
  gap: var(--s4);
  flex-wrap: wrap;
}

/* ------------------------------------------------------------------ home --- */

.home {
  display: grid;
  /* The article column is sized for reading, not for filling the window: letting it
     take 1fr on a wide screen leaves a 400px gulf between the text and the rail,
     because the prose is capped at a comfortable measure anyway. Sizing the column
     to the measure and centring the pair keeps them together. */
  grid-template-columns: minmax(0, 68ch) 320px;
  gap: var(--s6);
  justify-content: center;
  align-items: start;
}
.home-side {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  position: sticky;
  top: calc(var(--header-now, var(--header-h)) + var(--s5));
}

.rail-stat { padding: var(--s3) 0; border-bottom: 1px solid var(--line); }
.rail-stat:first-child { padding-top: 0; }
.rail-stat:last-child { padding-bottom: 0; border-bottom: 0; }
.rail-label { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted); }
.rail-value { font-size: 22px; font-weight: 600; margin-top: 2px; color: var(--ink); }
.rail-sub { font-size: 12px; color: var(--ink-muted); margin-top: 2px; }
.rail-note { margin: 0 0 var(--s3); color: var(--ink-secondary); font-size: 13px; line-height: 1.55; }

.post { max-width: 68ch; }
.post-title { font-size: 30px; font-weight: 650; letter-spacing: -0.02em; line-height: 1.2; color: var(--ink); }
.post-date { display: block; margin-top: var(--s2); color: var(--ink-muted); font-size: 13px; }
.post-page { margin-top: var(--s3); }

.post-list { display: flex; flex-direction: column; }
.post-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s4);
  padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line);
}
.post-row:last-child { border-bottom: 0; }
.post-row:hover { background: var(--surface-hover); }
.post-row-title { color: var(--ink); font-weight: 500; }
.post-row-date { color: var(--ink-muted); font-size: 12px; white-space: nowrap; }

/* Prose: measured for reading rather than for filling the column. */
.prose { margin-top: var(--s5); color: var(--ink-secondary); font-size: 15px; line-height: 1.7; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose h2 {
  margin: var(--s6) 0 var(--s3);
  font-size: 19px; font-weight: 620; letter-spacing: -0.01em; color: var(--ink);
}
.prose h3 { margin: var(--s5) 0 var(--s2); font-size: 16px; font-weight: 600; color: var(--ink); }
.prose p { margin: 0 0 var(--s4); }
.prose strong { color: var(--ink); font-weight: 600; }
.prose a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.prose ul, .prose ol { margin: 0 0 var(--s4); padding-left: var(--s5); }
.prose li { margin-bottom: var(--s2); }
.prose code {
  font-family: var(--mono); font-size: 13px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 4px; padding: 1px 5px;
}
.prose pre {
  margin: 0 0 var(--s4); padding: var(--s4);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  overflow-x: auto;                 /* long lines scroll here, not on the page */
  font-size: 13px; line-height: 1.6;
}
.prose pre code { background: none; border: 0; padding: 0; font-size: inherit; }

/* Code outside prose — the connect snippets on /agents. Same treatment as a prose
   block, but reachable from a view that builds its own markup rather than rendering
   Markdown. Scrolls rather than wraps: a command broken across lines is a command
   somebody pastes wrong. */
.code-block {
  margin: var(--s3) 0;
  padding: var(--s3) var(--s4);
  background: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  font-size: 12px;
  line-height: 1.6;
}
.code-block code { background: none; border: 0; padding: 0; font-size: inherit; white-space: pre; }
.prose blockquote {
  margin: 0 0 var(--s4); padding-left: var(--s4);
  border-left: 2px solid var(--line); color: var(--ink-muted);
}
.prose hr { margin: var(--s6) 0; border: 0; border-top: 1px solid var(--line); }

/* The header becomes two rows rather than dropping what does not fit. Freshness is
   the only thing on the page that changes by itself, so hiding it on a narrow window
   removes the one piece of live information there — and the countdown is precisely
   what a reader should not have to widen a window to find.

   This fires earlier than the content breakpoint because the header runs out of room
   first: brand, nav, search, freshness and the toggle stop fitting around 1050px,
   where the toggle was being pushed off the right edge. */
@media (max-width: 1080px) {
  .header {
    flex-wrap: wrap;
    height: auto;
    min-height: var(--header-h);
    padding-top: var(--s2);
    padding-bottom: var(--s2);
    row-gap: var(--s2);
  }
  /* Basis zero, not a width. Flexbox breaks lines on each item's *hypothetical*
     size — its basis — and only shrinks within a line afterwards. Any non-zero basis
     therefore forces a wrap at some width even though the item would have shrunk to
     fit: at 640-680px a 160px basis overflowed the row by a few pixels, pushed the
     34px toggle onto a line of its own, and the search then expanded to fill the row
     it had been left alone on. With basis 0 the search only ever claims space that
     is already spare, so it can never be the reason something else wraps. */
  .search { margin-left: auto; flex: 1 1 0; min-width: 0; }
  .freshness {
    order: 10;            /* after the toggle, whatever the source order */
    flex-basis: 100%;     /* claims a line of its own */
    flex-wrap: wrap;      /* and degrades to two on a phone */
    padding-top: var(--s2);
    border-top: 1px solid var(--line);
  }

  /* A data table's min-content width is 500px, and side-by-side columns are
     (viewport - 72) / 2 — so two of them stop fitting below 1072px and the table
     spills past the viewport rather than shrinking. The wrapper only becomes a
     scroll container lower down (making it one earlier would re-break sticky
     headers, which anchor to the nearest scroll container). Collapsing to one
     column here is what actually fits. */
  .grid-2 { grid-template-columns: minmax(0, 1fr); }

  /* The rail stops being a rail: below the article, full width, not sticky. */
  .home { grid-template-columns: minmax(0, 1fr); justify-content: stretch; }
  .home-side { position: static; }
  .post { max-width: none; }
}

@media (max-width: 900px) {
  /* Scroll shadows: without them a horizontally clipped table just looks like
     broken layout, and nobody discovers the columns to the right. The `local`
     gradients cover the shadows when scrolled to an edge, so the cue only appears
     when there is actually more to see. */
  .table-wrap {
    overflow-x: auto;
    background:
      linear-gradient(to right, var(--surface) 40%, transparent) left center / 32px 100% no-repeat local,
      linear-gradient(to left, var(--surface) 40%, transparent) right center / 32px 100% no-repeat local,
      radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.4), transparent) left center / 12px 100% no-repeat scroll,
      radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.4), transparent) right center / 12px 100% no-repeat scroll;
  }
  table.data th { position: static; }
  /* Names give up room first: the numbers are the reason for the table. */
  .name-cell { max-width: 18ch; }
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
  .header { padding-left: var(--s4); padding-right: var(--s4); }
  main { padding: var(--s5) var(--s4) var(--s6); }
}
/* The nav takes a row of its own rather than disappearing.

   It used to be display:none below 620px, which left a phone with a search box, a
   clock and two icons and no way to reach any page at all — the links are the one
   thing in this header that cannot be dropped. Below about 360px the five items are
   wider than the screen, so the row scrolls sideways rather than wrapping into a
   ragged block.

   The breakpoint is 800px, not the width where the nav stops fitting, because the nav
   is what squeezes everything else: with it inline the search box collapsed to 16-136px
   between 640 and 780 — present, focusable and far too narrow to use — and the GitHub
   icon was pushed onto a row of its own around 630. Moving the nav off first leaves the
   remaining row with room for a real search box. */
@media (max-width: 800px) {
  .nav {
    order: 6;                 /* after the search, before the freshness line */
    flex-basis: 100%;
    gap: 2px;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav a { padding: 6px 10px; white-space: nowrap; }
}

/* On a phone the search box can no longer share a row with the brand without
   squeezing the toggle onto a line of its own — a 34px button with a whole row to
   itself. Giving search the row instead spends the same vertical space on the thing
   that benefits from the width. */
/* On a phone the wordmark goes and the search keeps its place on the first row.

   Giving search a row of its own instead costs a fourth header row — and with the nav
   now needing one, that put a sticky header at 169px, a fifth of the screen, on every
   page. The diamond still links home, which is the part that was doing the work. */
@media (max-width: 480px) {
  .brand-name { display: none; }
  .header-actions { margin-left: auto; }

  /* Six entries do not fit a 380px screen at the wider spacing, and the nav scrolls
     horizontally with a hidden scrollbar — so the last one is simply not there as far
     as a reader is concerned. That is the exact failure that moving this link out of
     an icon was meant to fix, so the row gives up padding rather than an entry. */
  .header { padding: 0 var(--s4); }
  .nav { gap: 0; }
  .nav a { padding: 6px 8px; font-size: 13px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
