/*
 * The styling serves one thing: the order in which an answer is read is
 * part of the contract. Data completeness and caveats stand above the
 * numbers and look more prominent than them, because a warning read
 * after the result arrives at an already-formed conclusion.
 *
 * The shell's decisions follow from that. Colour belongs to the data:
 * the interface is grey, there is one accent and it means "click here",
 * and everything else coloured is either a chart or a caveat. Depth is
 * made with a line rather than a shadow: shadows are left only to
 * floating layers, which need to detach from the page. One radius for
 * everything — rounding is not a mark of hierarchy.
 *
 * The rules in full, together with the reasons, are in DESIGN.md.
 */

:root {
  color-scheme: light dark;

  --bg: #f4f4f2;
  --panel: #ffffff;
  --rail: #fafaf8;
  --line: #e2e2dc;
  --line-strong: #cfcfc7;

  /*
   * Three steps of text, not two. The third is for axis labels, units
   * and utility text: set in the same grey as secondary text, they
   * compete with it for attention.
   *
   * The steps are close together rather than contrasting. The hierarchy
   * here is a hint about reading order, and if it is more noticeable
   * than the text itself, an axis label reads as disabled rather than as
   * utility.
   *
   * Body text on light is not black: the page is read for hours, and
   * pure black on white tires over that span. The third step meanwhile
   * stays where it was: it is set at eleven pixels, and lightening it
   * further stops being hierarchy and becomes illegible.
   */
  --text: #33332e;
  --muted: #66665e;
  --faint: #7d7d76;

  --accent: #2f5d9e;
  --accent-hover: #24497d;
  --accent-soft: #ecf1f8;

  /*
   * Hover and selection are one colour at different strengths. Darkening
   * in their place would read as "pressed and stuck down" rather than as
   * "this can be clicked".
   */
  --hover: #eef2f9;
  --chosen: #dee8f5;

  /*
   * The filter's two poles: "only this" and "all but this". Separate
   * colours rather than `--accent` and `--error`, and the reason is not
   * the hue. These two are a pair, read against each other a centimetre
   * apart, and set in the interface's muted blue and a caveat's red they
   * are told apart worse than they should be: inclusion and exclusion
   * are opposite answers, not two shades of one.
   *
   * They are more saturated than their neighbours for another reason
   * too: they carry white text — here the fill is not a backdrop for the
   * colour but the colour itself.
   */
  --include: #1f62c9;
  --exclude: #c62f1c;

  /*
   * The backdrop of an open group is the same colour one step below
   * hover. Below rather than level with it: people hover and select on
   * top of it, and level with hover an open group would swallow the
   * response to the cursor.
   */
  --group-open: #f4f6fb;

  --fast: 0.1s;
  --mid: 0.2s;
  --ease: cubic-bezier(0.645, 0.045, 0.355, 1);

  --warn-bg: #fdf4e3;
  --warn-line: #d9a441;
  --warn-text: #6b4a0c;
  --error: #a32b1c;
  --ok: #2f6b3a;
  --missing: #a8a8a0;

  /*
   * Pale backdrops for the same three states. Separate colours rather
   * than transparency derived from `--ok` and `--error`: a notification
   * lands on a light page and on a dark one, and a semi-transparent red
   * on a panel reads as grime rather than as a caveat. A companion to
   * `--warn-bg`, which has been here from the start.
   */
  --ok-bg: #eaf4ec;
  --ok-line: #4b8f5e;
  --error-bg: #fbeceb;
  --error-line: #c0563f;

  --radius: 8.4px;

  /*
   * The composer's own rounding, and the only one besides `--radius`
   * that a block on this page gets. The reason is not hierarchy: this is
   * the single thing on the screen that is typed into, and an outline
   * unlike any bordered block above it tells the field from them before
   * a word is read.
   */
  --composer-radius: 26px;

  /*
   * The height of a filter control. One for every kind of them — the
   * range, a text field, the dimension filter, a flag — because they
   * stand side by side in one row, and a control half a step taller
   * than its neighbour reads as belonging to a different form.
   */
  --control: 34px;

  --rail-width: 244px;

  /*
   * The height of a page-grid row. One for all cards: a row of tiles of
   * differing heights reads as several pages in a row rather than as
   * one. The value is sized for a chart with an axis and labels, not for
   * the smallest distinguishable picture.
   */
  --card-row: 264px;

  /*
   * The backdrop behind an expanded card. Not a transparent black over
   * the page but its own colour per theme: on a dark page a black veil
   * does not separate the layer, it merely dims everything around.
   */
  --backdrop: rgb(28 28 26 / 34%);

  /*
   * The map's ramp, given by its two ends: the country with the fewest
   * players is `--map-weak`, the one with the most is `--map-strong`,
   * and everything between them is mixed by share. Both ends are the
   * accent — one hue and not a rainbow, because here the colour is a
   * quantity rather than a category, and a red-to-green ramp reads as
   * "bad to good", turning a count of players into a verdict about them.
   *
   * The pale end is still noticeably blue: it must not be mistaken for
   * `--map-blank`, which means "no rows at all" rather than "the
   * fewest".
   */
  --map-weak: #e0eaf6;
  --map-strong: #24497d;

  --map-blank: #e8e8e4;
  --map-line: #ffffff;

  /*
   * The top bar has one height on every screen and in every state: it
   * holds either the full logo or the mark with the game icon, and a bar
   * that changed height to follow its contents would move the whole page
   * on every entry into a game and exit from it.
   */
  --top-height: 50px;

  --shadow: 0 8px 24px rgb(28 28 26 / 12%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #131416;
    --panel: #1b1d20;
    --rail: #17181a;
    --line: #2b2e33;
    --line-strong: #3a3e45;

    --text: #dcdcd8;
    --muted: #a6a69f;
    --faint: #84847c;

    --accent: #7aa5e0;
    --accent-hover: #97b9e9;
    --accent-soft: #1e2732;

    --hover: #202b38;
    --chosen: #27354a;
    --group-open: #1b2128;

    /* on dark the same two, but lighter: they carry white text */
    --include: #5a9bf0;
    --exclude: #e0685a;

    --warn-bg: #2f2716;
    --warn-line: #c69a3f;
    --warn-text: #e8cf9b;
    --error: #e08b7f;
    --ok: #8ec79b;
    --missing: #5c5f66;

    --ok-bg: #1a2a1f;
    --ok-line: #5f9d72;
    --error-bg: #2f1d1a;
    --error-line: #c07a6e;

    --backdrop: rgb(6 7 9 / 62%);

    /*
     * The ramp runs the other way on dark: the more, the lighter. Same
     * hue and the same two ends — what changes is which of them reads as
     * "a lot", and on a dark page that is the light one.
     *
     * The pale end is a good deal lighter than the page and the blank a
     * good deal darker. On dark it is those two that run together: "the
     * fewest players" and "no players at all" both come out near-black,
     * and on a country the size of Belgium two dark greys cannot be told
     * apart at all.
     */
    --map-weak: #2f4a66;
    --map-strong: #a8cef5;

    --map-blank: #23262a;
    --map-line: #1b1d20;

    --shadow: 0 8px 24px rgb(0 0 0 / 45%);
  }
}

* {
  box-sizing: border-box;
}

/*
 * Transitions on colour and border only: offsets and sizes are not
 * animated, otherwise a table row under the cursor starts to travel —
 * and at that moment somebody is reading a number off it.
 */
button,
select,
input,
textarea,
.tool,
.icon,
.rail-item,
summary,
tr.clickable td {
  transition:
    background-color var(--fast) var(--ease),
    border-color var(--fast) var(--ease),
    color var(--fast) var(--ease);
}

/*
 * A floating layer appears rather than pops into being: it covers the
 * page, and an instantaneous swap reads as a redraw rather than as an
 * opening.
 */
@keyframes appear {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

/* motion is decoration, and whoever turned it off does not need it */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/*
 * The font sits next to the page rather than being pulled from a CDN:
 * the page goes nowhere but to itself (see the CSP in ui.ts), and the
 * font is no exception.
 *
 * Variable, but on weight only: the source file's other three axes
 * (width, optical size, YTLC) are pinned to their defaults, and the
 * character set is narrowed to Latin plus Cyrillic. 570 KB of source TTF
 * become 64 KB — and that is not optimization for its own sake but the
 * difference between "the page opened" and "the page opened half a
 * second later" on a first visit.
 *
 * `swap`: before it loads, text is drawn in a system font rather than in
 * nothing. An interface whose captions are invisible for the first half
 * second looks broken rather than loading.
 */
@font-face {
  font-family: "Nunito Sans";
  src: url("/nunito-sans.woff2") format("woff2-variations");
  font-weight: 200 1000;
  font-style: normal;
  font-display: swap;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);

  font: 15px/1.5 "Nunito Sans", ui-sans-serif, system-ui, -apple-system,
    "Segoe UI", sans-serif;
}

code,
pre,
.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
}

/* ---------- shell ---------- */

/*
 * A full-width bar on top, navigation filling the remaining height. The
 * left block and the page scroll separately: a shared scroll would carry
 * the navigation upwards along with a long answer feed.
 */
.shell {
  display: grid;
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  grid-template-rows: var(--top-height) minmax(0, 1fr);
  height: 100dvh;
}

.shell[hidden] {
  display: none;
}

.top {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 16px;
  flex: none;
  height: var(--top-height);
  padding-right: 16px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

/*
 * The logo holds the column above the left block and is separated from
 * the sections by the same line that separates the left block from the
 * page: the line runs from the top of the window to the bottom as one
 * stroke, and the bar stops looking laid on top of the navigation.
 *
 * The width is computed including the border — `box-sizing: border-box`
 * is global at the top — so the line sits exactly above the left block's
 * line rather than a pixel to the right.
 */
.brand {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  width: var(--rail-width);
  height: 100%;
  padding: 0 12px;
  border-right: 1px solid var(--line);
}

/*
 * The full logo appears only where no game is selected. It is clickable
 * there too, even though it leads to this same screen: the response to
 * the cursor is itself the answer "you are already here", and a logo
 * without one would read as broken.
 */
.logo-link {
  display: inline-flex;
  align-items: center;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  cursor: pointer;
}

.logo-link:hover {
  background: var(--hover);
  border-color: transparent;
}

/*
 * The logo is given a height rather than a width: the file has margins
 * around the artwork, and sized by width it would come out at a
 * different scale every time.
 */
.brand .logo img {
  display: block;
  height: 34px;
  width: auto;
}

/*
 * The mark is the "to all games" button. Its artwork is its own, with
 * rounding and a backdrop, so all it needs from a button is the response
 * to the cursor.
 */
.logo-mark {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  cursor: pointer;
}

.logo-mark:hover {
  background: var(--hover);
  border-color: transparent;
}

.logo-mark img {
  display: block;
  width: 30px;
  height: 30px;
}

/*
 * The open game is the switch-game button. It takes up the rest of the
 * column: game names differ in length, and a button sized to its
 * contents would move the chevron on every switch.
 */
.game-pick {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.game-pick:hover {
  background: var(--hover);
  border-color: transparent;
}

.game-pick .game-tile {
  flex: none;
  display: flex;
}

/*
 * The game name on one line: a long one is ellipsised rather than
 * wrapped — wrapping would carry the second line below the bar's rule.
 */
.game-pick .game-name {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.game-pick .chevron {
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--faint);
}

/*
 * The sections that have no place of their own in the left navigation:
 * they are not about "what is happening" but about "check it by hand".
 * They stand in a row after the logo, that is they begin exactly where
 * the left block ends.
 */
.tools {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.tool {
  border: 1px solid transparent;
  background: none;
  color: var(--muted);
  padding: 5px 10px;
  border-radius: var(--radius);
  cursor: pointer;
  font: inherit;
  white-space: nowrap;
}

.tool:hover {
  color: var(--text);
  background: var(--hover);
}

.tool.active {
  color: var(--text);
  background: var(--chosen);
  border-color: transparent;
}

.top-right {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;

  /*
   * It pushes itself to the right rather than relying on a spacer on the
   * left: on the game list there are no sections at all, and without
   * this the bar would bunch up on the left.
   */
  margin-left: auto;
}

/*
 * The bar without navigation — on the game list. There the logo does not
 * hold a column above the left block, because there is no left block;
 * there is no line to its right either — it would have nothing to
 * continue and would read as the edge of an empty column.
 */
.top-plain .brand {
  width: auto;

  /* the button's padding is absorbed by the margin: the logo sits where it would without it */
  padding-left: 10px;
  border-right: none;
}

.icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.icon:hover {
  color: var(--accent);
  background: var(--hover);
  border-color: transparent;
}

.icon svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
}

/*
 * An icon button that is also a section: game settings. It takes its
 * dimensions from an icon and its "we are here" marker from a section,
 * so it does not need a section's padding.
 */
.icon.tool {
  padding: 6px;
}

.icon.tool.active {
  color: var(--accent);
  background: var(--chosen);
}

/* ---------- left navigation ---------- */

.rail {
  grid-column: 1;
  overflow-y: auto;
  padding: 12px;
  background: var(--rail);
  border-right: 1px solid var(--line);
}

/* the groups are `details`, and their shared size is set small (see below) */
.rail-group {
  font-size: inherit;
}

.rail-group + .rail-group {
  margin-top: 18px;
}

/*
 * An open group is filled in full, heading included: otherwise the list
 * hangs below the row on its own, and the background gives no clue where
 * it ends and the next group begins.
 */
.rail-group[open] {
  padding-bottom: 4px;
  background: var(--group-open);
  border-radius: var(--radius);
}

/*
 * A group's heading is also its collapse button. The chevron sits right
 * after the name rather than at the edge of the rail: at the edge it
 * would read as a separate control, whereas what collapses is the
 * heading itself.
 *
 * The native `details` marker is removed: it is drawn on the left and
 * takes a different shape in every browser.
 *
 * The row is no shorter than a list item, even though its lettering is
 * smaller: a collapsed group is this single row, and the mouse has to
 * hit it as comfortably as it hits a conversation below.
 *
 * The left indent is shared by the heading, the items and the "empty"
 * line — 14px from the edge of the backdrop: the hover fill stretches
 * the full width, and what keeps the rows on one vertical is not the
 * fill but this indent.
 */
.rail-group > summary {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: 32px;
  margin-bottom: 4px;
  padding: 6px 8px 6px 14px;
  border-radius: var(--radius);
  color: var(--faint);
  list-style: none;
}

.rail-group > summary::-webkit-details-marker {
  display: none;
}

.rail-group > summary:hover {
  color: var(--text);
  background: var(--hover);
}

.rail-group > summary::after {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentcolor;
  border-bottom: 1.5px solid currentcolor;

  /* rotating about the centre lifts the chevron — hence the offset */
  transform: translateY(-1px) rotate(-45deg);
  transition: transform var(--fast) var(--ease);
}

.rail-group[open] > summary::after {
  transform: translateY(-2px) rotate(45deg);
}

.rail-group h2 {
  margin: 0;
  padding: 0;
  font-size: 11px;
  font-weight: 600;
  color: inherit;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.rail-item {
  display: block;
  width: 100%;
  text-align: left;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font: inherit;

  /* 13 rather than 14: an item has its own one-pixel border, a heading does not */
  padding: 6px 8px 6px 13px;
  cursor: pointer;

  /* a long question in the history is clipped; clicking opens it in full */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rail-item:hover {
  color: var(--text);
  background: var(--hover);
}

.rail-item.active {
  color: var(--text);
  background: var(--chosen);
  border-color: transparent;
  font-weight: 500;
}

.rail-item .sub {
  color: var(--faint);
  font-size: 12px;
}

/*
 * A conversation is a row rather than a single item: renaming and
 * deleting belong to it, and both are buttons of their own — a button
 * inside a button is not a thing.
 *
 * The hover fill is driven from the row, not from the item: with the
 * cursor on one of those buttons the item is no longer hovered, and the
 * row would go pale exactly while it is being clicked.
 */
.rail-row {
  position: relative;
}

.rail-row:hover .rail-item {
  color: var(--text);
  background: var(--hover);
}

.rail-row:hover .rail-item.active {
  background: var(--chosen);
}

/*
 * The buttons lie over the end of the name rather than taking width
 * away from it: the rail is narrow, and space held for them permanently
 * would shorten every name always for the sake of two rare actions.
 * The name goes under them through a fade in the row's own colour —
 * hence the two gradients: an open conversation is filled differently
 * from a hovered one, and a fade into the wrong colour reads as a
 * smudge over the letters.
 */
.rail-acts {
  position: absolute;
  top: 1px;
  right: 1px;
  bottom: 1px;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 4px 0 18px;
  border-radius: var(--radius);
  background: linear-gradient(to right, transparent, var(--hover) 18px);

  /*
   * Hidden by transparency rather than by `display`: the buttons stay
   * on the keyboard's path, and `:focus-within` shows them to whoever
   * arrives there by Tab.
   */
  opacity: 0;
  pointer-events: none;
}

/* while renaming or confirming, they are gone rather than transparent */
.rail-acts[hidden] {
  display: none;
}

.rail-row:has(.rail-item.active) .rail-acts {
  background: linear-gradient(to right, transparent, var(--chosen) 18px);
}

.rail-row:hover .rail-acts,
.rail-row:focus-within .rail-acts {
  opacity: 1;
  pointer-events: auto;
}

.rail-act {
  width: 24px;
  height: 24px;
}

.rail-act svg {
  width: 15px;
  height: 15px;
}

/*
 * The trash can is red standing still, not only under the cursor. That
 * is the one place colour is not given to the data here, and the reason
 * is the same one: red is the state of the action — it does not come
 * back — and a colour that appears only on hover announces that after
 * the aim has been taken.
 */
.rail-act.danger,
.rail-act.danger:hover {
  color: var(--error);
}

.rail-act.danger:hover {
  background: var(--error-bg);
}

/*
 * The title is edited in place, in the list, and the question about
 * deleting is asked there too. Both lie over the row rather than
 * replacing it: the row is two lines tall and either control is one, so
 * standing in its place they would pull every name below up by a line —
 * the very names the one being renamed is told apart from.
 */
.rail-rename,
.rail-confirm {
  position: absolute;
  inset: 1px;
  margin: 0;
}

.rail-rename {
  padding: 5px 8px 5px 12px;
  border-color: var(--accent);
}

/* worded rather than armed: two identical trash cans, one of which deletes, are worse than an extra word */
.rail-confirm {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px 4px 2px 13px;
  border-radius: var(--radius);
  background: var(--hover);
  color: var(--muted);
  font-size: 13px;
}

/*
 * On one line, always. The rail is narrow, and a question wrapped onto
 * two lines is taller than the row it covers — the answer to a click
 * would be the list jumping.
 */
.rail-confirm span {
  margin-right: auto;
  white-space: nowrap;
}

.rail-confirm button.link {
  padding: 4px;
}

button.link.danger,
button.link.danger:hover:not(:disabled) {
  color: var(--error);
}

button.link.danger:hover:not(:disabled) {
  background: var(--error-bg);
}

.rail-empty {
  padding: 4px 8px 4px 14px;
  color: var(--faint);
  font-size: 13px;
}

/*
 * "New conversation" is the only action button in the navigation, which
 * is why it is the only thing here filled with the accent.
 */
.new-chat {
  width: 100%;
  margin-bottom: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

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

main {
  grid-column: 2;
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
}

.view {
  min-height: 0;
  overflow-y: auto;
  padding: 24px 32px 40px;
}

.view > * + * {
  margin-top: 16px;
}

/* cards one below another: a page column rather than a grid of tiles */
.stack > * + * {
  margin-top: 16px;
}

.page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.page-head h1 {
  margin: 0;
  font-size: 19px;
  font-weight: 600;
}

.page-head .muted {
  max-width: 70ch;
}

h2 {
  margin: 0 0 12px;
  font-size: 15px;
}

h3 {
  margin: 18px 0 8px;
  font-size: 13px;
  color: var(--muted);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* ---------- gate ---------- */

.gate {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  padding: 24px;
}

.gate[hidden] {
  display: none;
}

.gate-card {
  width: min(420px, 100%);
  margin: 0;
}

.gate-card h1 {
  margin: 0 0 8px;
  font-size: 17px;
}

.gate-card label {
  display: block;
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--muted);
}

.gate-card input {
  width: 100%;
}

.gate-card .row {
  margin-top: 16px;
}

/* ---------- controls ---------- */

input,
select,
textarea {
  font: inherit;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 5px 8px;
  margin-top: 4px;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

textarea {
  width: 100%;
  resize: vertical;
}

input[type="checkbox"] {
  margin: 0;
}

/*
 * A file-picker field is the same kind of field as the rest: full block
 * width and on its own line under the label. Sized to its own content it
 * came out shorter than the neighbouring field and on the same line as
 * its label, and a form made of fields of differing lengths reads as
 * assembled by accident.
 */
input[type="file"] {
  width: 100%;
}

/*
 * The native button inside the field can only be styled this way and
 * otherwise stays a system box in the middle of a form. A soft fill with
 * no border: the field itself is already outlined, and a second outline
 * inside it would give a button within a button. The right margin keeps
 * the file name from sticking to it.
 */
input[type="file"]::file-selector-button {
  font: inherit;
  margin: 0 12px 0 0;
  padding: 4px 12px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--fast) var(--ease);
}

input[type="file"]::file-selector-button:hover {
  background: var(--chosen);
}

label.inline {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

label.inline input,
label.inline select {
  margin-top: 0;
}

button {
  font: inherit;
  padding: 6px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--accent);
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}

/*
 * A game card is a button for the keyboard's sake rather than for looks:
 * the list is walked with Tab and opened with Enter. All of its styling
 * is its own, below.
 */
.game-card {
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--text);
  padding: 21px;
}

/*
 * The darker accent belongs to the filled button, and the filled buttons
 * are named here one by one. Naming them is the whole point: this used
 * to be the list the other way round — every `button` got the accent
 * fill unless it had opted out — and a `button` that does not look like
 * a button had to remember to opt out or be filled with dark blue while
 * keeping the dark text it was drawn with. It does not lose the text
 * gradually: the accent fill is more specific than the component's own
 * `:hover` (one pseudo-class more), so it wins outright, and the caption
 * under the cursor goes from readable to invisible.
 *
 * Forgetting the list is what kept happening, so the list is arranged so
 * that forgetting it costs nothing: a new button left out of it simply
 * keeps whatever hover its own rules give it. A button with no class of
 * its own is filled — that is what `button` alone draws — and the three
 * that carry a class and are still filled say so here.
 */
button:not([class]):hover:not(:disabled),
.new-chat:hover:not(:disabled),
.add-game:hover:not(:disabled),
.game-pin-add:hover:not(:disabled) {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

button:disabled {
  opacity: 0.5;
  cursor: default;
}

button.ghost {
  background: none;
  color: var(--text);
  border-color: var(--line-strong);
}

button.ghost:hover:not(:disabled) {
  background: var(--hover);
  border-color: var(--accent);
  color: var(--text);
}

/*
 * A ghost button that deletes — the two on an opened session, standing
 * in a row with the three that only produce a file.
 *
 * Red at rest and not on hover, by the same rule as the trash beside a
 * conversation: red here is a state of the action ("there will be no
 * undo"), and a colour that arrives with the pointer announces it after
 * the aim has already been taken. The border reddens with it, because a
 * red word inside a grey outline in a row of grey outlines reads as a
 * button that failed rather than one that destroys.
 */
button.ghost.danger {
  color: var(--error);
  border-color: var(--error-line);
}

button.ghost.danger:hover:not(:disabled) {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--error);
}

button.link {
  background: none;
  border-color: transparent;
  color: var(--accent);
  padding: 4px 6px;
}

button.link:hover:not(:disabled) {
  background: var(--hover);
  border-color: transparent;
  color: var(--accent);
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.row.wrap {
  flex-wrap: wrap;
}

.muted {
  color: var(--muted);
  font-size: 13px;
}

.faint {
  color: var(--faint);
  font-size: 12px;
}

.error {
  color: var(--error);
  font-size: 13px;
}

/*
 * A block's inner padding is one and a half times what the grid step
 * dictates. One rule for every bordered block (card, floating layer,
 * answer section, chat bubble, `pre`): the content stands noticeably
 * further from the border than the lines stand from each other, and the
 * border stops reading as part of the text. These block paddings must
 * not drift apart — nested blocks would then step outwards.
 */
.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px;
}

/* ---------- a row of filters ---------- */

/*
 * A form of filters submitted as a whole. Every control in it is a box
 * of the same height and the same border — the range, the text fields,
 * the dimension filter and the flags. Assembled from controls of
 * differing heights the row reads as several forms that happen to stand
 * side by side, and the one control that looks unlike the rest reads as
 * belonging to its neighbour rather than as a filter of its own.
 *
 * Aligned to the top rather than to the centre: the dimension filter
 * carries a line of hints under its field, and centring would lift its
 * box above everything else by half that line — which is exactly how it
 * came out before this rule existed.
 */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 12px;
}

.filter-bar > * {
  min-height: var(--control);
}

/*
 * The submit button and the status line are sized by their text, and
 * left alone they stand a few pixels taller and higher than the boxes
 * beside them. Both are given the row's own height instead, and their
 * text is centred in it.
 */
.filter-bar > button,
.filter-bar > .muted {
  display: inline-flex;
  align-items: center;
  padding-top: 0;
  padding-bottom: 0;
}

/*
 * A caveat about what the row above selects takes the whole width and
 * stands under it, rather than squeezing in between two fields as a
 * very tall one. It is not a control, so the row's minimum height is
 * not its business either.
 */
.filter-bar > .note {
  flex-basis: 100%;
  min-height: 0;
  margin: 0;
}

/*
 * A text field of the bar. The placeholder carries its name, the way
 * the dimension filters do: a label beside the box would make this one
 * control wider than the rest for no reason anybody reads.
 */
.filter-input {
  width: 200px;
  margin: 0;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--panel);
}

.filter-input.wide {
  width: 340px;
}

.filter-input:hover {
  border-color: var(--accent);
}

/*
 * A flag is a box, not a word: it stands in a row of boxed fields, and
 * a bare label there reads as a caption to the field on its left. The
 * switch sits at the left edge of its own box, so the eye finds every
 * flag of the row in one column rather than at the end of a phrase.
 *
 * The label is set in the row's own size and colour — the same as the
 * text typed into the fields beside it. Set smaller or greyer it read
 * as a caption about a filter rather than as the filter itself.
 */
/*
 * The switch sits the same distance from the left edge as from the top
 * and the bottom — the box's height is fixed, so that distance is
 * arrived at rather than declared: border, padding and the half of the
 * free height the centring leaves. A wider left inset made the switch
 * look pushed into the middle of its own box.
 */
.toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 3px 12px 3px 5px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
}

.toggle:hover {
  border-color: var(--accent);
}

/*
 * A switch rather than a checkbox: a filter's flag is a state that
 * stays on — "only the open ones" — and a switch says on/off where a
 * tick says "chosen from a list". The state lives in the switch alone;
 * filling the box around it as well would say the same thing twice.
 *
 * The native input is the track itself rather than a hidden control
 * with a drawn stand-in beside it: the keyboard, the label click and
 * the checked state then work because the input is real, not because
 * something was wired up to imitate them.
 */
.toggle input[type="checkbox"] {
  appearance: none;
  position: relative;
  flex: none;
  width: 40px;
  height: 22px;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 100px;
  background: var(--line-strong);
  cursor: pointer;
  transition: background-color var(--fast) var(--ease);
}

/*
 * The knob travels; nothing changes size and nothing outside the track
 * moves. That is the whole reason a switch can animate on a page where
 * sizes do not: the movement is the answer to the click, and it happens
 * inside a control of fixed size.
 */
.toggle input[type="checkbox"]::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  transition: left var(--fast) var(--ease);
}

.toggle input[type="checkbox"]:checked {
  background: var(--accent);
}

.toggle input[type="checkbox"]:checked::before {
  left: 20px;
}

/*
 * Focus is outlined on the box rather than on the track: the row is
 * made of boxes, and a ring around a 40 px pill inside one of them
 * reads as a smaller control having been reached, not this filter.
 */
.toggle:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.toggle input[type="checkbox"]:focus-visible {
  outline: none;
}

/*
 * A flag that stopped applying goes grey rather than disappearing: a
 * control that vanishes on a neighbour's click reads as a page that
 * broke, and its place in the row is the answer to "where did the
 * filter go".
 */
.toggle:has(input:disabled) {
  color: var(--faint);
  border-color: var(--line);
  cursor: not-allowed;
}

.toggle input[type="checkbox"]:disabled {
  background: var(--line);
  cursor: not-allowed;
}

.toggle input[type="checkbox"]:disabled::before {
  background: var(--panel);
}

/*
 * Inside the bar the dimension filter is one fixed column, not a
 * stretching one: it shares the row with fields that do not stretch,
 * and a filter that grows into the free space would leave the others
 * looking shrunk.
 */
.filter-bar .filters {
  flex: 0 0 auto;
  gap: 12px;
}

.filter-bar .filter {
  flex: 0 0 220px;
  min-width: 220px;
}

/* ---------- floating layer ---------- */

/*
 * Shadows belong only to layers over the page — the popover and the
 * notification: they have to detach from it, otherwise they read as part
 * of it. There are none anywhere else.
 */
.popover {
  position: absolute;

  /*
   * Above a card expanded to full screen: the layer is opened from that
   * card's own header, and were it to end up beneath it, it would not
   * open at all.
   */
  z-index: 30;
  min-width: 240px;
  padding: 18px;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  animation: appear var(--mid) var(--ease);
}

.popover .pairs {
  margin-bottom: 12px;
}

/*
 * A layer holding a list rather than text: here the block's inner
 * padding is given over to the rows. The hover fill stretches the full
 * width of the layer, and a dozen and a half pixels of margin around it
 * would turn the list into a set of separate plates.
 */
.popover.menu {
  padding: 8px;
}

/*
 * The width is capped, and so is the height: the game list grows, and a
 * long name would stretch the layer across half the screen. What is
 * ellipsised is readable in full from the hover tooltip.
 */
.game-menu {
  display: grid;
  gap: 2px;
  max-width: 360px;
  max-height: min(60vh, 480px);
  overflow-y: auto;
}

.game-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.game-menu-item:hover {
  background: var(--hover);
  border-color: transparent;
}

/* the open game is marked in the same place as the open section */
.game-menu-item.active {
  background: var(--chosen);
}

.game-menu-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- range picker ---------- */

/*
 * The range and the series step stand flush and share a border: this is
 * one answer to "what is shown" — from which date to which, and what one
 * column stands for. Separated by a gap they would read as two filters,
 * the second of which is easy to miss.
 */
.period {
  display: inline-flex;
  align-items: stretch;
}

.period:has(.period-step-size) .period-field {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.period-step-size {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 0 -1px;
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  background: var(--panel);
  color: var(--text);
  font: inherit;
}

.period-step-size:hover {
  border-color: var(--accent);
}

.period-caret {
  display: flex;
  color: var(--muted);
}

.period-caret svg {
  width: 15px;
  height: 15px;
}

/*
 * The step list is the same row-based layer as the game picker: a
 * full-width row, and the selected one marked with the same fill as an
 * open section. A second kind of dropdown on one page would read as a
 * different mechanism.
 */
.period-menu {
  display: grid;
  gap: 2px;
  min-width: 140px;
}

.period-menu-item {
  width: 100%;
  padding: 6px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
}

.period-menu-item:hover {
  background: var(--hover);
}

.period-menu-item.active {
  background: var(--chosen);
}

/*
 * Both ends of the range are one field with one border: this is a single
 * quantity, and two separate "From" and "To" boxes would read as two
 * independent filters with nothing tying them together.
 */
.period-field {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  cursor: pointer;
}

.period-field:hover {
  border-color: var(--accent);
}

.period-date {
  width: 10ch;
  margin: 0;
  padding: 1px 2px;
  border: none;
  background: none;
  text-align: center;
  font-variant-numeric: tabular-nums;
  cursor: text;
}

.period-arrow {
  color: var(--faint);
}

.period-icon {
  display: flex;
  margin-left: 2px;
  color: var(--muted);
}

.period-icon svg {
  width: 17px;
  height: 17px;
}

/*
 * The layer's width is set by the months rather than by the buttons
 * below them: a `min-content` column with zero width on the presets row
 * stops it from stretching the layer into a half-screen ribbon — instead
 * it wraps below the calendar.
 */
.period-pop {
  position: fixed;
  display: grid;
  grid-template-columns: min-content;
  min-width: 0;
}

.period-months {
  display: flex;
  gap: 24px;
}

.period-month {
  display: grid;
  gap: 8px;
}

.period-head {
  display: flex;
  align-items: center;
  gap: 2px;
}

.period-title {
  flex: 1;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
}

.period-step {
  display: flex;
  padding: 2px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
}

.period-step svg {
  width: 16px;
  height: 16px;
}

.period-step:hover {
  background: var(--hover);
  color: var(--text);
}

/*
 * The eighth column is the week number. It is not here for looks: weekly
 * cohorts and "week over week" are computed by ISO number, and without
 * it the week boundary has to be counted out with a finger across the
 * calendar.
 */
.period-grid {
  display: grid;
  grid-template-columns: 26px repeat(7, 30px);
  gap: 2px;
  align-items: center;
  justify-items: center;
}

.period-wd {
  font-size: 12px;
  color: var(--muted);
}

.period-week {
  font-size: 12px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}

.period-day {
  width: 30px;
  padding: 4px 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

/* days of the adjacent month are visible but do not compete with the current one */
.period-day.out {
  color: var(--faint);
}

.period-day:disabled {
  color: var(--missing);
}

/*
 * The range's endpoints are excluded from the general hover: it has one
 * pseudo-class more, and the light hover fill overrode their accent one,
 * leaving white text on light — the selected day vanished exactly under
 * the cursor. Their own hover is below, in the same colour as a filled
 * button's.
 */
.period-day:hover:not(:disabled, .start, .end) {
  background: var(--hover);
}

/*
 * An accent fill means "click here" — here it is on the range's
 * endpoints, because those are what get moved. Everything between them
 * is filled softly: that is what has already been selected rather than a
 * place to click.
 */
.period-day.inside {
  background: var(--accent-soft);
}

.period-day.start,
.period-day.end {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.period-day.start:hover,
.period-day.end:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

/* today is marked with a border rather than a colour: it is not a selection but a reference point */
.period-day.today:not(.start, .end) {
  border-color: var(--line-strong);
}

/*
 * The presets. They are what almost every choice is made with, so they
 * do not hide in a list but lie in a row under the calendar — one click
 * instead of two.
 */
.period-quick {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  width: 0;
  min-width: 100%;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.period-preset {
  padding: 3px 10px;
  font-size: 13px;
  border-color: var(--line);
}

/* the selected preset is marked in the same place as the open game */
.period-preset.active {
  background: var(--chosen);
  border-color: var(--accent);
}

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

/*
 * Three fields in a row under the range: "over which period" and "for
 * whom" are one question, and placed in different parts of the screen
 * they would read as a page setting and a filter, the second of which is
 * easy to miss.
 */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
}

.filter {
  display: grid;
  gap: 6px;
  flex: 1 1 240px;
  min-width: 220px;
  max-width: 420px;
}

/*
 * The field is the same box as the range picker's: one border, the same
 * radius, the same response to the cursor. What is selected lies inside
 * it as chips rather than as a "3 selected" line — three countries you
 * cannot see are no better than a filter you forgot about.
 */
.filter-field {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: var(--control);
  padding: 3px 8px;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  cursor: pointer;
}

.filter-field:hover {
  border-color: var(--accent);
}

/*
 * Two different messages, and therefore two different strengths of
 * response.
 *
 * The caret is in the input — only the border reacts. Not
 * `:focus-within`: the chips inside are buttons too and also take focus
 * on a click, and an outlined box would answer "you clicked the filter"
 * where somebody clicked one value inside it. For the same reason, not
 * an outline: focus lands in the input on its own — a removed chip takes
 * it along — and outlining the whole filter for that means showing a
 * click that never happened.
 *
 * The list is open — an outline on the whole box. That one really is
 * about the filter as a whole: there is one open list per page, and the
 * box it grows from has to stay visible even if the list slid under the
 * cursor.
 */
.filter-field:has(.filter-search:focus) {
  border-color: var(--accent);
}

.filter-field.open {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.filter-search:focus-visible {
  outline: none;
}

/*
 * The chips scroll rather than growing the field without end: "+ All" on
 * countries is two hundred chips, and a half-screen-tall field would
 * push down the very page the filter is being touched for. Nothing is
 * hidden in the process: everything selected is there, and it can be
 * scrolled to.
 */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-height: 104px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

/*
 * The input shrinks to the last: the chips are what matters in it, and
 * the search string is a way of getting to them. Its own width is needed
 * only so there is somewhere to place the caret.
 */
.filter-search {
  flex: 1 1 60px;
  width: 60px;
  min-width: 60px;
  margin: 0;
  padding: 2px;
  border: none;
  background: none;
  cursor: text;
}

/*
 * "One more filter", beside the button that computes. Ghost rather than
 * filled: the accent means "this is what you press", and the thing you
 * press on this row is Show.
 */
.filter-more {
  display: flex;
  align-items: center;
  gap: 6px;
}

.filter-more svg {
  width: 15px;
  height: 15px;
}

.filter-more-menu {
  display: grid;
  gap: 2px;
  min-width: 300px;
  max-width: 420px;
  max-height: min(60vh, 480px);
  overflow-y: auto;
}

.filter-find-row {
  padding: 0 4px 4px;
}

.filter-find {
  width: 100%;
  margin: 0;
}

.filter-options {
  display: grid;
  gap: 2px;
}

.filter-group {
  margin-top: 8px;
}

.filter-group h3 {
  margin: 0 0 2px;
  padding: 0 8px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.filter-option {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.filter-option:hover,
.filter-option:focus-visible {
  color: var(--text);
  background: var(--hover);
}

.filter-caret {
  display: flex;
  margin-left: auto;
  color: var(--muted);
}

.filter-caret svg {
  width: 15px;
  height: 15px;
}

/*
 * A chip is filled with colour and is clickable in full — by the rule
 * that anything filled with the accent has to be clickable. Blue means
 * "count only by it", red means "all but it"; the sign next to the name
 * repeats that in a symbol for the case where the colours are not told
 * apart.
 */
.filter-chip {
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  border-radius: var(--radius);
  font-size: 13px;
}

.filter-chip.in {
  background: var(--include);
}

.filter-chip.out {
  background: var(--exclude);
}

/*
 * The response to the cursor belongs to the whole chip, even though only
 * its text is hovered. Inverting is an action on the chip as a whole,
 * and a highlighted rectangle under the text alone would read as a
 * second button cut out of it that is not there.
 *
 * A darkening rather than a second colour: there are two of them, blue
 * and red, and picking a hover for each would mean four colours where
 * one transparent overlay suffices. It also stays correct by itself on
 * the dark theme, where the fill is lighter.
 */
.filter-chip:has(.filter-chip-body:hover) {
  box-shadow: inset 0 0 0 999px rgb(0 0 0 / 16%);
}

.filter-chip:has(.filter-chip-body:active) {
  box-shadow: inset 0 0 0 999px rgb(0 0 0 / 28%);
}

.filter-chip-body,
.filter-chip-drop {
  padding: 2px 4px;
  border: none;
  background: none;
  color: #fff;
  font: inherit;
}

.filter-chip-body {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding-left: 8px;
}

.filter-chip-drop {
  display: flex;
  padding-right: 7px;
}

.filter-chip-drop svg {
  width: 13px;
  height: 13px;

  /*
   * The only size on the page allowed to change under the cursor. The
   * ban on animating sizes is about content not travelling under the
   * cursor; here nothing travels: the cross grows from its own centre
   * inside a button of constant size, and the neighbouring chip does not
   * shift by a pixel.
   */
  transition: transform var(--fast) var(--ease);
}

/*
 * The cross responds with size rather than with a fill. A fill under it
 * would read as a third chip state that does not exist: removing is not
 * a selection but the cancelling of one.
 */
.filter-chip-drop:hover svg {
  transform: scale(1.35);
}

.filter-chip-drop:active svg {
  transform: scale(1.15);
}

.filter-sign {
  font-weight: 600;
}

/*
 * The hints are the game's most frequent values. The row holds its
 * height even when empty: without that the fields would jump up and down
 * at the moment the values arrive from the server.
 */
.filter-hints {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  min-height: 18px;
  padding-left: 2px;
}

/*
 * A hint's colour is the next click: blue will add, red will invert,
 * struck through will remove. That is why it is set in the colour rather
 * than filled with it: a fill means "click here, and this is selected",
 * and what is selected is visible as a chip above.
 */
.filter-hint {
  padding: 0;
  border: none;
  background: none;
  color: var(--include);
  font: inherit;
  font-size: 13px;
}

.filter-hint.invert {
  color: var(--exclude);
}

.filter-hint.drop {
  color: var(--faint);
  text-decoration: line-through;
}

.filter-hint:hover {
  text-decoration: underline;
}

.filter-hint.drop:hover {
  text-decoration: line-through underline;
}

/*
 * The value list is the same row-based layer as the game and step
 * pickers. Its height is capped and the rest scrolls: there are two and
 * a half hundred countries, and a full-screen layer would cover the very
 * page the filter is being opened for.
 */
.filter-menu {
  min-width: 260px;
  max-width: 360px;
}

.filter-list {
  display: grid;
  gap: 2px;
  max-height: min(50vh, 360px);
  overflow-y: auto;

  /*
   * The thumb is always visible rather than only while scrolling: on its
   * own a list of ten rows looks like a list of ten rows, and the fact
   * that two hundred more follow is visible only from it. The thumb's
   * length is what says how many more there are.
   */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

/*
 * The standard properties above disable `::-webkit-scrollbar` entirely,
 * and on macOS they leave the thumb overlaid on their own — that is,
 * invisible until you scroll, exactly where it is needed. So where the
 * webkit pseudo-elements exist the thumb is drawn by them and takes up
 * space in the layout; Firefox stays on the standard properties, where
 * they work as intended.
 */
@supports selector(::-webkit-scrollbar) {
  .filter-list,
  .filter-chips {
    scrollbar-width: auto;
    scrollbar-color: auto;
  }

  .filter-list::-webkit-scrollbar,
  .filter-chips::-webkit-scrollbar {
    width: 8px;
  }

  .filter-list::-webkit-scrollbar-track,
  .filter-chips::-webkit-scrollbar-track {
    background: transparent;
  }

  .filter-list::-webkit-scrollbar-thumb,
  .filter-chips::-webkit-scrollbar-thumb {
    background: var(--line-strong);
    border-radius: var(--radius);
  }
}

/*
 * A value that is not one of the dimension's own — "No country" — stands
 * above the list behind a rule, the mirror of the actions below it. The
 * rule is the whole point: level with the rows it would read as the
 * first country in the list, and it is not a country at all.
 *
 * Outside the scrolling list, so it neither slides away under two
 * hundred rows nor moves when the search narrows them. Hidden when
 * empty, which is every field but this one.
 */
.filter-lead {
  display: grid;
  gap: 2px;
  margin-bottom: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}

.filter-lead:empty {
  display: none;
}

.filter-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
}

.filter-item:hover {
  background: var(--hover);
}

.filter-item.in {
  color: var(--include);
}

.filter-item.out {
  color: var(--exclude);
  text-decoration: line-through;
}

/* the sign holds its column even when empty: without it the rows slide under one another */
.filter-item .filter-sign {
  width: 9px;
}

.filter-item-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.filter-empty {
  padding: 6px 10px;
}

/*
 * Actions over the list as a whole go in a row below it and behind a
 * rule: they apply to the list rather than to yet another of its values,
 * and standing level with the rows they would read as four more values.
 *
 * The same two colours as the chips, and by the same rule: blue
 * includes, red removes. They are set in the colour rather than filled
 * with it — a fill would mean "selected", and an action cannot be
 * selected, it can only be performed.
 */
.filter-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 8px;
  padding: 8px 10px 2px;
  border-top: 1px solid var(--line);
}

.filter-bulk {
  padding: 0;
  border: none;
  background: none;
  color: var(--include);
  font: inherit;
  font-size: 13px;
}

.filter-bulk.remove {
  color: var(--exclude);
}

.filter-bulk:hover:not(:disabled) {
  text-decoration: underline;
}

/*
 * Nothing to invert or nothing to remove — the button is disabled rather
 * than hidden: a row that changes its contents makes you find your place
 * in it afresh on every click.
 */
.filter-bulk:disabled {
  color: var(--missing);
}

/* ---------- notifications ---------- */

/*
 * The corner below the top bar rather than over it: the bar is a place
 * and a notification is an event, and by covering it the notification
 * would eat the buttons for all the seconds it hangs there. A column
 * running top to bottom, new ones at the bottom.
 *
 * The area itself catches no clicks: it occupies the whole corner, and
 * without that an invisible column would intercept clicks on the page
 * between messages.
 */
.toasts {
  position: fixed;
  top: calc(var(--top-height) + 12px);
  right: 16px;
  z-index: 20;
  display: grid;
  justify-items: end;
  gap: 8px;
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  align-items: center;
  gap: 10px;
  width: max-content;
  max-width: min(380px, calc(100vw - 32px));
  padding: 10px 10px 10px 14px;
  border: 1px solid;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  animation: toast-in var(--mid) var(--ease);
}

/*
 * It slides in from the right and leaves the same way: a notification
 * comes from beyond the edge of the screen, and leaving in the same
 * direction reads as "it was dismissed" rather than "it blinked and
 * vanished".
 */
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateX(24px);
  }
}

@keyframes toast-out {
  to {
    opacity: 0;
    transform: translateX(24px);
  }
}

.toast.leaving {
  animation: toast-out var(--mid) var(--ease) forwards;
}

.toast.ok {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok);
}

.toast.warn {
  background: var(--warn-bg);
  border-color: var(--warn-line);
  color: var(--warn-text);
}

.toast.error {
  background: var(--error-bg);
  border-color: var(--error-line);
  color: var(--error);
}

.toast-text {
  font-size: 13px;
}

/*
 * The button fades in on hover, but the space for it is always
 * occupied: were it to appear by pushing things apart, the text would
 * slide out from under the eye exactly as the mouse travels to the
 * notification. From the keyboard it is revealed by focus — otherwise it
 * could not be reached with Tab.
 */
.toast-close {
  width: 22px;
  height: 22px;
  color: inherit;
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
}

.toast:hover .toast-close,
.toast-close:focus-visible {
  opacity: 1;
}

/*
 * The highlight is mixed from the notification's own colour: the
 * ready-made `--hover` is blue here, and on a green backdrop it reads as
 * an alien blotch.
 */
.toast-close:hover {
  color: inherit;
  background: color-mix(in srgb, currentColor 14%, transparent);
}

.toast-close svg {
  width: 16px;
  height: 16px;
}

/* ---------- conversation ---------- */

.chat {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/*
 * The bottom padding is the height of the gradient the composer hangs
 * above itself: without it, the last line of a conversation scrolled all
 * the way down would sit under the fade — washed out exactly where
 * somebody is reading the conclusion.
 */
.chat-log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 24px 32px 48px;
  display: flex;
  flex-direction: column;
}

/*
 * The feed runs top to bottom, like a conversation, rather than with new
 * answers on top: the next question almost always refines the previous
 * answer, and reading them in reverse order does not work.
 *
 * It accumulates from the bottom edge all the same: the first question
 * stands by the input where it was typed, not three screens above it. A
 * top margin on the first message rather than `justify-content` on the
 * feed: with that, a long conversation is not clipped at the top when
 * scrolled.
 */
.chat-log > :first-child {
  margin-top: auto;
}

/*
 * A message does not shrink to fit the feed: in a flex column the
 * children shrink by default, and a block-tall chart would turn into a
 * strip the moment the conversation stopped fitting on screen.
 */
.chat-log > * {
  flex: none;
}

.chat-log > * + * {
  margin-top: 20px;
}

/*
 * A block of an answer appears rather than being switched on. The fade
 * is short and the offset is a few pixels: this says "this is new", and
 * anything longer would be a page that takes its time.
 *
 * The delay is set per block when it is inserted (`reveal` in chat.js)
 * and staggers blocks that arrive together — a stored conversation
 * renders everything it has in one breath, and a dozen blocks landing at
 * once is the wall of content the feed exists not to be. Live, the
 * pieces are spaced by the loop itself and the delay is nearly always
 * zero.
 */
.reveal {
  animation: reveal-in 0.32s var(--ease) both;
  animation-delay: var(--reveal-delay, 0s);
}

@keyframes reveal-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/*
 * An empty conversation stands in the middle of the screen, input and
 * all: the feed accumulates from the bottom edge because messages push
 * it there, and with no messages at all the bottom edge is an input
 * field alone under three screens of nothing.
 */
.chat.blank {
  justify-content: center;
}

/*
 * The greeting is content-sized here rather than the growing part of the
 * column: as a stretching block it would push the field back down to the
 * bottom and the centring would come to nothing.
 */
.chat.blank .chat-log {
  flex: 0 1 auto;
  overflow: visible;
  padding-bottom: 12px;
}

/* nothing has been said yet, so there is no feed to fade out */
.chat.blank .composer::before {
  display: none;
}

.chat-empty {
  max-width: 60ch;
  margin: 0 auto;
  text-align: center;
  color: var(--muted);
}

.chat-empty h1 {
  font-size: 24px;
  color: var(--text);
  margin: 0 0 8px;
}

/*
 * The examples stand under the field rather than over it: they are ways
 * to fill it in, and the eye is already on the field by then. Centred on
 * it and not on the page — they belong to the input, not to the screen.
 */
.chat-hints {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 32px 24px;
}

.chat-hint {
  max-width: 100%;
  padding: 7px 14px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.chat-hint:hover {
  background: var(--hover);
  border-color: transparent;
  color: var(--text);
}

/* ---------- the line under an answer ---------- */

/*
 * What the model is doing now, and afterwards how long it took, when it
 * was asked and what it cost. One line for both, because it is one
 * thing: a waiting bar that vanished with the answer left a page of
 * charts and no way back to how they were arrived at.
 *
 * No border and no fill: it is not a message but the footer of one, and
 * a bordered block here would read as one more block of the answer.
 */
.status {
  display: grid;
  gap: 6px;
  padding: 0 2px;
  font-size: 13px;
}

.status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/*
 * The whole line is the button, not a caret at the end of it: what it
 * opens is the record of the line itself, and a target the size of an
 * icon would be a second, smaller control saying the same thing.
 */
.status-head {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 4px 8px;
  margin-left: -8px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}

.status-head:hover {
  background: var(--hover);
  border-color: transparent;
  color: var(--text);
}

/*
 * A line with nothing to open is not a button that does nothing: an
 * answer stored before there was a record says so, and stops responding
 * to the cursor.
 */
.status-head:disabled {
  background: none;
  color: var(--faint);
  cursor: default;
}

.status-icon {
  display: flex;
  flex: none;
}

.status-icon svg {
  width: 16px;
  height: 16px;
}

.status-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The stage is swapped rather than rewritten: the change is the one
 * thing this line has to say, and text that changes without moving is
 * missed by somebody looking at the charts.
 */
.status-word {
  display: inline-block;
  animation: status-say var(--mid) var(--ease) both;
}

@keyframes status-say {
  from {
    opacity: 0;
    transform: translateY(3px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/*
 * While it runs, the words shimmer. This is the one place on the page
 * where motion is decoration and it earns its keep: the loop stands
 * still for tens of seconds at a time, and a line that does not move at
 * all is indistinguishable from one that has stopped.
 */
.status.live .status-word {
  background: linear-gradient(
    90deg,
    var(--muted) 35%,
    var(--text) 50%,
    var(--muted) 65%
  );

  background-size: 250% 100%;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation:
    status-say var(--mid) var(--ease) both,
    status-shimmer 2.2s linear infinite;
}

/*
 * The sweep runs between the two positions at which the gradient still
 * covers the text completely. Past them the background leaves the box
 * altogether — and with the letters painted by the background, a line
 * that has run out of gradient is a line that is not there at all.
 */
@keyframes status-shimmer {
  from {
    background-position: 100% 0;
  }

  to {
    background-position: 0 0;
  }
}

.status.live .status-icon {
  animation: status-pulse 2.2s ease-in-out infinite;
}

@keyframes status-pulse {
  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.45;
  }
}

.status.broken .status-icon,
.status.broken .status-label {
  color: var(--error);
}

/* how long it has been going: the same step of text as the date opposite */
.status-clock {
  flex: none;
  font-size: 12px;
  color: var(--faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/*
 * The caret appears under the cursor rather than standing there: the
 * line is read far more often than it is opened, and an arrow on every
 * answer of a long conversation is a column of arrows.
 */
.status-more {
  display: flex;
  flex: none;
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
}

.status-head:hover .status-more,
.status-head:focus-visible .status-more,
.status.open .status-more {
  opacity: 1;
}

.status-more svg {
  width: 14px;
  height: 14px;
  transition: transform var(--mid) var(--ease);
}

.status.open .status-more svg {
  transform: rotate(180deg);
}

.status-head:disabled .status-more {
  display: none;
}

/* when it was asked and what it cost: read rarely, and never first */
.status-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex: none;
  font-size: 12px;
  color: var(--faint);
}

.status-meta > * + *::before {
  content: "·";
  margin-right: 8px;
}

/*
 * The record: every step of the loop down a rail, in the order it
 * happened. A list rather than a table — the lines are of wildly
 * different lengths, and half of them carry a block of text underneath.
 */
.status-record {
  display: grid;
  gap: 14px;
  margin: 4px 0 2px 10px;
  padding: 4px 0 4px 18px;
  border-left: 1px solid var(--line);
}

.status-record[hidden] {
  display: none;
}

.status-step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px;
  align-items: start;
}

/*
 * The mark straddles the rail: the rail is the order of events, and a
 * mark beside it would read as a bullet in a list rather than as a point
 * on a line.
 */
.status-step-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-left: -29px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--bg);
  color: var(--muted);
}

.status-step-mark svg {
  width: 13px;
  height: 13px;
}

.status-step.failed .status-step-mark {
  border-color: var(--error-line);
  color: var(--error);
}

.status-step-body {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.status-step-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.status-step-title {
  font-size: 13px;
  color: var(--text);
}

.status-step.failed .status-step-title {
  color: var(--error);
}

.status-step-time {
  flex: none;
  font-size: 12px;
  color: var(--faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/*
 * What was asked for and what came back, verbatim. Scrolls inside its
 * own box rather than growing: a tool's answer to the model runs to
 * dozens of lines, and the record is read for its order first.
 *
 * The colours are the other way round from a code block inside a card
 * (`pre.json`): that one is the page's colour inside a panel, and this
 * one is a panel on the page — the record has no card around it, and a
 * quote in the same colour as what it stands on is not a quote.
 */
.status-step-detail,
.status-step-thought pre {
  margin: 2px 0 0;
  padding: 10px 12px;
  max-height: 180px;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.status-step-thought pre {
  max-height: 320px;
}

.status-step-thought summary {
  font-size: 12px;
  color: var(--muted);
  cursor: pointer;
}

.status-step-usage {
  font-size: 11px;
  color: var(--faint);
}

/*
 * A person's question: a chat bubble, not a card heading. It is rounded
 * like the field it was typed into and carries no border — one is
 * what the other became, and a bubble outlined like the answer blocks
 * below it would read as one more of them.
 */
.chat-question {
  max-width: 70ch;
  margin-left: auto;
  padding: 12px 21px;
  background: var(--accent-soft);
  border-radius: var(--composer-radius);
  white-space: pre-wrap;
}

/*
 * The feed does not end at a line, it fades out. A ruled edge with a
 * filled strip under it made the composer a second panel stuck to the
 * bottom of the page — a block of solid colour holding one field. What
 * is actually true is that the conversation runs out of room: the last
 * rows dissolve into the page's own colour under a gradient, and the
 * field sits on nothing.
 *
 * The gradient hangs above the composer rather than inside it, so it
 * follows the field as it grows with the question — a fixed strip would
 * drift off the edge on the first three-line question.
 */
.composer {
  flex: none;
  position: relative;
  background: var(--bg);
  padding: 12px 32px 16px;
}

.composer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 48px;
  background: linear-gradient(to top, var(--bg), transparent);
  pointer-events: none;
}

/*
 * The field and the controls under it are one box. Everything inside it
 * is about the question being typed — which model gets it, whether the
 * answer comes back with words, and the arrow that sends it — and a
 * shared border says so without a caption; standing outside, the same
 * controls would read as page controls that happen to sit near a field.
 *
 * This is the one place on the page with a rounding of its own
 * (DESIGN.md). Not hierarchy: the composer is a shape rather than a
 * block — the single thing on the screen that is typed into, and it is
 * told apart from the bordered blocks above it by its outline before a
 * single word is read.
 */
/*
 * The box's own padding is the same on every side, and the field takes
 * the same inner padding as the controls under it: the typed question
 * and the words below it then start on one vertical. The controls keep
 * their own, roomier padding rather than being tightened to the field —
 * the hover fill needs to stand clear of the text it belongs to, and it
 * is the field that gets moved to meet the row.
 */
.composer-box {
  max-width: 900px;
  margin: 0 auto;
  padding: 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--composer-radius);
  background: var(--panel);
}

.composer-box:focus-within {
  border-color: var(--accent);
}

/*
 * Reached by keyboard, the ring goes round the box rather than round the
 * field: the field has no border of its own here, and a ring inside the
 * box would outline something the eye does not see as a control.
 */
.composer-box:has(textarea:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/*
 * The field carries no border of its own: the box around it is the
 * field. Growing with the question is done in chat.js — the height is
 * assigned from the content, and a `resize` handle would be a second
 * answer to the same question.
 */
.composer textarea {
  margin: 0;
  padding: 4px 10px;
  min-height: 0;
  max-height: 260px;
  border: none;
  border-radius: 0;
  background: none;
  resize: none;
}

.composer textarea:focus {
  outline: none;
  border: none;
  box-shadow: none;
}

.composer-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}

/*
 * The sending arrow is pushed to the far end of the row, away from the
 * settings on the left: they change what the next question will be, and
 * it sends it — the only one of them that cannot be undone by clicking
 * again.
 */
.composer .send {
  margin-left: auto;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
}

.composer .send:hover {
  background: var(--accent-hover);
  color: #fff;
  border-color: transparent;
}

/*
 * The button dims rather than disappears while the answer is being
 * computed: the row would otherwise lose its right-hand end for the
 * dozen seconds a person spends looking at the waiting bar.
 */
.composer .send:disabled {
  background: var(--line-strong);
  color: var(--panel);
  cursor: default;
}

/*
 * Which model answers. The same button-with-a-layer as the series step
 * and the game picker — a native `select` would be the only list on the
 * page drawn by the operating system.
 */
.model-pick {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.model-pick:hover {
  background: var(--hover);
  border-color: transparent;
  color: var(--text);
}

.model-pick[hidden] {
  display: none;
}

.model-caret {
  display: flex;
}

.model-caret svg {
  width: 14px;
  height: 14px;
}

/*
 * A row here is two lines and sometimes three: what the model is for,
 * and the name it carries at the provider. The name is the answer to
 * "which model answered this" and nothing more — it is set in the
 * quietest step of the three, because nobody chooses by it.
 */
.model-menu {
  display: grid;
  gap: 2px;
  max-width: 340px;
}

.model-menu-item {
  display: grid;
  gap: 2px;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
}

.model-menu-item:hover {
  background: var(--hover);
}

.model-menu-item.active {
  background: var(--chosen);
}

.model-menu-note {
  font-size: 13px;
  color: var(--muted);
}

/*
 * The name wraps rather than being cut with an ellipsis: what
 * distinguishes two models of one family is the tail of the name, and
 * `…v4-fl…` would hide exactly the part worth reading.
 */
.model-menu-id {
  font-size: 12px;
  color: var(--faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  overflow-wrap: anywhere;
}

/*
 * It stands under the list, once, rather than on every row: there is one
 * model, and the name answers "which model computed this" rather than
 * "which of these to press". Indented to the rows' own text so it reads
 * as a footnote to the list and not as a fourth choice.
 */
.model-menu > .model-menu-id {
  padding: 6px 10px 2px;
}

/* ---------- answer ---------- */

.answer {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.answer > section {
  padding: 21px 24px;
  border-bottom: 1px solid var(--line);
}

.answer > section:last-child {
  border-bottom: none;
}

.answer .title {
  background: var(--bg);
  font-weight: 500;
}

/*
 * Completeness warnings and analysis caveats look alike. In a
 * conversation they live under the card's yellow icon; the banner
 * remains where a caveat must be read before the content around it —
 * the session explorer, a page's shared completeness note.
 */
.warnings {
  background: var(--warn-bg);
  border-left: 3px solid var(--warn-line);
  color: var(--warn-text);
}

.warnings ul {
  margin: 0;
  padding-left: 18px;
}

.warnings li + li {
  margin-top: 4px;
}

/*
 * Notes: how to read what is on screen, as against the yellow banner's
 * "something here does not add up". Muted and unbannered on purpose —
 * they are true of every session or every run of a question, so drawn
 * as loudly as a warning they would be a permanent alarm.
 */
.notes {
  color: var(--muted);
  font-size: 13px;
}

.notes ul {
  margin: 4px 0 0;
  padding-left: 18px;
}

.notes li + li {
  margin-top: 4px;
}

/*
 * The model's declared limitation: the asked-for shape of the answer
 * was out of reach and a substitute stands in its place. Red, not the
 * warning yellow — yellow hedges the numbers, red reprices the whole
 * answer's shape — and it must not blend into the caveat banners
 * around it.
 */
.limits {
  background: var(--error-bg);
  border-left: 3px solid var(--error-line);
  color: var(--error);
}

.limits p {
  margin: 6px 0 0;
}

/*
 * The model's conclusion is a message below the chart grid rather than
 * a section inside a card: it is the text read as the answer, about
 * charts already shown. No heading over it — in a conversation, text
 * after the charts is the conclusion, and a label saying so would be
 * the only label in the feed.
 */
/*
 * The model's own text, with its marks read (`markdown.js`).
 *
 * No `white-space: pre-wrap` any more, and that is not a loss: the
 * blank lines that used to do the spacing are paragraphs now, and a
 * single newline inside one is still a line — the parser keeps it as a
 * break, so an answer stored before any of this reads exactly as it
 * did.
 */
.explanation > :first-child {
  margin-top: 0;
}

.explanation > :last-child {
  margin-bottom: 0;
}

.explanation p {
  margin: 0 0 10px;
}

/*
 * A heading inside an answer is a heading of a paragraph, not of the
 * page: the card above it already carries the question. So it is set at
 * the body size and told apart by weight and colour rather than by
 * growing — a model that opens with `#` must not out-shout the chart it
 * is explaining.
 */
.md-heading {
  margin: 16px 0 6px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text);

  /*
   * The page's own `h3` is a section label — small, spaced and set in
   * capitals — and that is right for a label the interface writes and
   * wrong for a line the model wrote. Set back deliberately rather than
   * by omission: a sentence of the answer put into capitals is shouted
   * by the stylesheet, not by whoever wrote it.
   */
  text-transform: none;
  letter-spacing: normal;
}

/*
 * Three steps and no more, the same way text has three: `#` and `##` are
 * what a model structures an answer with, and everything below them is
 * one quiet step rather than four sizes nobody can tell apart.
 */
h4.md-heading {
  font-size: 14px;
}

h5.md-heading,
h6.md-heading {
  font-size: 13px;
  color: var(--muted);
}

.md-list {
  margin: 0 0 10px;
  padding-left: 22px;
}

.md-list .md-list {
  margin: 4px 0 0;
}

.md-list li {
  margin: 0 0 4px;
}

/*
 * A ticked point keeps its box out of the text column: the marker is
 * dropped, because two marks in front of one line is one too many.
 */
.md-task-item {
  list-style: none;
  margin-left: -22px;
}

/* by attribute as well as by class: the page zeroes every checkbox's margin */
input[type="checkbox"].md-task {
  margin: 0 8px 0 0;
  vertical-align: baseline;
  accent-color: var(--accent);
}

/*
 * A quote is marked by a line and a step of text, not by a fill: filled,
 * it would read as one of the page's caveat blocks, which mean something
 * specific here.
 */
.md-quote {
  margin: 0 0 10px;
  padding: 2px 0 2px 14px;
  border-left: 2px solid var(--line-strong);
  color: var(--muted);
}

/* an inline name, quoted the way the registry writes it */
.md-code {
  padding: 1px 5px;
  border-radius: var(--radius);
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  overflow-wrap: anywhere;
}

.md-block {
  margin: 0 0 10px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  font-size: 12px;
  line-height: 1.5;
  overflow-x: auto;
}

.md-block code {
  padding: 0;
  background: none;
}

.md-rule {
  margin: 16px 0;
  border: none;
  border-top: 1px solid var(--line);
}

.md-table {
  margin: 0 0 10px;
}

.md-right {
  text-align: right;
}

.md-center {
  text-align: center;
}

/*
 * A link from the model is shown and not offered: the address stands
 * beside the words instead of behind them, and nothing here is
 * clickable. This text quotes values sent by anyone holding a build of
 * the game, and a page that turned those into a control would put an
 * unread address under the cursor of somebody with an open session.
 */
.md-href {
  margin-left: 4px;
  color: var(--faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85em;
  overflow-wrap: anywhere;
}

.md-href::before {
  content: "(";
}

.md-href::after {
  content: ")";
}

.note {
  color: var(--muted);
  font-size: 13px;
  margin: 8px 0 0;
}

.note:first-child {
  margin-top: 0;
}

details {
  font-size: 13px;
}

details > summary {
  cursor: pointer;
  color: var(--muted);
}

details > summary:hover {
  color: var(--text);
}

pre.json {
  overflow-x: auto;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 15px;
  margin: 8px 0 0;
}

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

/*
 * Four columns, a row of one height. A card's width — 1, 2 or 4 columns
 * — is declared in the card itself and changed from its header; the
 * tiles never have to be laid out by hand, they reflow on their own.
 *
 * Dense packing (`dense`) is deliberately absent here. The order of the
 * cards on a page is the reading order: data quality stands where it
 * stands, because it is what makes the other numbers start lying. A card
 * pulled up from below into a hole would silently rearrange that order.
 */
.board-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: var(--card-row);
  gap: 16px;
}

.size-small {
  grid-column: span 1;
}

.size-medium {
  grid-column: span 2;
}

.size-large {
  grid-column: span 4;
}

/*
 * A narrow screen means two columns, and the widest card shrinks to them
 * as well. Three columns as an intermediate step are not provided: a
 * "whole page" card on three columns stops being a whole page, and a
 * half-page one stops being a half.
 *
 * These rules sit below the ordinary ones deliberately: the selectors
 * carry the same weight, and placed above they would lose to them. A
 * `span 4` in a two-column grid does not shrink by itself — it adds the
 * missing columns, and the page falls apart into a picket fence of
 * strips.
 */
@media (max-width: 1200px) {
  .board-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .size-large {
    grid-column: span 2;
  }
}

@media (max-width: 720px) {
  .board-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .size-medium,
  .size-large {
    grid-column: span 1;
  }
}

.board-card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

/*
 * The answer's own grid, in the conversation feed. The same grid as a
 * page's, one difference: its row is taller. A page row is sized for
 * nine cards competing for one screen; an answer has a handful, and
 * next to the conclusion's text a page-height strip reads as a strip.
 */
.answer-grid {
  grid-auto-rows: 340px;
}

/* a card that failed to compute keeps its slot in the grid rather than standing beside it */
.board-grid > .answer {
  min-width: 0;
  overflow: auto;
}

/*
 * The card header: the title and the buttons on the same line. Below it
 * there is only the chart, so its height is constant — the cards in a
 * row have to agree on their plotting areas, otherwise two neighbouring
 * rows cannot be compared by eye.
 */
.card-head {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  padding: 0 8px 0 16px;
  border-bottom: 1px solid var(--line);
}

.card-title {
  flex: 1;
  min-width: 0;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-tools {
  flex: none;
  display: flex;
  align-items: center;
  gap: 2px;
}

/*
 * The header buttons are smaller than those in the top bar: there are
 * four of them in a row, and in a one-column-wide card they would
 * otherwise take up the title's space.
 */
.card-tool {
  width: 26px;
  height: 26px;
}

.card-tool svg {
  width: 16px;
  height: 16px;
}

/* the three dots stand in a column: the same shape from the set, turned on its side */
.card-tool.dots svg {
  transform: rotate(90deg);
}

/*
 * Chart remarks are yellow, like the caveat banner: this is the same
 * thing tucked under an icon. Hover does not tint it with the accent —
 * a blue "click here" in the place of a warning would stop being a
 * warning.
 */
.card-tool.warn {
  color: var(--warn-line);
}

.card-tool.warn:hover {
  color: var(--warn-text);
  background: var(--warn-bg);
}

.card-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 12px 14px 10px;
}

/* ---------- card menu ---------- */

.card-menu {
  display: grid;
  gap: 2px;
  min-width: 208px;
  max-width: 460px;
}

.card-spec {
  padding: 6px 8px;
  border-radius: var(--radius);
}

.card-spec:hover {
  background: var(--hover);
}

/* the disclosure is open — a hover highlight would leave along with the cursor */
.card-spec[open] {
  background: none;
}

.card-spec[open] > summary {
  color: var(--text);
}

.card-spec .stack > * + * {
  margin-top: 8px;
}

/*
 * An action in the menu is a row rather than a button: an accent-filled
 * button inside a list reads as "the main thing on screen", and this is
 * one item among several.
 */
.card-action {
  padding: 6px 8px;
  font-size: 13px;
  text-align: left;
  background: none;
  border-color: transparent;
  color: var(--text);
}

.card-action:hover:not(:disabled) {
  background: var(--hover);
  border-color: transparent;
  color: var(--text);
}

.card-spec pre.json {
  max-width: 420px;
  max-height: 44vh;
  overflow: auto;
}

/*
 * The numbers table under the three dots. The same disclosure as the
 * spec, with the same ceilings and for the same reason: the menu is a
 * floating layer, and a year of daily rows would otherwise run it off
 * the bottom of the screen.
 */
.card-numbers-body {
  max-width: 420px;
  max-height: 44vh;
  overflow: auto;
}

.card-spec .row {
  margin-top: 8px;
}

.card-remarks {
  max-width: 380px;
  font-size: 13px;
  color: var(--warn-text);
  background: var(--warn-bg);
  border-color: var(--warn-line);
}

.card-remarks ul {
  margin: 6px 0 0;
  padding-left: 18px;
}

.card-remarks li + li {
  margin-top: 4px;
}

/* ---------- full-screen card ---------- */

/*
 * A layer over the page rather than a separate screen: the same card,
 * the same buttons, only "expand" has become "collapse". A fifty-pixel
 * margin all round is what makes the layer announce itself as a layer:
 * the page is visible on every side, and it is clear it has not gone
 * anywhere.
 */
.board-back {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: var(--backdrop);
  opacity: 0;
  transition: opacity var(--mid) var(--ease);
}

.board-back.shown {
  opacity: 1;
}

/*
 * The only place where a size is animated. The "sizes are not animated"
 * rule is about the page not travelling under the cursor while a number
 * is being read off it; here what travels is the layer covering that
 * page, and it cannot not travel: a swap without motion reads as
 * "something else opened" rather than "this card got bigger".
 */
.board-full {
  position: fixed;
  z-index: 21;
  margin: 0;
  box-shadow: var(--shadow);

  transition:
    top var(--mid) var(--ease),
    left var(--mid) var(--ease),
    width var(--mid) var(--ease),
    height var(--mid) var(--ease);
}

/* the card's slot in the grid while it is expanded: empty, but taken */
.board-slot {
  visibility: hidden;
}

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

/*
 * The chart takes everything it is given and is drawn at its real size
 * (see `chartView` in answer.js). The height is set here rather than in
 * the markup: in a page card it is what the header leaves over, and in a
 * conversation answer it is a constant at which a two-week series reads
 * while the card still fits on screen in full.
 */
.chart-wrap {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 296px;
}

.card-body .chart-wrap {
  flex: 1;
  height: auto;
}

.chart-field {
  position: relative;
  flex: 1;
  min-height: 0;
}

/*
 * The number under the cursor. A layer over the chart — hence the
 * shadow, as on every layer. It catches no clicks: the tooltip stands
 * exactly where the mouse is heading, and by intercepting the cursor it
 * would flicker at itself.
 */
.chart-tip {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  padding: 8px 10px;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  white-space: nowrap;
}

.chart-tip[hidden] {
  display: none;
}

.tip-head {
  font-size: 12px;
  color: var(--muted);
}

.tip-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px;
  font-weight: 600;
  margin-top: 1px;
}

.tip-note {
  font-size: 12px;
  color: var(--faint);
  margin-top: 2px;
}

/*
 * The breakdowns in a column: swatch, name, value. The value is pushed
 * right and set in a monospace face — they are read one under another
 * and compared against each other rather than against the name.
 */
.tip-row {
  display: grid;
  grid-template-columns: 9px minmax(0, 1fr) max-content;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  margin-top: 3px;
}

.tip-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.tip-cell {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 600;
}

/* a single number filling the card sits in the middle, not under its header */
.chart-number {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 1;
  min-height: 0;
}

.chart {
  display: block;
  overflow: visible;
}

.chart .axis {
  stroke: var(--line-strong);
  stroke-width: 1;
}

.chart .grid {
  stroke: var(--line);
  stroke-width: 1;
  stroke-dasharray: 2 4;
}

/*
 * A breakdown's colour arrives as a `fill`/`stroke` attribute from the
 * palette, and CSS overrides a presentational attribute — which is why
 * there is none here. Otherwise every breakdown is coloured the same and
 * a chart with two platforms reads as a single series.
 */
.chart .whisker {
  stroke: var(--text);
  stroke-width: 1.5;
  fill: none;
}

.chart .line {
  fill: none;
  stroke-width: 2;
}

/*
 * The previous period is a dashed line across the bar. Not a fill and
 * not a second bar: a prominent quantity next to the current one reads
 * as one more breakdown, whereas "before" is a marker rather than a
 * value in the series.
 */
.chart .previous {
  stroke: var(--muted);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

/*
 * The previous period under a daily series: the same colour as its own
 * line, dashed and dimmed. Same colour so the eye pairs the two without
 * a legend entry; dashed and faded so "before" never reads as one more
 * breakdown.
 */
.chart .previous-line {
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  opacity: 0.55;
}

.chart .tick,
.chart .sample {
  fill: var(--faint);
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.chart .value {
  fill: var(--text);
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/*
 * The hover band: a place where the cursor is caught and nothing more.
 * It has no fill at all — highlighting the band under the cursor on a
 * fourteen-day chart would read as a selected day.
 */
.chart .band {
  fill: transparent;
}

/* the rule under the cursor: which band is currently in the tooltip */
.chart .guide {
  stroke: var(--line-strong);
  stroke-width: 1;
  pointer-events: none;
}

.chart .guide[hidden] {
  display: none;
}

/* a gap: the slot is taken, the value is absent */
.chart .gap {
  fill: var(--missing);
  font-size: 10px;
}

.chart .series-name {
  fill: var(--muted);
  font-size: 11px;
}

.big-number {
  font-size: 34px;
  font-weight: 600;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.big-number.missing {
  color: var(--missing);
}

/*
 * The legend does not grow at the chart's expense: there can be a dozen
 * breakdowns, and three rows of names under the picture in a
 * one-row-tall card leave nothing of the picture. What does not fit is
 * read by scrolling — or by expanding the card to full screen, where
 * there is room for all of them.
 */
.legend {
  flex: none;
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--muted);
  margin-top: 6px;
  max-height: 42px;
  overflow-y: auto;
}

.legend .swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2.8px;
  margin-right: 5px;
  vertical-align: baseline;
}

/*
 * A legend entry is pressed — it hides its series and rescales the chart —
 * so it is a button, by the rule that everything pressed is one. It does
 * not look like a button: the legend is a caption, and a row of framed
 * controls under the picture would read as the card's own toolbar.
 */
.legend-item {
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.legend-item:hover {
  color: var(--text);
}

/*
 * A hidden series keeps its entry, struck through: the list you are using
 * to bring it back must not lose the row you are looking for.
 */
.legend-item.off {
  color: var(--missing);
  text-decoration: line-through;
}

.legend-item.off .swatch {
  opacity: 0.4;
}

/* the rule between the readings and the ordering: two kinds of choice */
.menu-rule {
  height: 1px;
  margin: 4px 0;
  background: var(--line);
}

/* ---------- the sweep ---------- */

/*
 * A sweep of the headline metrics. A block of its own rather than a card,
 * because a card promises numbers reproducible by its own spec and a sweep
 * has no single spec — so it takes the section frame the answer's other
 * blocks take, and not the card frame.
 */
.sweep {
  padding: 21px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.sweep h3 {
  margin: 0 0 2px;
  font-size: 14px;
}

/*
 * One grid for the whole table, and the rows take part in it through
 * `display: contents` rather than each being a grid of its own. A grid per
 * row sizes its columns by its own contents, and then the numbers of one
 * metric stand under the names of another — in a table read by scanning a
 * column down, that is the one thing the layout has to get right.
 */
.sweep-rows {
  display: grid;
  grid-template-columns: minmax(90px, 1.4fr) minmax(80px, 2fr) auto auto auto;
  align-items: center;
  gap: 6px 10px;
  margin-top: 12px;
  font-size: 13px;
}

.sweep-row {
  display: contents;
}

.sweep-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The track has a centre line, and a movement grows from it: left for a
 * fall, right for a rise. The axis from zero is the same requirement every
 * chart here has, only the zero is in the middle — a bar of "minus twelve
 * percent" growing from the left edge would be read as a magnitude.
 */
.sweep-track {
  position: relative;
  height: 12px;
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
}

.sweep-track::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -2px;
  bottom: -2px;
  border-left: 1px solid var(--line-strong);
}

/*
 * One colour for both directions. A fall in churn is good news and a rise
 * in it is not, and the picture cannot know which metric it is drawing —
 * green and red here would be a verdict handed out by a bar chart.
 */
.sweep-fill {
  position: absolute;
  top: 2px;
  height: 8px;
  background: var(--accent);
  opacity: 0.75;
}

.sweep-fill.up {
  left: 50%;
}

.sweep-fill.down {
  right: 50%;
}

.sweep-change,
.sweep-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.sweep-was {
  color: var(--faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.sweep .note {
  margin-top: 12px;
}

/*
 * An empty legend takes no room at all. `display: flex` above beats
 * `hidden` on its own, and a box with nothing in it would still push the
 * chart up by its own margin.
 */
.legend[hidden] {
  display: none;
}

/*
 * A map has no legend — a ramp with its ends named would repeat above
 * the picture what the picture says. What stands in that place is the
 * one thing the map cannot say itself: the countries it has no shape
 * for. Set in the utility grey, because it is a caveat and not a
 * reading.
 */
.map-legend {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
}

.map-legend .faint {
  font-family: inherit;
  color: var(--faint);
}

/* ---------- map ---------- */

/*
 * The map is a picture with a fixed `viewBox` scaled to the box — the
 * one chart on the page not drawn at its real size. There is nothing in
 * it set in type: outlines scale without becoming coarser, and the
 * numbers live in the legend and the tooltip.
 */
.chart.map {
  width: 100%;
  height: 100%;
}

/*
 * The line between countries is the page's own background rather than a
 * grey: on a filled map a border is not a line drawn over the countries
 * but the gap between them, and a grey one adds a third colour to a
 * picture built out of two.
 */
/*
 * The fill is mixed here rather than computed in `answer.js`, which only
 * sets `--shade` — a country's place on the ramp from 0 to 1. The ends
 * are theme colours: a fill computed once in JavaScript would stay the
 * light theme's after the system switched to dark, while a mix of two
 * variables is recoloured by the browser itself.
 *
 * The plain fill above it is for a browser without `color-mix`: the
 * whole map comes out one colour, which is a map without a scale rather
 * than a black rectangle where the world should be.
 */
.chart .country {
  fill: var(--map-strong);
  fill: color-mix(
    in srgb,
    var(--map-strong) calc(var(--shade, 1) * 100%),
    var(--map-weak)
  );
  stroke: var(--map-line);
  stroke-width: 0.4;
  stroke-linejoin: round;
}

.chart .country.blank {
  fill: var(--map-blank);
}

/*
 * The country under the cursor is outlined rather than tinted: a change
 * of fill on a map whose fill is the value would read as a change in the
 * number.
 */
.chart .country:hover {
  stroke: var(--text);
  stroke-width: 1;
}

/* ---------- the readings ---------- */

/*
 * A grid's cell takes the map's ramp, and for the same reason the map has
 * one: the colour is a quantity, so it is one tone from pale to dark
 * rather than a scale of hues. A red-to-green grid of retention would
 * turn a number of players into a verdict on them.
 */
.chart .cell {
  fill: var(--map-strong);
  fill: color-mix(
    in srgb,
    var(--map-strong) calc(var(--shade, 1) * 100%),
    var(--map-weak)
  );
  stroke: var(--bg);
  stroke-width: 1;
}

/*
 * A cell with nothing in it is the map's blank rather than the palest
 * shade, and it carries the hatching besides: "fewest" and "not measured"
 * are different answers, and in a grid of cohorts the unmeasured corner
 * is a whole triangle that must not read as a run of zeros.
 */
.chart .cell.blank {
  fill: var(--map-blank);
}

.chart .cell:hover {
  stroke: var(--text);
}

/*
 * The hatching of everything that must not be read as settled: a thin
 * sample, an interval still filling up, a cohort whose day has not
 * arrived. One texture for all three — they say the same thing about the
 * number — and it is drawn in the interface's own faint grey rather than
 * in the series' colour, because it is not a quantity.
 */
.chart .hatch-line {
  /*
   * The page's own text colour, not a grey. The texture lies over fills
   * of every tone — a saturated series colour, a pale grid cell — and it
   * has to stay visible on both themes: a mid grey disappears over orange
   * on the dark theme, which is exactly where it was first missed. Taking
   * the text colour means taking whichever end of the contrast the theme
   * keeps for reading.
   */
  stroke: var(--text);
  stroke-width: 2;
}

/*
 * The texture lies over a shape that is already drawn in its colour, and
 * it catches nothing: the shape underneath owns the hover, and a
 * transparent lid over it would swallow the tooltip.
 */
.chart .hatch-over {
  pointer-events: none;

  /*
   * Faint enough to be a texture rather than a second fill: what is
   * underneath is a real number in its own colour, and this says only
   * that it must not be read as settled.
   */
  opacity: 0.35;
}

/*
 * A dot that is not settled: hollow, with the line through it left whole.
 * The number is real — a thin sample is a real measurement of too few
 * things — so nothing is broken, only marked.
 */
.chart .dot.unsettled {
  stroke-width: 2;
}

/* "was" on a dumbbell: the same colour, hollow, and the line to "is now" */
.chart .dot.before {
  fill: var(--bg);
  stroke-width: 2;
}

.chart .dumbbell {
  stroke-width: 2;
  opacity: 0.45;
}

/*
 * A dated mark: a thin vertical rule the whole height of the plot. Not a
 * band across the days an annotation lasted — a colour laid over the
 * numbers would tint the very thing it is there to explain — and not the
 * accent, which on this page means "press here".
 */
.chart .mark {
  stroke: var(--muted);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

/* the strip that catches the cursor around a mark or a threshold */
.chart .mark-grab {
  fill: transparent;
}

/*
 * A threshold from an alert rule: solid, dimmer than the data, and never
 * red. Red on this page belongs to the irreversible, and a number above a
 * threshold is a state of affairs rather than an action — the rule's own
 * name says what it means, under the cursor.
 */
.chart .reference {
  stroke: var(--muted);
  stroke-width: 1.5;
  stroke-dasharray: 6 4;
}

/* the rule's name on its own line: the line without it is a line somebody drew */
.chart .reference-name {
  fill: var(--muted);
  font-size: 10px;
}

/*
 * The band between the outermost quantiles: the lines' own colour, faint
 * enough that the lines stay the picture. It is drawn from the same
 * numbers as they are, so it must not compete with them.
 */
.chart .band-fill {
  opacity: 0.12;
}

/* the time to a funnel step, under the count and in its own unit */
.chart .secondary {
  fill: var(--muted);
  font-size: 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/*
 * A node of a path tree: a box with the event's name in it. Bordered
 * rather than filled — a fill would be a quantity, and the quantity here
 * is the height of the box.
 */
.chart .node {
  fill: var(--panel);
  stroke: var(--line-strong);
  stroke-width: 1;
}

.chart .node:hover {
  stroke: var(--text);
}

.chart .node-label {
  fill: var(--text);
  font-size: 11px;
}

/*
 * The elbow joining a node to its parent. Darker than the boxes' own
 * border: the branches are the structure of the picture, and drawn in the
 * same grey as the outlines they disappear into the gaps between them.
 */
.chart .twig {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.25;
}

/* the name of a facet or a pane, on the strip itself rather than in a legend */
.chart .facet-name {
  fill: var(--muted);
  font-size: 11px;
}

/*
 * The list of readings. Wider than the other popovers because each entry
 * is a sentence rather than a word: it names the question the reading
 * answers, the way the model list names what a model is for rather than
 * who made it.
 */
.popover.readings {
  min-width: 210px;
}

.reading-menu-item {
  width: 100%;
  padding: 6px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font: inherit;
  text-align: left;
}

.reading-menu-item:hover {
  background: var(--hover);
  color: var(--text);
}

/*
 * The reading currently drawn stays in the list and is marked. A list that
 * loses the entry you are looking at cannot be learned, and half of why
 * somebody opens it is to see where they are.
 */
.reading-menu-item.current {
  background: var(--chosen);
  color: var(--text);
}

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

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 13px;
}

th,
td {
  text-align: left;
  padding: 5px 10px 5px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

th {
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
}

td.num,
th.num {
  text-align: right;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  padding-right: 16px;
}

/* "no data" is never printed as a zero */
td.missing,
dd.missing {
  color: var(--missing);
}

tr.clickable {
  cursor: pointer;
}

tr.clickable:hover td {
  background: var(--hover);
}

tr.flagged td:first-child {
  border-left: 3px solid var(--warn-line);
  padding-left: 7px;
}

tr.muted td {
  color: var(--muted);
}

.table-wrap {
  overflow-x: auto;
}

.flag {
  display: inline-block;
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 14px;
  border: 1px solid var(--warn-line);
  color: var(--warn-text);
  background: var(--warn-bg);
  margin-right: 4px;
}

.flag.ok {
  border-color: var(--line-strong);
  background: none;
  color: var(--muted);
}

.pairs {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 3px 16px;
  font-size: 13px;
}

.pairs dt {
  color: var(--muted);
}

.pairs dd {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /*
   * An ingestion key is a long string without spaces: without
   * character-level wrapping it stretches the card past the edge of the
   * screen.
   */
  overflow-wrap: anywhere;
}

/* ---------- ingestion key ---------- */

/*
 * A value carried from here into somebody else's project — the only
 * place of its kind in the interface, and that is why it does not look
 * like card text. The blue fill here is neither decoration nor a breach
 * of the "colour belongs to the data" rule: what is painted with the
 * accent is what gets clicked, and what gets clicked is the whole block.
 *
 * The block is a button for the keyboard's sake: the key is carried away
 * without a mouse too, and a `div` with a handler is not reachable by
 * Tab and not pressed by Enter.
 */
.keys {
  display: grid;
  gap: 9px;
  margin: 12px 0;
}

.key-copy {
  display: grid;
  grid-template-columns: 1fr max-content;
  align-items: center;
  gap: 18px;
  width: 100%;
  padding: 15px 18px;
  text-align: left;
  background: var(--accent-soft);
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
}

.key-copy:hover {
  border-color: var(--accent);
}

.key-copy-text {
  display: grid;
  gap: 3px;
  min-width: 0;
}

.key-copy-name {
  color: var(--muted);
  font-size: 12px;
  letter-spacing: 0.02em;
}

.key-copy-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;

  /* the key is a long string without spaces: otherwise the block runs past the edge */
  overflow-wrap: anywhere;
}

/*
 * The icon is not a button inside a button: a nested button would break
 * both the markup and the mouse target — half the block would copy and
 * half would not.
 */
.key-copy-icon {
  display: inline-flex;
  color: var(--muted);
}

.key-copy:hover .key-copy-icon {
  color: var(--accent);
}

.key-copy-icon svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
}

/* a checkmark in place of the icon: a response to the click that does not shift the page */
.key-copy.copied .key-copy-icon {
  color: var(--ok);
}

/*
 * The width of the settings page caps the content, not the page. The cap
 * used to sit on `.settings` itself — and that element is the scrolling
 * box: capping it took the scrollbar off the window edge and stood it in
 * the middle of the screen, with the empty half of a wide monitor to the
 * right of the bar rather than under it.
 *
 * The cap is a row of three blocks at their own full width. Line length
 * is already bounded inside a block, so there is nothing left for the
 * page to protect: it only says where the row ends, and it ends where the
 * blocks do. Beyond that width the leftover space is split in two rather
 * than piled up on the right.
 */
.settings > * {
  max-width: calc(3 * 520px + 2 * 16px);
  margin-inline: auto;
}

/*
 * The settings blocks stand in a row and wrap as far as they fit: they
 * are independent pieces rather than steps, and a column of them would
 * promise an order that does not exist between them.
 *
 * Columns rather than a wrapping row of flexible blocks. Under a flex
 * wrap each row divided its own width: three blocks in one row came out
 * narrower than two in the next, and a block left alone in its row
 * stretched to its cap. Widths that differ from row to row read as a
 * difference in weight between the blocks, and there is none — they are
 * four independent pieces. A track is the same width wherever it stands.
 *
 * The count is written out rather than left to `auto-fill` or
 * `auto-fit`: the first counts its repetitions by the track's maximum
 * and lays out two columns of 520px where three of 447px would fit, and
 * the second collapses the tracks nobody occupies, which is the same
 * thing one card later.
 */
.settings-blocks {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 520px));
  align-items: start;
  gap: 16px;
}

.settings-block {
  min-width: 0;
}

/*
 * Fewer columns rather than narrower ones. The numbers here are window
 * widths, not content widths, and they are allowed to be: the rail is a
 * fixed 244px, 200px below 1000px, and the page padding is 32px, 20px
 * below 1000px — so what a column gets follows from the window alone.
 *
 * A tablet is the case these two thresholds are for. In landscape it
 * holds two columns comfortably; in portrait they come out at some 280px
 * each, which is narrow but is still two columns of blocks rather than
 * one column with the other two pushed below the fold.
 *
 * Two columns is also the width at which the four blocks fill the grid
 * exactly, two rows by two. Three of them in two columns would leave the
 * second column empty for the whole height of the page: a grid row is as
 * tall as the tallest card in it, and nothing is ever placed beside a
 * card that starts a row of its own.
 */
@media (max-width: 1279px) {
  .settings-blocks {
    grid-template-columns: repeat(2, minmax(0, 520px));
  }
}

@media (max-width: 799px) {
  .settings-blocks {
    grid-template-columns: minmax(0, 520px);
  }
}

/* ---------- danger zone ---------- */

.erase-block h2 {
  margin-top: 0;
}

/*
 * A row per part. The separators run between rows rather than around
 * them: eight boxes in a frame read as eight cards, while this is one
 * list where the consequences grow downward.
 */
.erase-row + .erase-row {
  border-top: 1px solid var(--line);
}

.erase-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
}

.erase-name {
  min-width: 0;
}

/*
 * The button is outlined rather than filled: filled red on eight rows
 * turns the whole block into a wall of alarm, in which the two rows that
 * really are irreversible stop standing out. Red arrives on hover — that
 * is, once the pointer has already chosen a row.
 */
.erase-open {
  flex: none;
  background: none;
  color: var(--text);
  border-color: var(--line-strong);
}

.erase-open:hover:not(:disabled) {
  background: var(--error-bg);
  border-color: var(--error-line);
  color: var(--error);
}

/* the two that take the game with them are red from the start */
.erase-open.danger {
  color: var(--error);
  border-color: var(--error-line);
}

.erase-confirm {
  border-left: 3px solid var(--error-line);
  background: var(--error-bg);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 0 0 12px;
}

.erase-confirm h3 {
  margin: 0 0 8px;
  font-size: 14px;
  color: var(--error);
}

.erase-confirm p {
  margin: 8px 0;
}

.erase-confirm label {
  display: block;
  margin-top: 12px;
}

.erase-confirm code {
  user-select: all;
  font-weight: 600;
}

/*
 * What goes, item by item with its count. A list rather than a
 * paragraph: it is read to find the one line that makes a person stop,
 * and a line is found in a list, not in prose.
 */
.erase-list {
  margin: 4px 0 8px;
  padding-left: 20px;
  font-size: 13px;
}

.erase-list li {
  margin: 2px 0;
}

/*
 * The consequence that outlives the deletion — a build that keeps
 * sending, a rule that goes silent. Yellow, because it is a caveat about
 * what happens next rather than about the loss itself, which the red
 * frame around it already carries.
 */
.erase-note {
  border-left: 2px solid var(--warn-line);
  padding-left: 10px;
  color: var(--muted);
}

.erase-field {
  width: 100%;
  max-width: 320px;
  margin-top: 6px;
}

.erase-actions {
  margin-top: 12px;
}

/*
 * The only filled red button on the page, and it is unreachable until
 * the identifier is typed. Disabled it stays red rather than turning
 * grey: it must be visible what the field is about to unlock.
 */
.erase-go {
  background: var(--error);
  border-color: var(--error);
}

.erase-go:hover:not(:disabled) {
  background: var(--error-line);
  border-color: var(--error-line);
}

/* ---------- read keys ---------- */

/*
 * The list borrows the danger zone's row anatomy — separators between
 * rows, name left, actions right — because it answers the same kind of
 * question ("which of these do I act on"), and the revoke button reuses
 * `.erase-open.danger` outright: revoking is the same one-way door, and
 * a second red of its own would be a third color meaning "no way back".
 */
.read-key-row + .read-key-row {
  border-top: 1px solid var(--line);
}

.read-key-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
}

.read-key-name {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.read-key-status {
  margin-left: 8px;
}

.read-key-acts {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}

/*
 * The question stands in the row, in words, on the red background every
 * irreversible confirmation on the page shares.
 */
.read-key-confirm {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  border-left: 3px solid var(--error-line);
  background: var(--error-bg);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin: 0 0 12px;
  font-size: 13px;
}

.read-key-log {
  margin: 0 0 12px;
  display: grid;
  gap: 8px;
}

.read-key-log-row {
  display: grid;
  gap: 2px;
}

/*
 * The SQL is the payload of the journal, not decoration, and it is a
 * long string written by a program: it wraps anywhere rather than
 * scrolling, because a horizontal scrollbar per row hides exactly the
 * tail the row was opened for.
 */
.read-key-sql {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  color: var(--muted);
}

/*
 * The setup card sits between the description and the list of keys: it
 * is the thing handed over together with a key, and it belongs beside
 * them rather than under everything.
 */
.read-key-setup {
  margin: 12px 0 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.read-key-setup p {
  margin: 8px 0 0;
}

.read-key-issue {
  border-top: 1px solid var(--line);
  padding-top: 12px;
  margin-top: 12px;
}

.read-key-issued {
  display: grid;
  gap: 12px;
}

/*
 * The heading of an explorer card and the actions on what it shows, on
 * one line. Not `.card-head`: that one belongs to the chart card, where
 * the height is fixed because neighbouring cards have to agree on their
 * plotting areas. Here the card is a page-wide block and the row is only
 * as tall as its buttons.
 *
 * The actions stand at the far end rather than beside the title. The
 * title carries a session id — sixty characters of it — and a button
 * following that text would land in a different place on every session.
 */
.explorer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.explorer-head h2 {
  margin: 0;
}

.explorer-head .row {
  gap: 8px;
}

/* a gap in the timeline: a row in place of the hole rather than silence */
.gap-row td {
  background: var(--warn-bg);
  color: var(--warn-text);
  font-size: 12px;
}

/* ---------- narrow window ---------- */

/*
 * The tool is opened on a working monitor; there is no phone layout
 * here. A narrow window simply tightens the margins so that a table does
 * not run off the screen entirely.
 */
@media (max-width: 1000px) {
  :root {
    --rail-width: 200px;
  }

  .view,
  .chat-log {
    padding-left: 20px;
    padding-right: 20px;
  }

  .composer {
    padding-left: 20px;
    padding-right: 20px;
  }
}

/* ---------- game list ---------- */

/*
 * The first screen after signing in. Tiles rather than a table: there
 * are few games, each has a face, and they are recognised by eye from
 * the icon rather than by reading identifiers down a column.
 */
.games-screen {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.games-screen[hidden] {
  display: none;
}

.games-main {
  flex: 1;
  padding: 24px;
  overflow-y: auto;
}

.games {
  max-width: 1000px;
  margin: 0 auto;
}

.games-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
}

.games-head h1 {
  margin: 0;
  font-size: 20px;
}

.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
}

.game-card {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 21px;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  font: inherit;
  color: var(--text);
  transition: border-color var(--fast) var(--ease),
    background var(--fast) var(--ease);
}

/*
 * Hover strengthens the line rather than filling the block. In this
 * theme a `--hover` fill reads as a dark blue blotch the size of a card
 * — it marks navigation items and table rows, that is narrow and dense
 * things. A card is a block, and depth on blocks here is made with a
 * line (DESIGN.md).
 */
.game-card:hover {
  border-color: var(--line-strong);
}

.game-icon {
  flex: none;
  width: 48px;
  height: 48px;
  border-radius: var(--radius);
  object-fit: cover;
}

.game-icon.letter {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 22px;
  font-weight: 600;
}

/*
 * The same mark inline — in the header and in the game-switch list. Only
 * the size differs: a game is recognised by its picture, and a second
 * look for it would imply that the header and the list show different
 * games.
 */
.game-icon.tiny {
  width: 24px;
  height: 24px;
}

.game-icon.letter.tiny {
  font-size: 12px;
}

.game-body {
  min-width: 0;
  flex: 1;
}

.game-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.game-id {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The numbers stand in a row and are captioned: the "yesterday" beneath
 * them is not decoration but part of the number. A DAU without a period
 * reads as "right now".
 */
.game-numbers {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: auto auto;
  justify-content: start;
  gap: 0 20px;
  margin: 8px 0 4px;
}

.game-numbers dt {
  font-size: 11px;
  color: var(--faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.game-numbers dd {
  margin: 0;
  font-size: 18px;
  font-variant-numeric: tabular-nums;
}

.new-game {
  max-width: 520px;
}

.new-game h2 {
  margin: 0 0 4px;
  font-size: 16px;
}

/*
 * The fields for creating a game and for its settings share one rule:
 * they are the same name and icon, and having drifted apart they would
 * look like two different forms about the same thing.
 */
.new-game label,
.game-identity label,
.game-filters label,
.read-key-issue label {
  display: block;
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--muted);
}

/* the file picker stretches to full width on its own; it needs that everywhere */
.new-game input:not([type="file"]),
.game-identity input:not([type="file"]),
.game-filters input,
.game-filters select,
.read-key-issue input {
  width: 100%;
}

.game-filters .row {
  margin-top: 16px;
}

/*
 * The list of defined filters, above the form that adds one. Rows
 * separated by lines rather than boxed: this is one list, and five
 * frames inside a block already framed read as five cards.
 */
.game-filter-rows {
  margin-top: 12px;
  border-top: 1px solid var(--line);
}

.game-filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}

/*
 * The name and what it reads, one under the other. The second line is
 * the one that gets long — an event name and a parameter name — and it
 * is the one allowed to wrap, while the buttons keep their width.
 */
.game-filter-what {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.game-filter-row .row {
  flex: none;
}

/*
 * The panel, as the list it is: one row per pinned filter, moved by the
 * grip on the left and unpinned by the button on the right. The rows
 * behave like a conversation in the rail — same idea, same reason: two
 * actions of their own, so a row is a row and not a control with a
 * label.
 */
.game-pin-rows {
  margin-top: 12px;
  border-top: 1px solid var(--line);
}

.game-pin-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 4px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}

/*
 * The row being dragged keeps its place in the flow and goes pale: a
 * row lifted out of the list would leave a gap that is not where it
 * lands, and the gap is the one thing the hand is aiming at.
 */
.game-pin-row.dragging {
  opacity: 0.4;
}

/*
 * `grab` standing still, `grabbing` while held. The cursor is the whole
 * of the affordance here — the icon says which part of the row is the
 * handle, and the cursor says what the handle does.
 */
.game-pin-grip {
  width: 24px;
  height: 24px;
  flex: none;
  cursor: grab;
  color: var(--faint);
}

.game-pin-grip:active {
  cursor: grabbing;
}

.game-pin-row:hover .game-pin-grip,
.game-pin-row:focus-within .game-pin-grip {
  color: var(--muted);
}

.game-pin-grip svg {
  width: 16px;
  height: 16px;
}

/*
 * The name and what it reads, one under the other. The second line is
 * the one that gets long — an event name and a parameter name — and it
 * is the one allowed to wrap, while the buttons keep their width.
 */
.game-pin-what {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;

  /* the second line is a control on some rows: it starts where the name does */
  align-items: flex-start;
}

/*
 * The caption that is also a button, on filters over events. Underlined
 * on hover rather than coloured: it stands in the muted line under a
 * name, and an accent there would read as the row's own state rather
 * than as something to press.
 */
.game-pin-scope {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}

.game-pin-scope:hover,
.game-pin-scope:focus-visible {
  color: var(--text);
  text-decoration: underline;
}

/*
 * Hidden by transparency rather than by `display`, as in the rail: the
 * button stays on the keyboard's path and `:focus-within` shows it to
 * whoever arrives there by Tab.
 */
.game-pin-acts {
  flex: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}

.game-pin-acts[hidden] {
  display: none;
}

.game-pin-row:hover .game-pin-acts,
.game-pin-row:focus-within .game-pin-acts {
  opacity: 1;
  pointer-events: auto;
}

.game-pin-act {
  width: 28px;
  height: 28px;
}

/*
 * The name is edited over the row rather than in place of it: the row
 * is two lines tall and a field is one, so everything below would jump
 * up by a line the moment the pencil is clicked.
 */
.game-pin-rename {
  position: absolute;
  top: 6px;
  right: 8px;
  left: 44px;
  margin: 0;
  border-color: var(--accent);
}

.game-pin-act svg {
  width: 16px;
  height: 16px;
}

/*
 * The question lies over the right-hand end of the row rather than in
 * its place: the row is two lines tall and the question is one, so
 * every row below would jump up by a line the moment the pin is
 * clicked. Worded rather than armed, for the reason the rail's is: an
 * icon has nowhere to write "for sure?", and two identical pins, one of
 * which unpins, are worse than an extra word.
 */
.game-pin-confirm {
  position: absolute;
  top: 1px;
  right: 1px;
  bottom: 1px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 8px 0 18px;
  border-radius: var(--radius);
  background: linear-gradient(to right, transparent, var(--hover) 18px);
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
}

/*
 * Adding is a button under the list rather than a row inside it: what
 * it opens is a layer, and a row that opened a layer would read as one
 * more filter with a strange name.
 */
.game-pin-add {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding: 6px 12px 6px 8px;
}

.game-pin-add svg {
  width: 16px;
  height: 16px;
}

/*
 * The layer the filters are picked from. Capped in both directions: the
 * registry grows without limit, and this is the list that grows with
 * it — which is the whole reason it lives in a layer rather than on the
 * page.
 */
.game-pin-menu {
  display: grid;
  gap: 2px;
  min-width: 300px;
  max-width: 420px;
  max-height: min(60vh, 480px);
  overflow-y: auto;
}

.game-pin-search {
  padding: 0 4px 4px;
}

.game-pin-find {
  width: 100%;
  margin: 0;
}

.game-pin-options {
  display: grid;
  gap: 2px;
}

.game-pin-group {
  margin-top: 8px;
}

.game-pin-group h3 {
  margin: 0 0 2px;
  padding: 0 8px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.game-pin-option {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/*
 * The same hover a conversation in the rail gets, and the same reason
 * it is spelled out here: a row in a list is a `button` for the
 * keyboard, not for looks, so without its name in the exclusion list
 * above it took the filled button's hover — accent behind text that
 * stayed dark, which in the light theme is one unreadable row under
 * the cursor.
 */
.game-pin-option:hover,
.game-pin-option:focus-visible {
  color: var(--text);
  background: var(--hover);
}


/* the button at the foot of the block rather than flush against what is above it */
.game-identity .row {
  margin-top: 16px;
}

/*
 * The icon sits between the label and the file picker: first what is
 * there now, then what to replace it with. The top margin is smaller
 * than the bottom one — the preview belongs to the label above it rather
 * than to the field below.
 */
.game-identity .icon-preview {
  margin: 6px 0 12px;
}

/*
 * Edited but unsaved is yellow: the same colour that marks every caveat
 * on the page. Not red — nothing is broken; and not grey — otherwise it
 * is indistinguishable from an ordinary field.
 */
.game-identity input.dirty,
.game-notes textarea.dirty {
  border-color: var(--warn-line);
}

.game-identity .icon-preview.dirty .game-icon {
  outline: 2px solid var(--warn-line);
  outline-offset: 2px;
}

/*
 * The counter stands at the far end of the button's row, against the
 * right edge of the block: it is a reference number, not an instruction,
 * and next to "Save" it would read as a caption for the button. Tabular
 * digits so that the number does not twitch on every keystroke.
 */
.game-notes .notes-count {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

/*
 * The budget is spent: from here on the field silently refuses input,
 * and a grey number would leave that unexplained — the person would be
 * typing into a field that stopped listening. The same yellow every
 * caveat on the page is set in.
 */
.game-notes .notes-count.full {
  color: var(--warn-text);
}

.new-game code {
  user-select: all;
  word-break: break-all;
}

/*
 * The icon preview is the same size as in the list: the crop should be
 * inspected at the size the icon will actually be seen at.
 */
.icon-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 8px 0 16px;
}

.icon-preview:empty {
  display: none;
}

.small {
  font-size: 12px;
}

/*
 * A proposal: what the model offered and nobody has agreed to yet.
 *
 * Marked by its left edge rather than by a colour of its own. A card
 * assembled from real, computed charts is meant to look exactly like the
 * page it will become — that is the whole point of a preview — so the
 * one thing that must read differently is its status, and a stripe says
 * "this is not on the shelf yet" without repainting the charts.
 */
.answer.proposal {
  border-left: 3px solid var(--accent);
}

/*
 * The preview grid inside a proposal keeps the page's row height. A
 * grid that sized itself to its contents here would show cards of a
 * different shape from the ones the button produces.
 */
.answer.proposal .board-grid {
  grid-auto-rows: var(--card-row);
}

/*
 * What the data is missing: a list of items, not a wall of text. Each
 * one is a separate thing somebody would have to add, and they are
 * usually added by different people.
 */
.gap-item + .gap-item {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.gap-item h3 {
  margin: 0 0 6px;
  font-size: 14px;
  font-weight: 500;
}

.gap-item p {
  margin: 0 0 6px;
}

/* the link a saved proposal leaves behind, in place of its button */
a.link {
  color: var(--accent);
  text-decoration: none;
}

a.link:hover {
  text-decoration: underline;
}
