/*
  Tokens from the design handoff. Written by hand rather than generated by a utility framework,
  because the system is small and specific — three text steps, one accent whose value changes but
  never its hue, four state glyphs — and a utility layer would bury exactly the discipline that
  makes it work.

  The one rule the whole visual system exists to serve:
    accent  = measured
    amber   = declared, unverified, or ageing
    absence = unknown
  Never used decoratively. That is why the accent is not spent on buttons or links in body copy.
*/

:root {
    /*
      62.5% of the reader's browser default rather than a fixed size, so the site actually tracks
      the font-size preference a reader sets — the control someone reaches for who wants bigger text
      without bigger everything, and the one browser *zoom* does not replace. 1rem no longer means
      "the base"; it means a tenth of it, chosen so the type scale below reads as its own pixel value
      (1.35rem is 13.5px at a 16px default) instead of the awkward fractions a body-relative rem
      would produce. Every size in this file is rem so it moves with that preference; only the
      handful the comments beside them name as deliberately exempt stay in absolute units.
    */
    font-size: 62.5%;

    /*
      Spacing, radii and motion, in the relationships the design handoff's own token files carry.
      The values are theirs; the colours below are ours — the handoff's palette is a different
      product's and its README says so. What transfers is the scale: content padding at 20px rather
      than the 14 this site had, one radius for controls and a larger one for the surfaces that hold
      them, and a single easing curve at an eighth of a second.
    */
    --cpad: 24px;               /* the density control writes this, and only this */
    --row-pad: 13px;            /* a listing row: 13px 20px, over a 44px floor */

    /*
      The one horizontal inset the whole site shares — the bar, every content band, every listing
      row. Fluid rather than fixed because the page is full-bleed now: at the width the handoff was
      drawn to it lands on the 24px the drawing uses, and it grows a little rather than leaving a
      wide monitor's chrome pinned to the very edge of the glass.
    */
    --gutter: clamp(14px, 1.6vw, 28px);

    --radius: 9px;              /* controls, chips, rows, nav items */
    --radius-lg: 14px;          /* cards and panels — the things controls sit in */
    --radius-pill: 99px;        /* pills and badges */

    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --dur: 0.14s;

    --bg: #0f1113;
    --surface: #16191c;
    --raised: #1d2125;
    --recessed: #0b0d0f;

    /*
      Two hairline steps, as the handoff's tokens carry them: --line is a control's own edge and
      --line-soft is the rule between two panels. One value did both, so a filter column's edge and
      a button's border were the same weight — and the panel rules, which are ten times longer,
      read as the loudest thing on the page.
    */
    --line: #262b30;
    --line-soft: #1c2024;

    --text: #e8eaec;           /* values and names */
    --dim: #9aa2a9;            /* prose and secondary */
    --faint: #6b747c;          /* provenance, ages, meta */

    --accent: #35d29a;         /* measured */

    /*
      The accent as *text on an accent tint* — the selected facet row, which paints a 12% wash of
      the accent behind accent-coloured words. Every ratio beside a token here is measured against
      the page, and a tinted row is not the page: in light, --accent is 5.0:1 on the background it
      was tuned for and 3.9:1 on this one, which is the row a reader has just clicked. Dark clears
      it either way, so there the two are the same colour.
    */
    --accent-ink: var(--accent);
    --amber: #d8a13a;          /* declared / ageing */
    --derived: #a394f0;        /* our own classification, and nobody else's */

    /*
      Refusal, and only refusal. Not a provenance colour — nothing on this site is red because of
      what we did or did not measure. It marks one thing: a facet value the reader has excluded,
      where the handoff draws a danger tint, a danger ring and a struck-through label. A state the
      reader put the control into, never a state of the data.
    */
    --danger: #f4574c;

    /* The card's own elevation, and the one glow the accent is allowed. */
    --shadow: 0 1px 0 rgb(255 255 255 / 3%) inset, 0 4px 16px rgb(0 0 0 / 40%);
    --glow: 0 0 8px color-mix(in srgb, var(--accent) 55%, transparent);

    --sans: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;
    --mono: "Cascadia Mono", ui-monospace, "SFMono-Regular", Consolas, monospace;
    --kick: "JetBrains Mono", var(--mono);

    /*
      The same stack with the duospaced CJK faces appended, for the one place a game's own bytes are
      shown back. The order is not cosmetic and the families are not interchangeable: a connect
      screen from a Chinese, Japanese or Korean game is column art built on the assumption that a Han
      glyph is exactly twice the advance of an ASCII one, which holds for Noto Sans Mono CJK, Sarasa
      Mono and MS Gothic and does NOT hold for PingFang, Microsoft YaHei or Hiragino Sans — those are
      proportional, and falling into one turns pkuxkx's bordered banner into a ragged staircase. The
      Latin faces stay in front so an ASCII screen is unchanged.

      No webfont, deliberately. Noto Sans Mono CJK SC is some eight megabytes a weight, and
      subsetting is per-screen — a different glyph set for every game, regenerated on every crawl.
      A reader with none of these installed gets their system's default CJK face, which is legible
      and merely misaligned; that is the right end to lose at.
    */
    --mono-cjk: "Cascadia Mono", ui-monospace, "SFMono-Regular", Consolas,
        "Noto Sans Mono CJK SC", "Noto Sans Mono CJK TC", "Noto Sans Mono CJK JP",
        "Noto Sans Mono CJK KR", "Sarasa Mono SC", "Sarasa Mono J", "MS Gothic", "SimSun",
        monospace;

    /*
      Named type steps, all rem so every one of them tracks the reader's font-size preference.
      Nineteen ad-hoc pixel values collapsed to these twelve where two neighbours were close enough
      that the difference between them looked like drift rather than a decision — a handful of
      elements move up to 1px at a 16px default as a result. What did NOT collapse: the kicker's own
      readability overrides for RTL/complex scripts and CJK (still exact, now pointing at --fs-xs and
      --fs-sm because those happen to already be their values), the header bar's responsive shrink
      ladder, and the heatmap's narrow-viewport squeeze — all three are hand-tuned against measured
      constraints (string width, cell fit) and are converted to rem in place rather than folded in.
    */
    --fs-2xs: 1rem;      /* 10px @16 — kicker labels, chart-axis ticks */
    --fs-xs: 1.15rem;    /* 11.5px @16 — small captions and hints */
    --fs-sm: 1.2rem;     /* 12px @16 — secondary/meta text, the site's most common size */
    --fs-sm2: 1.3rem;    /* 13px @16 — body-small: code, disagreements, identity tables */
    --fs-base: 1.4rem;   /* 14px @16 — body */
    --fs-md: 1.5rem;     /* 15px @16 — lede paragraphs, subheads */
    --fs-lg: 1.6rem;     /* 16px @16 — find-a-game heading */
    --fs-xl: 1.7rem;     /* 17px @16 — section h2 */
    --fs-2xl: 2rem;      /* 20px @16 — h2, stat figures */
    --fs-3xl: 2.2rem;    /* 22px @16 — figure glyph */
    --fs-4xl: 2.6rem;    /* 26px @16 — h1 */
    --fs-5xl: 2.8rem;    /* 28px @16 — the narrow-viewport find-count step */
    --fs-6xl: 4rem;      /* 40px @16 — the find-count itself */
}

/*
  Both themes are first-class and the accent changes value, never hue. Dark is the default because
  the audience reads text games at night and the ANSI frame sits inside a dark page without a seam —
  but a reader whose system says light gets light, which is what the media query is for. An explicit
  data-theme wins over the system in both directions, so a toggle can be added without moving any of
  this.
*/
/*
  ── the light theme, and why none of it is white ───────────────────────────
  The page surface was #ffffff, and once the card went full-bleed that stopped being a panel and
  became the whole window. A large field of pure white glares: the halation it produces around dark
  text is worst for readers with astigmatism and is one of the things dyslexic readers most often
  ask to be turned down, and 21:1 body contrast is past the point where more helps and into the
  point where it tires. Nobody's guideline forbids it — WCAG has a floor and no ceiling — which is
  why every design system that has thought about it lands in the same place by convention instead:
  GitHub Primer's canvas.subtle #f6f8fa, IBM Carbon's #f4f4f4 background under white layers,
  Material 3's tinted surfaces, Solarized Light's cream. All of them are off-white.

  So the ramp is a soft cool grey and the near-white is reserved for the things that are supposed to
  sit *above* it — tiles, the hover plane. That also restores a distinction the light theme had lost
  entirely: --surface and --raised were both #ffffff, so a card lifted off the page by a step in the
  dark theme was lifted by nothing at all here, and its border was doing all the work.

  Text is a near-black with the ramp's own hue rather than #000, at ~15:1 — comfortably past the
  4.5:1 floor and short of the glare. Every step below was measured against the new surface, and
  --faint moved because it did not clear 4.5:1 against the old one: it carries ages and provenance
  at 12px, which is exactly the small low-contrast text the handoff asked to have checked.
*/
@media (prefers-color-scheme: light) {
    :root {
        --bg: #eef1f2;         /* behind the page; barely visible now the shell is full-bleed */
        --surface: #f4f6f7;    /* the page itself */
        --raised: #fbfcfc;     /* tiles, hover planes — above the page, never below it */
        --recessed: #e9edee;   /* the bar, the filter column */
        --line: #d3d9dc;
        --line-soft: #e2e7e9;

        --text: #1a1f23;       /* 15.3:1 — past the floor, short of the glare */
        --dim: #4d585f;        /* 6.7:1 */
        --faint: #646f76;      /* 4.8:1 — was 3.5:1 and carried 12px ages */

        --accent: #04795b;     /* same hue, dropped in value: 5.0:1 */
        --accent-ink: #036349; /* 5.3:1 on the accent's own 12% tint, where --accent is 3.9:1 */
        --amber: #7d5400;      /* 6.2:1 */
        --derived: #5240b8;    /* 7.0:1 */
        --danger: #a8261e;     /* 6.5:1 */

        --shadow: 0 1px 2px rgb(16 24 32 / 4%), 0 4px 16px rgb(16 24 32 / 5%);
    }
}

:root[data-theme="light"] {
    --bg: #eef1f2;
    --surface: #f4f6f7;
    --raised: #fbfcfc;
    --recessed: #e9edee;
    --line: #d3d9dc;
    --line-soft: #e2e7e9;

    --text: #1a1f23;
    --dim: #4d585f;
    --faint: #646f76;

    --accent: #04795b;
    --accent-ink: #036349;
    --amber: #7d5400;
    --derived: #5240b8;
    --danger: #a8261e;

    --shadow: 0 1px 2px rgb(16 24 32 / 4%), 0 4px 16px rgb(16 24 32 / 5%);
}

:root[data-theme="dark"] {
    --bg: #0f1113;
    --surface: #16191c;
    --raised: #1d2125;
    --recessed: #0b0d0f;
    --line: #262b30;
    --line-soft: #1c2024;

    --text: #e8eaec;
    --dim: #9aa2a9;
    --faint: #6b747c;

    --accent: #35d29a;

    /* Reset with the rest of them. The light media block sets this unconditionally, so a reader who
       pinned dark on a light-preferring system kept the light ink — a dark green on a dark tint,
       2.7:1, which is worse than the value it was added to fix. Every token this block redeclares
       is redeclared for that reason; this one was missed. */
    --accent-ink: var(--accent);
    --amber: #d8a13a;
    --derived: #a394f0;
    --danger: #f4574c;

    --shadow: 0 1px 0 rgb(255 255 255 / 3%) inset, 0 4px 16px rgb(0 0 0 / 40%);
}

:root[data-density="compact"] {
    --cpad: 13px;
    --row-pad: 8px;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: var(--fs-base)/1.65 var(--sans);
    text-wrap: pretty;
}

/*
  The shell: everything the handoff draws sits in one card — the bar along its top edge, the content
  under it — rather than loose on the page. It is the difference between a document and an
  instrument, and this site is the second: a directory whose bar, filters and rows are one panel.
*/
/*
  ── the shell ──────────────────────────────────────────────────────────────
  Full-bleed. The handoff draws the site as one card floating on a ground, and that is a drawing
  convention rather than a product decision — a directory is an instrument you fill the window with,
  not a document you frame. So the card's border, radius, shadow and side margins are gone and the
  page IS the surface; what the card was actually carrying — one bar along the top, bands separated
  by hairlines, every block sharing one left edge — is unchanged and is the part that mattered.

  The radial that used to sit on the body moves here, because the body is no longer visible behind
  anything. It is the one thing keeping a full-height dark page from reading as flat black.
*/
.shell {
    min-height: 100vh;
    margin: 0;
    background: var(--surface) radial-gradient(80% 90% at 78% -20%,
        color-mix(in srgb, var(--raised) 45%, var(--surface)) 0%, var(--surface) 100%) fixed;

    /*
      Clipped sideways and open downwards. `hidden` on both axes is what a card wants and it
      swallowed the nav's own menu: at the width where both groups collapse into a disclosure, the
      panel is positioned under the bar and every pixel of it was being clipped away, so the one
      control that carries the whole catalogue on a narrow window opened onto nothing. `clip` pairs
      with `visible` — `hidden` would coerce it to `auto` and hand the page a scrollbar.
    */
    overflow-x: clip;
    overflow-y: visible;
}

/*
  The content column's own padding, at the handoff's 30px/24px/26px. A page whose blocks bleed to
  the card's edges — the listing's two columns, the game page's hairline-separated bands — cancels
  it and pads each block itself, because a full-bleed rule cannot be drawn inside a padded box.
*/
main { padding: 30px var(--gutter) 40px; }
main:has(> .card-page) { padding: 0; }

/*
  One vertical rhythm for the whole content column, as a gap rather than as margins on each block.
  Margins collapse, disagree with each other and leave a dead band wherever a block renders nothing
  — which is what the absent crawler strip did to the front page.
*/
main > .card-page,
.hero { display: flex; flex-direction: column; gap: 22px; }

h1 { font-size: var(--fs-4xl); font-weight: 600; letter-spacing: -0.01em; margin: 0 0 6px; }
h2 { font-size: var(--fs-2xl); font-weight: 600; margin: calc(var(--cpad) * 1.4) 0 10px; }
a { color: inherit; }

.dim { color: var(--dim); }
.faint { color: var(--faint); }
.mono { font-family: var(--mono); font-variant-ligatures: none; }

/* Every count and every age, so the columns scan. */
.mono, .tabular { font-variant-numeric: tabular-nums; }

/*
  ── the kicker, and why its typography is locale-gated ──────────────────────
  Uppercase plus 0.16em tracking is this site's most recognisable label, and it is Latin-specific
  twice over (i18n S1). Letter-spacing severs the cursive joins in Arabic, so connected words come
  apart into unrelated shapes; Devanagari and Thai lose conjuncts and mark stacks the same way.
  text-transform: uppercase is a no-op in CJK, turns ß into SS, and gets Turkish dotted/dotless i
  wrong. The class is the semantics; :lang() decides the typography.
*/
.kicker {
    font: var(--fs-2xs)/1 var(--kick);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--faint);
}

/* Cursive and complex scripts: prominence from weight and size, never from tracking. */
:is(.kicker, .nav-label, .bar-label, .facet-name, .evidence, .kick):is(
    :lang(ar), :lang(he), :lang(fa), :lang(ur), :lang(th), :lang(hi), :lang(mr), :lang(ne)) {
    text-transform: none;
    letter-spacing: normal;
    font-weight: 600;
    font-size: var(--fs-xs);
    line-height: 1.5;
}

/* CJK: uppercase does nothing, tracking looks like a fault, and 10px is unreadable. */
:is(.kicker, .nav-label, .bar-label, .facet-name, .evidence, .kick):is(
    :lang(zh), :lang(ja), :lang(ko)) {
    text-transform: none;
    letter-spacing: 0.02em;
    font-size: var(--fs-sm);
}

/* ── site chrome ───────────────────────────────────────────────────────────
   A fixed 60px bar that never wraps. It stretches rather than centres, so a nav item is a
   full-height box and its current-page marker can land on the bar's own bottom edge.            */

header.site {
    /* One row is 60px and stays 60px; a bar that has wrapped is as tall as what it holds. */
    min-height: 60px;
    row-gap: 4px;
    flex: none;
    border-bottom: 1px solid var(--line);
    background: var(--recessed);
    padding: 0 var(--gutter);
    display: flex;
    align-items: stretch;
    justify-content: space-between;
    gap: 20px;

    /*
      A bar spanning the window is chrome, so it stays where chrome belongs. The game page runs to
      three and a half thousand pixels and the catalogue is at the top of it; scrolling back up to
      reach the nav is a cost the card layout hid by being short.
    */
    position: sticky;
    top: 0;
    z-index: 30;

    /*
      Sideways only. `overflow: hidden` is what a fixed-height bar wants — nothing may spill out of
      it — but it clipped the nav's own dropdown to nothing at the width where that dropdown IS the
      navigation. `clip` on one axis leaves `visible` legal on the other; `hidden` would coerce it.
    */
    overflow-x: clip;
    overflow-y: visible;
}

/*
  The bar is one line of chrome and the demo banner above it is a statement about the whole site, so
  the banner scrolls away and the bar does not. Where there is no banner the bar is simply at the
  top of the document.
*/
.demo-banner { position: relative; z-index: 31; }

/*
  The bar measures itself.

  Every threshold in the ladder below used to be a pixel count, and every one of them was really a
  measurement of *English*: the seven catalogue links are 430px in English and 517px in Japanese, so
  860 was the width at which the English bar stopped fitting and German, Dutch and Japanese spent the
  whole 861–1100 band with links running out of their box. 1024 is one of the four widths this design
  is reviewed at, and all three failed at it.

  `max-content` is the browser's own answer to "how wide is this text, in this language, in this
  font" — so the nav asks for exactly what its links need and `flex-wrap` gives it its own row the
  moment the bar cannot. No number to keep in step with a translation, nothing to re-tune when a
  string changes, and a language nobody has measured yet is right on arrival.

  What the reader gets in that band is a two-row bar rather than a menu, which is the trade: every
  destination stays visible and in the same order, and the row that moves is the one that was being
  clipped. Below the collapse width it is still one disclosure, because a menu is the right shape on
  a phone whatever the language.
*/
header.site { flex-wrap: wrap; }
header.site .mark { flex: 0 0 auto; }
header.site .site-nav { flex: 1 1 auto; min-width: max-content; }
header.site nav.account { flex: 0 0 auto; }
header.site .reading { flex: 0 0 auto; }

header.site .mark {
    display: flex;
    align-items: center;
    font: 700 var(--fs-xl)/1 var(--sans);
    letter-spacing: -0.01em;
    text-decoration: none;
    white-space: nowrap;
}
header.site .mark .star { color: var(--accent); text-shadow: var(--glow); }

/* ── provenance chip ───────────────────────────────────────────────────────
   Not a box. At rest it is the value, one state glyph and a relative age — no border, no fill,
   because it appears hundreds of times and a chip shape at that density is noise.               */

/* A chip holds somebody else's text, and somebody else's text can be a paragraph: Beutelland's
   MSSP DESCRIPTION is 1,421 characters, and `white-space: nowrap` turned that one field into a
   9,971-pixel page — every other column on /g dragged out behind a horizontal scroll bar, with the
   game's own writing off the right-hand edge where nothing reaches it. So a value wraps, and it
   wraps mid-word when a word is longer than the column (a URL, a run-on address) rather than
   pushing the page out again. The chip's own furniture still cannot come apart: the markup puts no
   whitespace between the value, the state glyph and the age — those gaps are margins — so the only
   break opportunities in a chip are inside the value's own words. */
.chip { overflow-wrap: anywhere; }
.chip .glyph { font-family: var(--mono); }

/* The gap between a value and the glyph that qualifies it. A margin and not a space in the markup,
   for the reason every other gap on this stylesheet is: the renderer trims whitespace between two
   adjacent elements, so the chip shipped as "3Scapes mudlib◇ 73m" and "CircleMUD◇ 3h" — the declared
   diamond welded to the last letter of the value, while the measured protocols beside it kept the
   space their markup happens to put there. One fact read as two different shapes depending on which
   column it was in. Adjacent-sibling, so the glyph gains nothing after the screen-reader word, which
   is positioned out of the flow and would otherwise leave a gap before a chip that shows no value. */
.chip .value + .glyph { margin-left: 4px; }
.chip .age { font-family: var(--mono); font-size: var(--fs-sm); color: var(--faint); margin-left: 4px; }
.chip.measured .glyph { color: var(--accent); }
.chip.declared .glyph { color: var(--amber); }
.chip.stale .age { color: var(--amber); }
.chip.unknown .glyph { color: var(--faint); }

/* ── capability matrix ─────────────────────────────────────────────────── */

/*
  A framed table, as the handoff draws it: a header row on the recessed surface, hairlines between
  the rows, and the caption as a foot band inside the same frame rather than a loose line under it.
  A caption that sits outside the border reads as a note about the section; inside it, it reads as
  what it is — the table's own summary, with the table still in view.
*/
table.matrix {
    border-collapse: collapse;
    width: 100%;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    overflow: hidden;
}
table.matrix th, table.matrix td {
    text-align: left;
    /* Its own padding rather than the row token: this table is six rows of two-word states, and a
       listing row's 13px by 20px turns each of them into a paragraph. */
    padding: 11px 16px;
    border-top: 1px solid var(--line-soft);
    white-space: nowrap;
}
table.matrix thead th {
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint);
    background: var(--recessed);
    border-top: 0;
    padding: 10px 16px;
}
table.matrix tbody tr:first-child th, table.matrix tbody tr:first-child td { border-top: 1px solid var(--line-soft); }
table.matrix tr.disagrees { background: color-mix(in srgb, var(--amber) 9%, transparent); }
/* At the foot and inside the frame, where a caption belongs on a table whose heading is directly
   above it — and stated once, here, rather than in the heading as well. */
table.matrix caption.count {
    caption-side: bottom; text-align: left; font-size: var(--fs-sm);
    padding: 10px 16px;
    background: var(--recessed);
    border-top: 1px solid var(--line-soft);
}
.state-present { color: var(--accent); }
.state-absent  { color: var(--dim); }
.state-unknown { color: var(--faint); }

/* ── activity heatmap ──────────────────────────────────────────────────────
   A real table with row and column headers, because that is the accessibility requirement rather
   than a compromise. Flat fill versus hatched outline carries the state without colour, so it
   survives greyscale and colour blindness.                                                      */

table.heat { border-collapse: separate; border-spacing: 2px; }
table.heat th { font: var(--fs-2xs)/1 var(--kick); color: var(--faint); font-weight: 400; padding: 2px 4px; }
table.heat td { width: 15px; height: 15px; padding: 0; border-radius: 3px; }
td.counted   { background: var(--accent); }
td.zero      { background: color-mix(in srgb, var(--accent) 18%, transparent); }
/* The hatch has to be visible at 12px and in greyscale, or the middle state is indistinguishable
   from the empty one — which is the worst bug this codebase can ship. Hence --dim, not --line. */
td.unmeasurable { background: repeating-linear-gradient(45deg, var(--dim) 0 1.5px, transparent 1.5px 4px);
                  outline: 1px solid var(--dim); outline-offset: -1px; }
td.gap          { background: transparent; outline: 1px dashed var(--faint); outline-offset: -1px; }

/* ── the ANSI quotation frame ──────────────────────────────────────────────
   Foreign colour is quoted, not hosted. Its own fixed black ground and its own locked 16-colour
   table in both themes, so arbitrary SGR is expected inside and nothing bleeds either way.      */

/*
  One panel holding the art, the words and the caption — rather than a black box with a kicker strip
  welded under it and the words in a separate disclosure below. The frame is the site's own recessed
  surface at the controls' radius; the art keeps its fixed black ground inside, because the SGR it
  carries is somebody else's and has to sit on the terminal it was drawn for.
*/
figure.ansi {
    margin: 0;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    background: var(--recessed);
    padding: 16px 18px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    overflow: hidden;
}
figure.ansi figcaption {
    font-family: var(--mono);
    font-size: var(--fs-xs);
    color: var(--faint);
    padding: 0;
    background: none;
    border: 0;
}

/*
  Amber means declared, unverified or ageing, and nothing else — so the archived pill is not amber.
  Archived is a lifecycle state derived from measurement, and colouring it as staleness would put
  two meanings on one hue. It is a plain outlined pill at the faint step: dimmed one step, no red.
*/
.badge {
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--faint); border: 1px solid var(--line); border-radius: 99px;
    padding: 3px 8px; white-space: nowrap;
}
.badge.archived { color: var(--faint); }

/* ── things a reader cannot see but a reader still gets ────────────────────
   Text present for a screen reader and absent from the picture. Never display:none — that removes
   it from the accessibility tree as well, which is the opposite of the point.                 */

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

.skip {
    position: absolute;
    left: -9999px;
    background: var(--surface);
    border: 1px solid var(--accent);
    padding: 8px 12px;
    z-index: 10;
}

.skip:focus { left: 8px; top: 8px; }

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

/* ── chrome ────────────────────────────────────────────────────────────── */

/*
  Every item is a full-height flex box, and nothing wraps. The bar's job is to be one line of
  chrome; when it stops fitting it degrades through the steps below rather than reflowing.
*/
header.site nav { display: flex; align-items: stretch; min-width: 0; }
header.site nav a, header.site nav span.nav-item {
    display: flex; align-items: center; white-space: nowrap;
    color: var(--dim); text-decoration: none; font-size: var(--fs-sm2);
}
header.site nav a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }

/* ── the two groups ────────────────────────────────────────────────────────
   Places to browse, and things to read about the hobby. The label is the kicker face the rest of
   the site uses for column heads, and it is aria-hidden: the group carries the same word as its
   accessible name, and a screen reader announcing "browse" as a text node and again as a label is
   the duplication this pass exists to remove.                                                   */

header.site .site-nav { display: flex; align-items: stretch; justify-content: center; gap: 14px; }
header.site .nav-group { display: flex; align-items: stretch; gap: 11px; min-width: 0; }
header.site .nav-label {
    display: flex; align-items: center; white-space: nowrap;
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint);
}

/* A hairline between the groups rather than a wider gap: the bar is short and the gap that would
   read as a separation is one that reads as a wrap. 18px, centred in the 60px bar. */
header.site .nav-rule { flex: 0 0 auto; width: 1px; margin: 21px 0; background: var(--line); }

/*
  The page you are on, marked *inside* the item's own box.

  The marker is an inset shadow and NOTHING else — no border, no padding change. Either of those
  adds a pixel to the current item's box, and because the bar is a flex line that grows to its
  tallest child, the whole row moved down one pixel the moment a marked item was on it. A bar that
  shifts when you arrive is a bar you cannot aim at, and it is the defect the handoff names by hand.
*/
header.site nav a.on { color: var(--text); box-shadow: inset 0 -2px 0 0 var(--accent); }
header.site nav a.on:hover { text-decoration: none; }

/* The far end of the bar: what to do, rather than where to go. Submit is an action and about is
   site meta, so neither is a catalogue and neither sits in a group above. */
header.site nav.account { display: flex; align-items: center; gap: 12px; }
header.site nav.account a { color: var(--text); }
header.site nav.account a.action {
    border: 1px solid var(--line); border-radius: var(--radius); padding: 6px 12px;
    font-size: var(--fs-sm2); white-space: nowrap;
}
header.site nav.account a.action:hover { border-color: var(--dim); text-decoration: none; }

/* Icon-only, so the SVG's own inline baseline gap doesn't add a sliver of extra pill height. */
header.site nav.account a.discord .reach-icon { display: block; line-height: 0; }

/* The accent button: ring and tint together, as the handoff draws it — the fill is what tells it
   apart from the bordered one beside it at a glance. */
header.site nav.account a.action.primary {
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    color: var(--accent);
}

/*
  ── the nav's degradation order (i18n S2) ──────────────────────────────────
  Width triggers the steps, never language: a German reader on a wide monitor never leaves step 0
  and an English reader in a narrow window reaches step 2. Each step loses DECORATION, never a
  destination — which is the whole point, and why the collapse at the end is a disclosure holding
  every link rather than a shorter bar.

    step 1  group labels drop; the divider keeps the grouping
    step 2  submit loses its border, becoming a plain link
    step 3  both groups collapse into one `menu` disclosure

  Budget every nav string at 1.4x the English width when reviewing a locale.
*/

/*
  Step 0 to 2 the disclosure is not a disclosure: the summary is hidden and its content forced
  visible, so the two groups sit in the bar exactly as if the <details> were not there. Guarded on
  ::details-content, because a browser without it cannot be made to show a closed <details> — there
  the summary stays and the nav is a menu button at every width, which is a worse bar and a working
  one.
*/
header.site .nav-menu { display: flex; position: relative; align-items: stretch; min-width: 0; }
header.site .nav-menu > summary {
    display: flex; align-items: center; gap: 6px; cursor: pointer;
    color: var(--dim); font-size: var(--fs-sm2); white-space: nowrap; list-style: none;
}
header.site .nav-menu > summary::-webkit-details-marker { display: none; }
header.site .nav-menu > summary:hover { color: var(--text); }
header.site .nav-menu[open] > summary { color: var(--text); }

/* The panel, as a dropdown. Above the collapse width the same element is the bar's own centre row. */
header.site .nav-menu .menu-panel {
    position: absolute; top: 100%; left: 0; z-index: 20; min-width: 200px;
    display: flex; flex-direction: column; gap: 2px;
    padding: 8px; margin-top: 1px;
    background: var(--raised); border: 1px solid var(--line);
    border-radius: var(--radius); box-shadow: var(--shadow);
}
header.site .nav-menu .menu-panel > .nav-group { flex-direction: column; align-items: stretch; gap: 2px; }
header.site .nav-menu .menu-panel a { padding: 6px 10px; border-radius: 6px; }
header.site .nav-menu .menu-panel a:hover { background: var(--surface); text-decoration: none; }

/*
  The current page, marked for a list rather than for a bar — and only while the panel IS a list.
  The inset shadow that lands on the bar's bottom edge becomes a full-width rule under one row of a
  dropdown, which reads as a text field rather than as "you are here", so in the panel the marker
  moves to the leading edge. Scoped to the collapsed width: unscoped it drew a vertical accent bar
  and a tinted box down the side of the current item in the open bar.
*/
@media (max-width: 56.25em) {
    header.site .nav-menu .menu-panel a.on {
        box-shadow: inset 2px 0 0 0 var(--accent);
        background: color-mix(in srgb, var(--accent) 10%, transparent);
        color: var(--accent);
    }
}
header.site .nav-menu .menu-panel .nav-label { padding: 6px 8px 2px; }
header.site .nav-menu .menu-panel .nav-rule { display: none; }
/*
  Submit's overflow home, and it opens at the width the overflow actually happens.

  It was shown wherever the panel was a dropdown — below 881 — while the bar keeps its own submit
  link down to 640. So between 641 and 880 the menu and the bar both offered "submit a game", which
  is one destination drawn twice in one document and the exact duplication the disclosure is built
  to avoid. Step 4 is where the bar drops it, so step 4 is where this picks it up.
*/
header.site .menu-tail { display: none; flex-direction: column; gap: 2px; }

@media (max-width: 40em) {
    header.site .menu-tail { display: flex; }
}

@supports selector(::details-content) {
    @media (min-width: 56.3125em) {
        header.site .nav-menu > summary { display: none; }

        /*
          The wrapper has to carry the bar's full height, not its content's. A nav item is a
          full-height box so that the current-page marker — an inset shadow, drawn inside the box —
          lands on the bar's own bottom edge; with the wrapper sized to its text the marker floated
          eighteen pixels above it, which is the underline hanging in mid-bar rather than sitting on
          it. `display: flex` is what makes the panel below a stretched flex item.
        */
        header.site .nav-menu::details-content {
            content-visibility: visible;
            block-size: 100%;
            display: flex;
            align-items: stretch;
        }

        /* The panel is the bar's middle row: a static flex line, not a dropdown. */
        header.site .nav-menu .menu-panel {
            position: static; min-width: 0; height: 100%;
            display: flex; flex-direction: row; align-items: stretch; gap: 14px;
            padding: 0; margin: 0; background: none; border: 0; border-radius: 0; box-shadow: none;
        }
        header.site .nav-menu .menu-panel > .nav-group { flex-direction: row; align-items: stretch; gap: 11px; }
        header.site .nav-menu .menu-panel a { padding: 0; border-radius: 0; }
        header.site .nav-menu .menu-panel a:hover { background: none; }
        header.site .nav-menu .menu-panel .nav-rule { display: block; }
        header.site .nav-menu .menu-panel .nav-label { padding: 0; }

    }

    /*
      Step 1 — the labels go and the divider stays. The grouping is the fact; the words naming the
      groups are a gloss on it, and the rule between them carries the same fact in one pixel. Losing
      both at once would leave seven links in a row in no order anybody could name, which is the
      state this bar was rebuilt out of.
    */
    @media (min-width: 56.3125em) and (max-width: 73.75em) {
        header.site .nav-label { display: none; }
        header.site .nav-menu .menu-panel { gap: 14px; }
        header.site .nav-menu .menu-panel > .nav-group { gap: 11px; }
    }
}

@media (max-width: 62.5em) {                    /* step 2 — submit keeps the word, loses the box */
    header.site nav.account a.action:not(.primary) {
        border-color: transparent; padding: 6px 0; color: var(--dim);
    }
    header.site nav.account a.action:not(.primary):hover { color: var(--text); }
}

@media (max-width: 56.25em) {                     /* step 3 — one disclosure, every destination kept */
    header.site .site-nav { justify-content: flex-start; }

    /*
      And it stops being the bar's shock absorber. Above this width the nav holds seven links and
      `flex: 1 1 auto; min-width: 0` is what lets it give room back to the two ends; below it the
      nav is one word and a chevron, and the same rule let flexbox squeeze that word to 33px — the
      German bar rendered "Men" with the chevron cut off, on the only control that reaches the other
      six pages. The ends are sized by their content and the middle was the only thing that could
      yield, so a longer word at either end came out of the one item that must never shrink.
    */
    header.site .site-nav { flex: 0 0 auto; }
}

/*
  Step 4 — the right cluster runs out too. `submit` joins the menu, which is where the markup
  already carries it; `about` and `sign in` stay, the first because it is two words and the second
  because it is the one item at that end that changes what the site will let a reader do. Still no
  destination lost.
*/
@media (max-width: 40em) {
    header.site { padding: 0 14px; gap: 12px; }
    header.site nav.account a.action:not(.primary) { display: none; }
    header.site form.theme { gap: 6px; }
}

/*
  Step 5 — below a small phone the type gives way before anything else does. Nothing is dropped and
  nothing moves; the two clusters at the ends are simply set a step smaller, which is the last
  decoration the bar has left to spend.
*/
@media (max-width: 28.75em) {
    header.site { padding: 0 8px; gap: 6px; }
    header.site .mark { font-size: 1.5rem; }
    header.site form.theme { gap: 4px; }
    header.site form.theme button { font-size: 1.2rem; }
    header.site nav.account { gap: 6px; }
    header.site nav.account a { font-size: 1.25rem; }
    header.site nav.account a.action { padding: 5px 8px; }
}

/*
  Step 6 — and the bar is measured in a language, not in English.

  Every step above was tuned against a bar reading "menu · about · submit · sign in · auto light
  dark", which is the shortest this chrome is ever going to be. The same bar in German is "Menü ·
  über uns · anmelden · auto hell dunkel" and wanted seventeen more pixels than a 430px phone has;
  Dutch was twelve pixels from the same wall. So the last step is a real one rather than a hedge,
  and it spends the only thing left that is not a destination: the theme control's three words
  become the row's tightest type, and the wordmark gives up a point with them. Nothing moves and
  nothing is dropped — the German bar and the English bar hold the same seven pages either side of
  it, which is the property the whole ladder exists to keep.
*/
/*
  Step 7 — and on the narrowest phone the bar stops being one line, for whichever language needs it.

  At 360px the German bar wants 412px: "mu*index · Menü ▾ · über uns · anmelden · auto hell dunkel".
  Everything above this step spends decoration and the ladder has none left — and the item that fell
  off the end was the theme control, clipped past `.shell`'s `overflow-x: clip` and so not merely
  cramped but gone. A reader on a 360px phone in German could not change the theme at all.

  No width triggers this and no language is named in it: the bar already wraps on content, so the
  theme control takes a second row exactly where it stops fitting. German and Dutch reach it at 360
  and English and Chinese do not, which is the ladder working rather than a rule about German. The
  one thing set here is what happens *after* a wrap — the control keeps the trailing edge it has on
  one line, so the bar does not appear to change its mind about which end it belongs to.
*/
header.site .reading { margin-inline-start: auto; }

/* ── the reading controls ─────────────────────────────────────────────────
   The three things in this bar that do not name a page: read it as text, read it in another
   language, read it against another background. They were a footer under a bar that already
   carried every link beside them, so the two that were not duplicates were the two nobody found.

   One cluster and not three siblings, so they are one thing at the trailing end rather than three
   that happen to be adjacent — and so the caret and the buttons it sits beside cannot be separated
   by anything the bar does.                                                                      */

header.site .reading { display: flex; align-items: center; gap: 10px; }

/* It gives up the auto margin that pushed it to the far end of a footer row; the cluster around it
   does that job now. Everything else — the hairline, the caret, the arrow — is the same control. */
header.site .reading form.locale { margin: 0; }

/*
  Step 5 — the two reading controls go behind a caret, and the theme does not.

  **The bar may not get wider than it was.** Every threshold in the ladder above is a pixel count
  measured in English, which the ladder says out loud, and the wrap is the escape valve those
  numbers rely on. Two controls added at the trailing end spend that valve: with the text link and
  the switcher drawn in the bar it cost 406px, and swept from 340 to 1440 it put a second row under
  German and Dutch at 1020–1220 and again at 1410, under Japanese at 1020–1200. Pushing the numbers
  until German fits lands the nav's collapse near 1220 — which takes the seven inline links off an
  English reader at 1200px, and is the exact failure the ladder's own comment was written about.

  So the cluster is sized instead. A caret and a panel cost about thirty pixels where the two
  controls cost three hundred, and the trailing cluster comes to 147px against the 151px the theme
  alone used to occupy: the bar is never wider than the one this branch started from, at any width,
  in any language. Nothing about the ladder needed changing after that, and nothing was.

  The theme stays drawn. It is the control a reader reaches for because they cannot comfortably read
  the page, and putting it one click further away is the wrong direction for exactly that reader.
  Its label is what pays for the caret beside it — `auto light dark` says what the buttons are.
*/

/* Taken out of the drawing and left in the accessibility tree: the form names itself with
   `aria-labelledby="theme-label"` and this is that label, so `display: none` would leave a screen
   reader three buttons in a group with no name. Scoped with `>` so the switcher's own label, which
   sits inside the panel where there is room for it, keeps its word. */
header.site .reading > form.theme .kick {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The caret is the bar's own chevron, at the size the nav's summary uses, so the disclosure reads as
   the same kind of control as `menu ▾` rather than as a new one. */
header.site .read-menu { display: flex; position: relative; align-items: center; min-width: 0; }
header.site .read-menu > summary {
    display: flex; align-items: center; cursor: pointer;
    padding: 4px 6px; border-radius: 6px;
    color: var(--dim); font-size: var(--fs-sm2); line-height: 1; list-style: none;
}
header.site .read-menu > summary::-webkit-details-marker { display: none; }
header.site .read-menu > summary:hover { color: var(--text); background: var(--raised); }
header.site .read-menu[open] > summary { color: var(--text); background: var(--raised); }

/* Right-anchored, because it opens at the trailing end of the bar and a panel that grew rightwards
   from there would open off the edge of the window. */
header.site .read-panel {
    position: absolute; top: 100%; right: 0; z-index: 20; min-width: 190px;
    display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
    padding: 10px; margin-top: 1px;
    background: var(--raised); border: 1px solid var(--line);
    border-radius: var(--radius); box-shadow: var(--shadow);
}
header.site .read-panel .mirror {
    align-self: stretch;
    padding: 6px 8px; border-radius: 6px;
    color: var(--dim); font-size: var(--fs-sm2); text-decoration: none; white-space: nowrap;
}
header.site .read-panel .mirror:hover { background: var(--surface); color: var(--text); }
header.site .read-panel form.locale > label { flex-direction: column; align-items: flex-start; gap: 4px; padding: 0 8px; }

/*
  Below the nav's own collapse width the caret goes and the two controls ride in the nav menu, where
  the markup already carries them. Two disclosures side by side in a bar that is four items wide is
  one more tap target than that width has room to explain, and the menu is right there.

  That width is 900 rather than the 880 it was, and the twenty pixels are the whole point: 900 is
  where the theme control's own label goes, so below it the trailing cluster measured 105px on the
  bar this branch started from and 134px with a caret added — the one band where this bar was *wider*
  than that one. Both steps happen at one width now, and the cluster is 105px there, as before.
*/
@media (max-width: 56.25em) {
    header.site .read-menu { display: none; }
}

/*
  In the panel the two sit as a labelled group like the ones above them, with the switcher on its
  own line under the text link — a select in a dropdown row would be a control inside a menu inside
  a bar, and the box needs the width it has.
*/
header.site .menu-reading { display: none; flex-direction: column; gap: 2px; }

@media (max-width: 56.25em) {
    header.site .menu-reading { display: flex; }
}
header.site .menu-reading .mirror { padding: 6px 10px; border-radius: 6px; color: var(--dim); }
header.site .menu-reading .mirror:hover { background: var(--surface); color: var(--text); }
header.site .menu-reading .nav-label { padding: 6px 8px 2px; }
header.site .menu-reading form.locale { margin: 6px 8px 2px; }
header.site .menu-reading form.locale > label { flex-direction: column; align-items: flex-start; gap: 4px; }

@media (max-width: 30em) {
    header.site .mark { font-size: 1.4rem; letter-spacing: 0; }
    header.site form.theme { gap: 4px; }
    header.site form.theme button { font-size: 1.15rem; }
    header.site nav.account a { font-size: 1.2rem; }
}

/* ── the reader's theme ────────────────────────────────────────────────────
   Buttons drawn as the links beside them, because they belong to that row and a row of three real
   buttons in a text bar reads as a toolbar bolted on. The one in force is underlined and not merely
   brighter: a state carried by colour alone is a state a greyscale display does not carry, which is
   the same rule the heatmap's hatch exists for.                                                  */

/* Centred in the bar rather than baseline-aligned to it: the bar stretches its children to 60px,
   and a baseline inside a stretched box lands wherever the box's first line happens to be — which
   put this cluster sixteen pixels above the buttons beside it. */
form.theme { display: flex; align-items: center; gap: 8px; margin: 0; }
form.theme .kick {
    font: var(--fs-2xs)/1 var(--kick);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--faint);
}
form.theme button {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--dim);
    cursor: pointer;
}
form.theme button:hover { color: var(--text); }
form.theme button[aria-pressed="true"] {
    color: var(--text);
    text-decoration: underline;
    text-underline-offset: 3px;
}

h3 { font-size: var(--fs-md); font-weight: 600; margin: var(--cpad) 0 4px; }
p { margin: 0 0 8px; }

.summary { font-size: var(--fs-md); line-height: 1.6; color: var(--text); max-width: 68ch; }
.note { max-width: 64ch; }
pre.plain { white-space: pre-wrap; font-size: var(--fs-sm2); line-height: 1.6; color: var(--dim); }

/* ── home ──────────────────────────────────────────────────────────────── */

.page-flow { display: flex; flex-direction: column; gap: 22px; }

/*
  The heading runs to whatever width it has. It carried a 24ch clamp, which broke "A directory of
  the MU* hobby" across two lines at every width including 1440 — a measure is for paragraphs, and
  this is one short line whose whole job is to be read at a glance.
*/
.hero-head { display: flex; flex-direction: column; gap: 8px; }
.hero h1 { font-size: var(--fs-4xl); line-height: 1.2; letter-spacing: -0.01em; margin: 0; }

/* The one sentence a stranger needs before they read a count. Dimmed a step so the heading above it
   stays the first thing the eye takes, and measured so it never runs past a comfortable line. */
.hero .lede { font-size: var(--fs-md); line-height: 1.6; color: var(--dim); max-width: 62ch; margin: 0; }

/*
  One field with the search glyph inside it, and the submit beside it at the field's own height.
  The two used to be separate boxes at two different sizes, which read as a form rather than as the
  one control it is.
*/
form.search { display: flex; gap: 8px; max-width: 520px; margin: 0; }
.search-field {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: center; gap: 10px;
    background: var(--recessed);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0 14px;
}
.search-field:focus-within { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }
.search-glyph { color: var(--accent); font-size: var(--fs-md); line-height: 1; }
input[type="search"], input[type="text"] {
    background: var(--recessed);
    border: 1px solid var(--line);
    color: var(--text);
    font: var(--fs-base) var(--sans);
    padding: 8px 11px;
    border-radius: var(--radius);
}

/* After the generic input rule, not before it: the field's own box is the bordered thing here and
   the input inside it is only the caret. Ordered the other way round the generic rule won on equal
   specificity and drew a second border inside the first. */
.search-field input[type="search"] {
    flex: 1; min-width: 0;
    background: none; border: 0; padding: 11px 0; border-radius: 0;
    color: var(--text); font: var(--fs-base) var(--sans);
}
.search-field input[type="search"]:focus { outline: none; }
.search-field input[type="search"]::placeholder { color: var(--faint); }

button {
    background: var(--raised);
    border: 1px solid var(--line);
    color: var(--text);
    font: var(--fs-base) var(--sans);
    padding: 8px 14px;
    border-radius: var(--radius);
    cursor: pointer;
}

/* ── the four figures ──────────────────────────────────────────────────────
   A label and a value, in a framed tile, at the size a front page's whole claim deserves. They were
   one 12px mono line reading like a caption, which is what a count of the hobby is not.        */

ul.tiles {
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px;
}
ul.tiles li {
    border: 1px solid var(--line-soft); border-radius: var(--radius-lg);
    background: var(--surface); box-shadow: var(--shadow); padding: 16px;
    display: grid; gap: 8px; align-content: start;
}
ul.tiles a { text-decoration: none; display: grid; gap: 8px; }

/*
  The value stays in the machine face. The drawing sets it in the UI sans, and the handoff's own
  token rules say the opposite two pages earlier — "mono for anything the machine said: counts,
  versions, ages … the one typographic rule carrying meaning". A count of the hobby is the purest
  case of a machine-said value on this site, so the prose wins over the component. The size is the
  drawing's.
*/
ul.tiles .kicker { color: var(--faint); }
ul.tiles .figure {
    font-size: var(--fs-5xl); font-weight: 600; letter-spacing: -0.01em; color: var(--text);
    line-height: 1;
}
ul.tiles .figure.state-present { color: var(--accent); }

/* The crawler strip, under the counts it is the provenance of. A step smaller and dimmer than they
   are, because it is about the instrument rather than about the hobby — a reader who never notices
   it has lost nothing, and one who comes looking for it knows where to look. */
/* Hairlines top and bottom, as the handoff frames it: the strip is a band of provenance under the
   figures it qualifies, and a band reads as one thing where a loose line reads as a footnote. */
p.crawler {
    margin: 0; padding: 12px 0; font-size: var(--fs-sm);
    display: flex; align-items: center; gap: 10px;
    border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);
}
p.crawler .state-present { text-shadow: var(--glow); }

.feeds {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 22px;
    margin: 0;
}

/* A column: its heading, then rows. The heading carried a 33.6px top margin from the global h2
   rule, which put each of the three columns a block lower than the rhythm the page is set on. */
.feeds .feed { display: flex; flex-direction: column; }
.feeds .feed h2 { margin: 0 0 10px; }
.feeds .feed-lit h2 { color: var(--accent); }
.feeds .feed-dark h2 { color: var(--amber); }

/* ── liveness feed cards ───────────────────────────────────────────────────
   One shape, three registers. Neutral, unlit, and — for a return — the one place the site raises
   its voice, because a game dark for two years answering is what no incumbent can tell you.    */

/* A row: name left, age right, a hairline between. Three columns of boxed cards gave a name and an
   age the furniture of a section, and framed things on this site are supposed to mean something —
   the connect screen is quoted in a frame because it is somebody else's. */

.feed-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    padding: 9px 0;
    border-bottom: 1px solid var(--line-soft);
}

/* The last row of a column closes on the column's own edge rather than on a rule to nothing. */
.feeds .feed-row:last-child { border-bottom: 0; }

.feed-row .feed-name {
    min-width: 0; font-size: var(--fs-base); color: var(--text); text-decoration: none;
}
.feed-row .feed-name:hover { text-decoration: underline; text-underline-offset: 3px; }
.feed-row .feed-state {
    flex: none; display: inline-flex; align-items: baseline; gap: 5px;
    font-size: var(--fs-sm); color: var(--faint);
}
.feed-row .feed-state .sep { color: var(--faint); }
.feed-row.unlit .feed-name { color: var(--dim); }

/* The return is the same row as the other two. The accent is on the word that says it is back and
   nowhere else — a tinted card with a glow gave one column furniture the other two did not have,
   and the eye cannot run down three lists that are drawn as different kinds of thing. */
.feed-row.returned .feed-state .state-present { color: var(--accent); }

/* ── /crawler: the status board ────────────────────────────────────────────
   `.prose`'s 62ch is a reading measure, right for the intro paragraph and wrong for everything
   under it: three feed columns, a four-column table and two lists of timestamps, squeezed into a
   500px ribbon nearly three screens long with two thirds of the window empty beside it. Only the
   intro keeps the measure now. Every band below is a grid that collapses at the same 56.25em step
   the rest of the site collapses at, so the narrow case is exactly what it was.               */

.status-page > .prose { margin-bottom: calc(var(--cpad) * 1.5); }

/* The pulse leads at the figures' own size. On the front page the same sentence is deliberately a
   step smaller and dimmer than the counts it sits under, because there it is provenance for
   somebody else's numbers; here the instrument is the subject of the page. */
.status-page .pulse {
    margin: 0 0 var(--cpad);
    display: flex; align-items: baseline; gap: 10px;
    font-size: var(--fs-md); color: var(--text);
}
.status-page .pulse .state-present { text-shadow: var(--glow); }

/* A third line the front page's four tiles don't carry: the figure is a count, the note is what the
   count is made of. */
ul.tiles .tile-note { font-size: var(--fs-sm); line-height: 1.5; }

/* The newest cycle, banded under the tiles it is the last increment of — same hairlines, same
   register as the front page's provenance strip. */
.status-page .cycle-line {
    margin: 12px 0 0; padding: 11px 0; font-size: var(--fs-sm);
    border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);
}
.status-page .cycle-line .sep { padding: 0 4px; }

.status-page h2 { margin-top: calc(var(--cpad) * 2); }
.status-page .feeds { margin-top: var(--cpad); }

/* Four facts per row, the same four in every row, which is a table. Written as sentences at 62ch
   each of them wrapped onto a second line with the game's own link stranded on it. */
table.transitions {
    width: 100%; margin: var(--cpad) 0 0; border-collapse: collapse;
    font-size: var(--fs-sm); color: var(--dim);
    /*
      Fixed, so the four widths are a decision rather than an auction between a long field name and a
      longer value. An auto table's min-content width is the sum of what its cells refuse to go below,
      and `capability.tls.declared` beside `1358670 → 1361433` refused hard enough to push the whole
      document 70px past a 430px viewport — the one thing no page here may do.
    */
    table-layout: fixed;
}
/* Root font-size is 62.5%, so a rem here is 10px: 9rem is 90, not 144. */
table.transitions th:nth-child(1), table.transitions td:nth-child(1) { width: 9rem; }
table.transitions th:nth-child(2), table.transitions td:nth-child(2) { width: 24%; }
table.transitions th:nth-child(3), table.transitions td:nth-child(3) { width: 20rem; }
table.transitions th, table.transitions td {
    text-align: left; padding: 7px 14px 7px 0; vertical-align: baseline;
    border-bottom: 1px solid var(--line-soft);
}
table.transitions thead th {
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--faint); border-bottom: 1px solid var(--line);
}
table.transitions tbody tr:last-child td { border-bottom: 0; }
table.transitions td.when { white-space: nowrap; color: var(--faint); }
table.transitions td.field { color: var(--faint); overflow-wrap: anywhere; }
/* A game named for its hostname is one unbreakable token, and a fixed column does not grow for it —
   it overprinted the field beside it at 430 until this. */
table.transitions td.game { overflow-wrap: anywhere; }
table.transitions td.game a { color: var(--text); text-decoration: none; }
table.transitions td.game a:hover { text-decoration: underline; text-underline-offset: 3px; }
/* A DESCRIPTION or a connect-screen line is long and arbitrary: it wraps inside its own cell rather
   than widening the table past the shell, which is the one thing this page may not do. */
table.transitions td.value { overflow-wrap: anywhere; }
table.transitions td.value .was { color: var(--faint); }
table.transitions td.value .arrow { padding: 0 4px; color: var(--faint); }
table.transitions td.value .is { color: var(--text); }

/* The queue: two lists of "when — what", which is one shape twice rather than two designs. */
.two-up.queue { margin-top: calc(var(--cpad) * 2); }
ul.ticks { list-style: none; margin: var(--cpad) 0 0; padding: 0; font-size: var(--fs-sm); }
ul.ticks li {
    display: flex; flex-wrap: wrap; gap: 2px 10px;
    padding: 6px 0; border-bottom: 1px solid var(--line-soft);
}
ul.ticks li:last-child { border-bottom: 0; }
ul.ticks .when { flex: none; }
ul.ticks .what { color: var(--dim); min-width: 0; overflow-wrap: anywhere; }

/*
  The language switcher, in the bar beside the text link and the theme control — the three controls
  that change how the page is read rather than which page it is. It used to be the far end of a
  footer row, which is why the pushing margin was here rather than on whatever held it.

  The label wraps the control: the switcher is rendered twice in a document now (bar and menu, one
  of them hidden), and a `for=` would have been a duplicate id in both copies.
*/
form.locale { display: flex; align-items: center; margin: 0; }
form.locale > label { display: flex; align-items: center; gap: 10px; cursor: pointer; }

/* The label is the theme control's kicker, because these two sit side by side and are the same kind
   of thing: a small named control that changes how the page is read. */
form.locale .kick {
    font: var(--fs-2xs)/1 var(--kick);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--faint);
}

/*
  One box, drawn as the search field is: the control and its submit inside a single hairline rather
  than a native dropdown and a wide button beside it. The footer is otherwise text, and two heavy
  rectangles in it read as the loudest thing on the page — which a language switcher is not.
*/
.locale-field {
    display: flex; align-items: center; gap: 2px;
    background: var(--recessed);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 0 4px 0 11px;
}
.locale-field:focus-within { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); }

/* Stripped of the platform's own chrome so it matches the site rather than the OS — a native select
   is a different shade, a different radius and a different caret on each of the three, and none of
   them is this one. */
form.locale select {
    appearance: none;
    background: none; border: 0; border-radius: 0; margin: 0;
    padding: 7px 18px 7px 0;
    color: var(--text); font: var(--fs-sm2) var(--sans);
    cursor: pointer;

    /* Sized to the option in force rather than to the longest one in the list. A select is as wide
       as its widest child by default, so "English" sat in a box cut for "Русский (CI canary)" and
       the caret ended up a hand's width from the word it belongs to. Where this is not supported the
       box is merely the old width, which is the behaviour it replaces — nothing is lost by it. */
    field-sizing: content;
}
/*
  The ring goes round the box, not round the flat control inside it. Stripping `appearance` takes the
  platform's focus ring with it, and the border tint below is a mouse affordance rather than an
  indicator a keyboard reader can rely on — 2.4.7 wants something that survives at any contrast.
*/
form.locale select:focus { outline: none; }
.locale-field:has(select:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The nav's chevron, not a new glyph. Pulled back over the select's right padding and transparent to
   the pointer, so the caret is part of the same click target rather than a dead spot beside it. */
.locale-caret {
    margin-left: -16px;
    color: var(--faint); font-size: var(--fs-xs); line-height: 1;
    pointer-events: none;
}

/* The search's own arrow. A submit is needed because this site runs no script, and this is the
   smallest shape that says "commit" in a vocabulary the reader has already met. */
.locale-go {
    flex: none; display: flex; align-items: center; justify-content: center;
    width: 2.2rem; height: 2.2rem; padding: 0;
    background: none; border: 0; border-radius: 6px;
    color: var(--faint); font-size: var(--fs-sm2); line-height: 1; cursor: pointer;
}
.locale-go:hover { color: var(--accent); background: var(--raised); }



/* ── game page ─────────────────────────────────────────────────────────── */

/* The header band: padded like every other band on a card page, with a rule under it running the
   card's full width. */
.game-head {
    display: flex; gap: 20px; flex-wrap: wrap;
    align-items: flex-start; justify-content: space-between;
    padding: 24px var(--gutter);
    border-bottom: 1px solid var(--line-soft);
}

.game-identity { display: flex; gap: 16px; align-items: flex-start; min-width: 0; flex: 1 1 340px; }
.game-head .game-title { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.game-head .game-title h1 { margin: 0; }
.game-head .claim-line { margin: 0; font-size: var(--fs-sm2); }
.game-head .plate {
    width: 6.4rem; height: 6.4rem; flex: none;
    border: 1px solid var(--line-soft); border-radius: 12px;
    background: var(--recessed); color: var(--dim);
    display: grid; place-items: center; font-size: var(--fs-3xl);
    padding: 8px; box-sizing: border-box;
}

/*
  The count and the probe that produced it, as one block. It was a number and a provenance chip on
  one line at body size, which set the fact most readers came for at the size of the metadata
  qualifying it.
*/
.game-figure {
    flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
    text-align: right;
}
.game-figure .figure-count {
    margin: 0; font-size: var(--fs-2xl); color: var(--accent);
    text-shadow: var(--glow); font-variant-numeric: tabular-nums;
}
.game-figure .figure-count .unit { font-size: var(--fs-md); }

/* A count the game states about itself rather than one we read: amber, which is what declared means
   everywhere else here, and no glow — the glow is the accent's, and the accent means measured. */
.game-figure .figure-count.declared { color: var(--amber); text-shadow: none; }
.game-figure .figure-count.unknown {
    color: var(--faint); text-shadow: none; font-size: var(--fs-md); font-variant-numeric: normal;
}
.game-figure .figure-age { margin: 0; }

/* One column, and the sections spaced by it rather than by margins that disagree. */
.game-body { display: flex; flex-direction: column; gap: 28px; padding: 28px var(--gutter) 8px; }
.game-body > section { display: flex; flex-direction: column; gap: 10px; }
.game-body > section > h2 { margin: 0; font-size: var(--fs-xl); }
/* An icon sits in the monogram's place and at its size. `contain` rather than `cover`: a logo that
   is not square is letterboxed rather than cropped, because cropping somebody's artwork to fit our
   box is a small edit to a thing they published. The plate's own background shows through where the
   image does not reach, so a transparent PNG lands on the same recessed square as the monogram. */
.game-head img.plate { object-fit: contain; padding: 4px; }

/* Wider than .hero .lede's 62ch on purpose: that one is a single-sentence tagline sized for an ideal
   reading line, this is a game's own self-description at whatever length its operator wrote it —
   often several sentences — and 62ch was wrapping it well short of the column .game-title already
   has, not for readability but because the two shared a number that only fit one of their jobs. 80ch
   is WCAG 1.4.8's own ceiling for a comfortable line, so it still bounds width on a wide viewport. */
.game-head .lede { font-size: var(--fs-md); line-height: 1.6; max-width: 80ch; margin: 0; color: var(--dim); }
.game-head .tagline { font-size: var(--fs-md); margin: 0; }

/*
  An address is a line, not a card. Each was a bordered panel the full width of the title column,
  which gave a hostname the furniture of the one framed thing on this page — somebody else's output,
  quoted.
*/
.game-head .endpoint {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px;
    margin: 0; padding-top: 4px;
    font-size: var(--fs-sm2); color: var(--text);
}
.game-head .endpoint .faint { font-size: var(--fs-sm); }
.game-head .endpoint.former .address { color: var(--dim); }

/*
  The reach row: where this game's people are, under its name.

  A row of icons and not a list of addresses, because nine URLs under an <h1> would push the page's
  content off the first screen. Each is a 32px target — the row is the one place on this page where
  a reader is aiming at something small, and 18px of glyph inside a 32px box is what makes it a
  thumb-sized hit without making the icons loud.

  The hover colour is the accent, which everywhere else on this site means measured — and these are
  declared. That is not a contradiction being papered over: the accent is also the interactive
  colour here (every link takes it), and a row of links that refused to use it would be reading the
  provenance palette as though it governed states it was never about. The fact itself is unglowed,
  and its chip below says declared.
*/
.reach { display: flex; flex-wrap: wrap; gap: 2px; margin: 2px 0 0; padding: 0; list-style: none; }
.reach a {
    display: grid; place-items: center;
    width: 32px; height: 32px; border-radius: 8px;
    color: var(--dim); text-decoration: none;
}
.reach a:hover, .reach a:focus-visible { color: var(--accent); background: var(--recessed); }
.reach .reach-icon { display: block; line-height: 0; }

/* Reduced motion is not the question here — this is a colour change and nothing moves — but the
   transition is still worth having only where the reader has not asked for less. */
@media (prefers-reduced-motion: no-preference) {
    .reach a { transition: color 120ms ease, background-color 120ms ease; }
}

.badges { margin: 4px 0 6px; }
.badge.claimed { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 34%, transparent); }

/* Archived: dimmed one step. No red, no strikethrough, no "dead". */
.game.archived .game-title h1 { color: var(--dim); }
.archive-plate {
    background: var(--recessed); border: 1px solid var(--line); border-radius: 8px;
    padding: 12px 14px; margin: var(--cpad) 0; color: var(--dim); max-width: 72ch;
}
.archive-plate strong { color: var(--text); font-weight: 500; }

/* Two columns where the content is two lists rather than one argument — /crawler's queue band.
   The game page is one column and stays that way (see the mockup QA's F1: a whole column first in
   the DOM threw focus order backwards by 800px). */
.two-up { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); gap: calc(var(--cpad) * 2); }

/*
  A grid item is min-width:auto by default, so it refuses to shrink below its widest child — and the
  heatmap is 438px wide. Without this the whole page grew past a 390px viewport and every paragraph
  on it was clipped, while .heat-wrap's own overflow-x sat there doing nothing. The only region
  allowed to scroll sideways is the ANSI frame.
*/
.two-up > *, .feeds > *, .row-main > *, .game-body > * { min-width: 0; }

.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--cpad); flex-wrap: wrap; }
.section-head .count { font-size: var(--fs-sm); margin: 0; }
.warn { color: var(--amber); }

ol.changes { list-style: none; margin: 0; padding: 0; font-size: var(--fs-sm); }
ol.changes li { padding: 4px 0; color: var(--dim); }

dl.declared { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; margin: 0; }
dl.declared dt { align-self: baseline; }
dl.declared dd { margin: 0; }

/* ── capability matrix ─────────────────────────────────────────────────── */

table.matrix .mark {
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.14em; text-transform: uppercase;
    border: 1px solid currentColor; border-radius: 99px; padding: 2px 6px; margin-left: 8px;
}
.state-declared { color: var(--amber); }
details.disagreement { margin-top: var(--cpad); font-size: var(--fs-sm2); }
details.disagreement p { max-width: 68ch; }

/* ── activity heatmap ──────────────────────────────────────────────────── */

.heat-wrap { overflow-x: auto; margin: var(--cpad) 0 8px; }
table.heat th.quiet { padding: 0; }
td.counted { background: color-mix(in srgb, var(--accent) calc(var(--fill, 1) * 100%), var(--raised)); }
td.counted.zero { background: var(--raised); outline: 1px solid var(--line); outline-offset: -1px; }

ul.legend {
    list-style: none; margin: 0 0 8px; padding: 0;
    display: flex; flex-wrap: wrap; gap: 16px; font-size: var(--fs-sm); color: var(--dim);
}
ul.legend li { display: inline-flex; align-items: center; gap: 6px; }
.swatch { width: 13px; height: 13px; border-radius: 3px; display: inline-block; flex: none; }
.swatch.counted { background: var(--accent); }
.swatch.unmeasurable {
    background: repeating-linear-gradient(45deg, var(--dim) 0 1.5px, transparent 1.5px 4px);
    outline: 1px solid var(--dim); outline-offset: -1px;
}
.swatch.gap { background: transparent; outline: 1px dashed var(--faint); outline-offset: -1px; }

/* ── presence trend ────────────────────────────────────────────────────────
   Measured players per day, over a range the reader can move. The heatmap folds every week into one
   grid, which answers "when is anyone on" and destroys "is this game growing" — this is the second
   question, and the graphic has to keep the same three states the grid does.

   A column per counted day. It was a broken line with a min–max band, which is the right picture
   for a game probed every day and an empty box for everybody else — three counted days of ninety
   came out as one two-pixel dot on a dark rectangle. The load-bearing rule survives the change and
   is the one NOT expressed here: a day nobody measured has no bar, and it has none because the
   geometry emits none rather than because anything below hides it. See TrendGeometry.           */

.trend-wrap { position: relative; margin: var(--cpad) 0 8px; }
svg.trend { width: 100%; height: 168px; display: block; overflow: visible; }
/* The plot is a surface, not a hole: ninety unmeasured days on the page's own background read as a
   fault rather than as a question nobody has answered yet. */
svg.trend .plot { fill: var(--surface); }
svg.trend .grid, svg.trend .monthline { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }
svg.trend .axis { stroke: var(--dim); stroke-width: 1; vector-effect: non-scaling-stroke; }
svg.trend .mean { fill: var(--accent); }
/* Half the accent over the plot's own surface, which is where it clears 3:1 against it in the dark
   theme. It stays under that on white, where the relief is the legend, the per-day tooltip and the
   "read as text" lines — every number in the cap is reachable without seeing it. */
svg.trend .peak { fill: color-mix(in srgb, var(--accent) 50%, var(--surface)); }

/* The line shape. The band is the same fact as the columns' cap and wears the same hue, a step
   lighter: a cap is a sliver above a bar and needs the weight, while a band is the largest area on
   the chart and at the cap's value it buries the line it is the spread of. */
svg.trend .band { fill: color-mix(in srgb, var(--accent) 24%, var(--surface)); }
svg.trend .line {
    fill: none; stroke: var(--accent); stroke-width: 2;
    stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke;
}
/* Ringed in the surface colour, and bigger than a line's usual marker: on a game measured on three
   days of ninety these dots are not an accent on the chart, they are the chart. */
svg.trend .dot {
    fill: var(--accent); stroke: var(--surface); stroke-width: 2;
    vector-effect: non-scaling-stroke;
}
/* The hatched state sits in its own gutter below the baseline. On the zero line it would read as a
   measured zero, which is §5.4's middle state collapsed into a filled one. */
svg.trend .uncounted { fill: var(--dim); opacity: 0.55; }
svg.trend .hit { fill: transparent; }

/* Month names, positioned at the same fractions the rules inside the plot are drawn at. They live
   out here because the canvas is stretched to the page width and text inside it stretches too. */
.trend-axis { position: relative; height: 15px; font-size: var(--fs-2xs); }
.trend-axis span { position: absolute; top: 0; white-space: nowrap; }

/* The value beside each line TrendGeometry.Gridlines already draws. Positioned the same way as the
   month labels above: outside the SVG, because a y-coordinate maps to a CSS pixel 1:1 (only the
   width stretches) and text inside the canvas would distort with it. A chip behind each number so a
   bar or the band passing under the left edge doesn't erase it. */
.trend-yaxis { position: absolute; top: 0; left: 2px; font-size: var(--fs-2xs); pointer-events: none; }
.trend-yaxis span {
    position: absolute; left: 0; transform: translateY(-50%);
    padding: 0 3px; background: var(--bg); white-space: nowrap;
}

.trend-scale { display: flex; justify-content: space-between; font-size: var(--fs-2xs); margin: 2px 0 8px; }

.swatch.mean { background: var(--accent); }
.swatch.peak { background: color-mix(in srgb, var(--accent) 50%, var(--surface)); }
.swatch.line { background: var(--accent); height: 3px; border-radius: 2px; }
.swatch.band {
    background: color-mix(in srgb, var(--accent) 24%, var(--surface));
    outline: 1px solid color-mix(in srgb, var(--accent) 45%, var(--surface)); outline-offset: -1px;
}
.swatch.uncounted { background: var(--dim); opacity: 0.55; height: 6px; border-radius: 2px; }

ul.weeks { list-style: none; margin: 8px 0 0; padding: 0; font-size: var(--fs-sm); color: var(--dim); }
ul.weeks li { padding: 2px 0; }

/* ── the ranking window selector ───────────────────────────────────────────
   Three links, not a control: the window is part of the address, so back works, the tab is
   shareable, and a text browser gets the same three choices.                                    */

nav.spans { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 var(--cpad); }
nav.spans .span {
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.08em; text-transform: uppercase;
    padding: 6px 10px; border: 1px solid var(--line); border-radius: 999px;
    color: var(--dim); text-decoration: none;
}
nav.spans a.span:hover { border-color: var(--dim); color: var(--text); }

/* The pressed one, which is a span rather than a link to the page you are on. Tinted as well as
   outlined, because with no hover to distinguish it the border alone was doing all the work. */
nav.spans .span.on {
    border-color: var(--accent); color: var(--accent);
    background: color-mix(in srgb, var(--accent) 10%, transparent);
}
/* The shape sits under the range and is the smaller question of the two, so it follows immediately
   rather than being spaced as a second block. */
nav.spans.shapes { margin-top: -4px; }

details.read-as-text { margin: 8px 0 var(--cpad); font-size: var(--fs-sm2); }
details.read-as-text summary { cursor: pointer; color: var(--dim); }
details.read-as-text[open] > *:not(summary) { animation: reveal 120ms ease-out; }

/* Motion budget, three of three. */
@keyframes reveal { from { opacity: 0; } to { opacity: 1; } }

ul.spells {
    list-style: none; margin: 6px 0 0; padding: 0;
    font-family: var(--mono); font-size: var(--fs-sm); color: var(--dim);
}
ul.spells li { padding: 2px 0; }

/* The grid's text alternative: seven rows rather than a hundred and sixty-eight cells. Numbers are
   tabular so the columns scan, and the caption sits under the table it describes. */
table.perday {
    width: 100%; margin: 6px 0 0; border-collapse: collapse;
    font-family: var(--mono); font-size: var(--fs-sm); color: var(--dim);
    caption-side: bottom;
}
table.perday caption { padding: 8px 0 0; color: var(--faint); text-align: left; }
table.perday th, table.perday td { padding: 4px 8px 4px 0; text-align: left; }
table.perday thead th {
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint);
    border-bottom: 1px solid var(--line); padding-bottom: 6px;
}
table.perday tbody th { color: var(--text); font-weight: 600; }
table.perday td.num { font-variant-numeric: tabular-nums; }

/* Not enough measured days to draw a week. A panel rather than a bare line, so the section reads as
   answered rather than as a graphic that failed to load. */
.sparse {
    margin: 10px 0 var(--cpad); padding: calc(var(--cpad) * 1.5) var(--cpad);
    border: 1px dashed var(--line); border-radius: 8px;
    background: var(--recessed); color: var(--dim); text-align: center;
}
.sparse p { margin: 0; }
.sparse .kicker { margin-bottom: 6px; }

/* ── availability strip ────────────────────────────────────────────────────
   Ninety bars, oldest left. Degraded is a *short* bar and not merely another colour; unreachable
   is hatched and outlined so it reads as absent rather than as an alarm; and a day before we were
   watching says so instead of being painted as the game's fault.                              */

.strip { display: flex; align-items: flex-end; gap: 2px; height: 26px; margin: 10px 0 0; }
.strip .bar { flex: 1 1 0; min-width: 2px; border-radius: 2px; }
.bar-reachable { height: 26px; background: color-mix(in srgb, var(--accent) 62%, var(--raised)); }
.bar-degraded { height: 13px; background: var(--amber); }
.bar-unreachable {
    height: 26px;
    background: repeating-linear-gradient(45deg, var(--dim) 0 1px, transparent 1px 4px);
    outline: 1px solid var(--dim); outline-offset: -1px;
}
.bar-unmeasured { height: 26px; background: transparent; outline: 1px dashed var(--line); outline-offset: -1px; }
.swatch.bar-reachable, .swatch.bar-unreachable, .swatch.bar-unmeasured { height: 13px; }
.swatch.bar-degraded { height: 7px; align-self: flex-end; }

.strip-scale { display: flex; justify-content: space-between; font-size: var(--fs-2xs); margin: 6px 0 8px; }

dl.stats { display: flex; flex-wrap: wrap; gap: 22px; margin: 0 0 var(--cpad); }
dl.stats dd { margin: 3px 0 0; font-size: var(--fs-xl); }

/* ── the ANSI quotation frame ──────────────────────────────────────────── */

/* Every row, once, in a region that scrolls — a screen twice the height of the frame is scrolled
   rather than cropped and offered again below. The cap is about 26 rows, which is what a terminal
   showed the player; past that the reader scrolls the same way they would have on connecting.
   Focusable, because a scrolling region a keyboard cannot reach is one a keyboard cannot read. */
figure.ansi .quote {
    position: relative; background: #000;
    border-radius: 6px;
    max-height: 33em; overflow: auto; overscroll-behavior: contain;
}
figure.ansi .quote:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
figure.ansi .quote pre {
    margin: 0; padding: 14px 16px;
    font: var(--fs-sm)/1.4 var(--mono-cjk); font-variant-ligatures: none;
    color: #aaaaaa; white-space: pre;
}
figure.ansi figcaption { display: flex; gap: 8px; flex-wrap: wrap; }

/*
  ── the words, in the same figure ──────────────────────────────────────────
  The prose lines are what a player actually needs — the address and the connect/create/QUIT
  instructions — and they were behind a disclosure outside the frame, which made the block a picture
  with a footnote rather than a quotation with a reading. Under a hairline, with the kicker the
  handoff names, and open: this is the one text copy of the art, and a text alternative a reader has
  to open is one most readers never see.
*/
.screen-text {
    border-top: 1px solid var(--line-soft);
    padding-top: 12px;
}
.screen-text > summary { cursor: pointer; list-style: none; }
.screen-text > summary::-webkit-details-marker { display: none; }
.screen-text > summary::after { content: " ▾"; color: var(--faint); letter-spacing: normal; }
.screen-text[open] > summary::after { content: " ▴"; }
.screen-text .kicker { color: var(--accent); }
.screen-text > summary:hover .kicker { color: var(--text); }
.screen-text > pre { margin-top: 8px; }
.screen-text pre {
    margin: 0;
    font: var(--fs-sm)/1.7 var(--mono-cjk); font-variant-ligatures: none;
    color: var(--text);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

pre.plain-screen { background: var(--recessed); padding: var(--cpad); overflow-x: auto; font-size: var(--fs-sm); }

.ansi-plate {
    border: 1px dashed var(--line); border-radius: 8px;
    padding: calc(var(--cpad) * 2) var(--cpad);
    color: var(--dim); background: var(--recessed);
}
.ansi-plate p { margin: 0; }

/* ══ listing and archive rows ═══════════════════════════════════════════════
   Two columns and four lines of type. What the game is on the left, what we
   last measured of it on the right, and the eye runs down one edge or the other
   rather than across every row.

   Density is deliberate and is the reason the row is a grid rather than a stack:
   this list is 700 games long and growing, so the name, the count and the age
   each have a column of their own and land in the same place on every row. The
   hairline is the only separator — the design's own rule is that separation
   comes from surface steps, not from rules, and a rule per row at this density
   would be the loudest thing on the page.
   ═══════════════════════════════════════════════════════════════════════════ */

ul.games, ul.archive { list-style: none; margin: 0; padding: 0; }

/*
  The row pads itself and the hairline runs the column's full width. It used to hang on a negative
  margin so a hover plane could reach past the reading measure — the results column owns its own
  padding now, so the row simply is the width it is drawn at.
*/
ul.games > li, ul.archive > li {
    border-bottom: 1px solid var(--line-soft);
    padding: var(--row-pad) var(--gutter);
    margin: 0;
}

ul.games > li.game-row:hover { background: var(--raised); }

/* The row is a touch target before it is a layout: the game's name is the link, and on a phone a
   list of five hundred of them is a list of five hundred things to hit. The handoff's geometry —
   13px by 20px over a 46px floor — is what puts a comfortable target under every name. */
ul.games > li.game-row { min-height: 46px; padding: var(--row-pad) var(--gutter); }

/*
  Five tracks and two lines: the game's face, then identity, measurement and freshness across the
  top, and the machine facts spanning beneath. The count and the age are siblings on one baseline —
  they were a nested two-row grid in the third column, which stacked the age under the count and left
  neither able to line up down the page.

  The plate is what gives a five-hundred-row list a left edge the eye can run down: before it every
  row began with a different-length name and the only way to find a game was to read them. It spans
  both lines rather than sitting on the first, so the meta line runs under the name and not under the
  square.

  The three measurement tracks are fixed lengths, not `auto`. Every row builds its own grid, so an
  `auto` track is sized by that row's content alone and nothing lines up with the row above: "219"
  and "9" and "no count" each put the count's left edge somewhere different, and a row with no trend
  at all moved it again. Measured down a seven-row listing, the count began at five different x
  positions across a 53px spread and the trend at three across 27px, which is the movement a reader
  sees when scanning the column rather than the row.

  Fixed rather than a shared track sizing (subgrid, or one grid over the whole list) because those
  size to whatever is on screen: a page whose busiest game has four digits would draw its columns
  differently from the next page, which is the same movement one scroll further away. The widths
  below hold the widest thing each column can carry — "no count" at 62px, a six-glyph trend, and
  "never" — so the geometry is a property of the listing rather than of a page of it. The plate's own
  track is `auto` and can be, because the plate is one fixed square on every row by construction.
*/
.row-main {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 6.4rem 5.2rem 4rem;
    gap: 3px 20px;
    align-items: baseline;
}

/*
  `contents` so the name and the metadata are grid items of the row itself rather than of a box
  inside column one — that is what lets the metadata span all four columns while the name occupies
  only the first.
*/
.row-main .row-text { display: contents; }
.row-main .row-head  { grid-column: 2; grid-row: 1; min-width: 0; }
.row-main .row-count { grid-column: 3; grid-row: 1; }

/* Explicit, not left to the grid's own auto-flow — an item with no placement of its own falls to a
   new implicit row once every explicit column in row 1 is taken, putting the trend on a line below
   the count it describes rather than beside it. */
.row-main .row-trend { grid-column: 4; grid-row: 1; }
.row-main .row-seen  { grid-column: 5; grid-row: 1; }

/* From the name's column, not the plate's: the meta line belongs to the text block, and starting it
   at the card edge would put a codebase and a protocol list under the square instead. */
.row-main .meta      { grid-column: 2 / -1; grid-row: 2; min-width: 0; }

/* The row's own plate. Same recessed square, same border, same monogram treatment as the game
   page's — one vocabulary at two sizes, so a reader who has seen the page recognises the row. */
.row-main .plate {
    /* Both lines, and `start` rather than the row's `baseline`: a 36px box has no baseline worth
       aligning a name to, and letting it take one dropped it half a line below the text it belongs
       beside. */
    grid-column: 1; grid-row: 1 / span 2; align-self: start;

    width: 3.6rem; height: 3.6rem; flex: none;
    border: 1px solid var(--line); border-radius: 6px;
    background: var(--recessed); color: var(--dim);
    display: grid; place-items: center;
    font-size: var(--fs-sm); letter-spacing: 0.02em;

    /* Nudged down to sit against the name's cap height rather than above it. */
    margin-top: 2px;
}

/* `contain`, never `cover`: a logo that is not square is letterboxed rather than cropped, because
   cropping somebody's artwork to fit our box is a small edit to a thing they published. */
.row-main img.plate { object-fit: contain; padding: 3px; }

.row-text { min-width: 0; }
.row-head { margin: 0; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }

ul.games .name, ul.archive .name { font-size: var(--fs-md); font-weight: 600; text-decoration: none; }
ul.games .name:hover { text-decoration: underline; text-underline-offset: 3px; }
ul.archive .name { color: var(--dim); }

/* No rule for "unclaimed": it is not on a row any more. The majority state of the catalogue said on
   every row is an administrative fact about our records, repeated once per game at a reader looking
   for somewhere to play. It stays on the game page, where it arrives with the way to change it. */

.row-text .tagline {
    margin: 2px 0 0;
    color: var(--dim);
    font-size: var(--fs-sm2);

    /* One line. A game's own description is a paragraph on its page; here it is a hint at what the
       game is, and a listing whose rows are different heights cannot be scanned at all. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

ul.games .meta, ul.archive .meta { margin: 3px 0 0; font-size: var(--fs-sm); color: var(--dim); }
ul.games .meta .protocol { white-space: nowrap; }

/* The protocols this row did not print. A count rather than a name, and the names are in its title
   for a pointer and on the game's page for everybody. */
ul.games .meta .protocol.more { color: var(--dim); }

/* A provenance chip on a listing or archive row. The chip's own age size is set for body copy, and
   these contexts are already smaller than that — so the age rides the row's size rather than growing
   larger than the text it qualifies. Everything else about the chip is unchanged, deliberately: the
   row, the archive and the game page have to be one vocabulary and not three that resemble each
   other. `.meta` is the class both lists give that line, so both are covered by naming it once. */
.meta .chip .age { font-size: inherit; }

/* ── the two measured columns ──────────────────────────────────────────────
   Tabular figures so the counts line up down the page without a table, and right-aligned so the
   units place is the edge the eye runs down.

   The count carries no unit and no provenance glyph. "on" after every number was one English word
   repeated 515 times — the column is named once, above, which is also one string per locale rather
   than five hundred — and where a number came from is a fact you weigh when choosing between two
   games rather than while scanning five hundred, so it lives on the game page and on the facet
   badges instead.                                                                                */

.row-count {
    margin: 0;
    grid-column: 2;
    text-align: right;
    white-space: nowrap;
    font-size: var(--fs-md);
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

/* Somebody is on. The one place a live count is marked as such, and the glow follows the digits
   rather than a pip beside them. */
.row-count.live { color: var(--accent); }

/* A measured zero: dimmed one step, never faint. We got in and nobody was there is a result, and
   it is not the same fact as a count we could not read. */
.row-count.empty { color: var(--dim); }

/* Not a number at all, so it is not set as one: smaller, faint, and in words. */
.row-count.unknown { color: var(--faint); font-size: var(--fs-sm2); font-variant-numeric: normal; }

/* The growth arrow and its own percentage: its own column beside the count, its own colour rather
   than the count's, so a rising game with a currently-empty "now" cell still reads as rising. */
.row-trend {
    margin: 0;
    text-align: right;
    white-space: nowrap;
    font-size: var(--fs-sm2);
    font-variant-numeric: tabular-nums;
}
.row-trend.up { color: var(--accent); }
.row-trend.down { color: var(--danger); }
.row-trend.steady { color: var(--dim); }

.row-seen {
    margin: 0;
    text-align: right;
    white-space: nowrap;
    font-size: var(--fs-sm);
    color: var(--faint);
    font-variant-numeric: tabular-nums;
}

/*
  Claimed: a ring with a tick in it and no visible word. It was a pill the width of "claimed" on the
  handful of rows that carry it — furniture for a fact a browsing reader did not come for. The name
  is on the element, where a screen reader and a tooltip both reach it.
*/
.claim-ring {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.6rem; height: 1.6rem; flex: none;
    border-radius: 99px;
    border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    color: var(--accent);
    font: var(--fs-2xs)/1 var(--mono);
}

/* Archived: dimmed one step, and that is the whole treatment. No red, no strikethrough, no "dead" —
   the entry is a library record for a periodical that ceased publication. */
ul.games > li.archived { opacity: 0.72; }

/*
  Where a sort ran out of things it could rank. Not a row and not styled as one: it is a rule with a
  sentence on it, so the reader can see that the list below it is a different kind of thing from the
  list above. The words are the carrier; the rule only says where the change happened.
*/
li.unranked-break {
    display: flex;
    gap: 10px;
    align-items: baseline;
    flex-wrap: wrap;
    border-bottom: 1px solid var(--line);
    border-top: 1px solid var(--line);
    background: var(--surface);
    color: var(--dim);
    font-size: var(--fs-sm);
    margin-top: var(--cpad);
}

li.unranked-break:hover { background: var(--surface); }

/*
  The archive row is two zones, not the listing's three — it has no count column, and its facts
  panel is a definition list rather than two cells. It shares .row-main with the listing for the
  gap and the baseline, so it names its own columns rather than inheriting a third it has nothing
  to put in.
*/
ul.archive .row-main { grid-template-columns: minmax(0, 1fr) auto; }

dl.archive-facts { margin: 0; font-size: var(--fs-sm); min-width: 200px; }
dl.archive-facts dd { margin: 0 0 6px; color: var(--dim); }

/* Below the width where a 200px facts panel and a game's metadata both fit, they stack — the panel
   was holding its minimum and clipping the name and codebase beside it. */
@media (max-width: 38.75em) {
    ul.archive .row-main { grid-template-columns: minmax(0, 1fr); }
    dl.archive-facts { min-width: 0; }
}

/* ── the listing's own header ────────────────────────────────────────────── */

/* The card's own header band: padded like the drawing, with a rule under it running the full
   width of the card rather than stopping at a content measure. */
.listing-head {
    margin: 0;
    padding: 22px var(--gutter) 18px;
    border-bottom: 1px solid var(--line-soft);
    display: flex; flex-direction: column; gap: 7px;
}
.listing-head h1 { margin: 0; }
.listing-head p { margin: 0; max-width: 62ch; font-size: var(--fs-base); }

/* ── the listing's toolbar ─────────────────────────────────────────────────
   What order this is in on the left, the control that changes it and the two column names on the
   right, over the columns they name.                                                            */

.listing-bar {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px 20px;
    flex-wrap: wrap;
    margin: 0;
    padding: 11px var(--gutter);
    border-bottom: 1px solid var(--line-soft);
    font-size: var(--fs-sm2);
    color: var(--dim);
}

.listing-bar .listing-order { margin: 0; }
.listing-bar .listing-order .order-name { color: var(--text); }
.listing-bar .listing-order .sep { margin: 0 6px; color: var(--faint); }
.listing-bar .listing-order a { color: var(--dim); }

.listing-tools { display: flex; align-items: baseline; gap: 20px; }
.listing-tools .column-head { margin: 0; }

/* ── a pressed-state toggle group ──────────────────────────────────────────
   Not links to the page you are already reading. The one in force is a <span> with aria-current, so
   there is no keystroke that lands on a control whose only effect is to re-fetch this page — the
   same fix the trend range and shape selectors already carry.                                    */

.switch { display: flex; align-items: baseline; gap: 8px; font-size: var(--fs-sm); }
.switch a { color: var(--dim); text-decoration: none; }
.switch a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.switch .on { color: var(--accent); }
/* A rule, not a wider gap: the two halves of the switch are different kinds of order — three that
   change which measurement the column shows, and two that only change the stacking. */
.switch .switch-rule {
    width: 1px; height: 13px; align-self: center; margin: 0 2px; background: var(--line);
}

/* The window under the statistic it belongs to, quieter than the switch that produced it. */
.switch.windows {
    padding: 8px var(--gutter);
    border-bottom: 1px solid var(--line-soft);
    font-size: var(--fs-sm);
    justify-content: flex-end;
}

/*
  Nothing matched. A plate rather than a paragraph, because an empty listing is a state and not an
  aside — and it says the same thing the archive section says, since a name that once worked here
  still resolves.
*/
.empty-state {
    border: 1px dashed var(--line);
    border-radius: 8px;
    background: var(--recessed);
    padding: calc(var(--cpad) * 1.5);
    margin: var(--cpad) 0;
    max-width: 60ch;
}

.empty-state .empty-head { font-size: var(--fs-md); font-weight: 600; margin: 0 0 4px; }
.empty-state .kicker { margin: 8px 0 0; }

/* ── one column below 900px; no horizontal scroll except inside the frame ── */

@media (max-width: 56.25em) {
    .feeds { grid-template-columns: minmax(0, 1fr); }

    /* The collapse this class never had, which is half of why nothing used it. */
    .two-up { grid-template-columns: minmax(0, 1fr); }

    /*
      All 168 cells at 12px rather than a scrolling grid — the whole week at a glance is the reason
      anyone opens this page on a phone, and a heatmap you have to drag is one you cannot read.
    */
    table.heat { border-spacing: 1px; }
    table.heat td { width: 12px; height: 12px; border-radius: 2px; }
    table.heat th { padding: 1px 3px; font-size: 0.9rem; }
}

/* The changes table keeps its four columns on a phone — a table stripped to blocks loses the header
   association a screen reader reads each cell against — but the two fixed columns give ground first. */
@media (max-width: 38.75em) {
    table.transitions th:nth-child(1), table.transitions td:nth-child(1) { width: 6rem; }
    table.transitions th:nth-child(2), table.transitions td:nth-child(2) { width: 30%; }
    table.transitions th:nth-child(3), table.transitions td:nth-child(3) { width: 9rem; }
    table.transitions th, table.transitions td { padding-right: 8px; }
}

/* All three motions become plain state changes, and nothing updates under the reader. */
@media (prefers-reduced-motion: reduce) {
    * { animation: none !important; transition: none !important; }
}

/*
  The demo banner. Amber, because amber means "declared, unverified" site-wide and demo data is the
  purest case of that — nothing here was observed. Deliberately at the very top, before the mark, so
  it cannot be mistaken for a footnote about one page.
*/
.demo-banner {
    margin: 0;
    padding: var(--cpad);
    background: color-mix(in srgb, var(--amber) 16%, transparent);
    border-bottom: 1px solid var(--amber);
    color: var(--text);
}

/* ── about ─────────────────────────────────────────────────────────────────
   Prose measure and nothing else. The one page whose whole job is to say what the rest of the site
   can and cannot prove is the wrong place to spend the accent on decoration, so it carries no
   colour of its own — the only tinted thing here is a contact address nobody configured, which is
   amber for the same reason every other unverified value on this site is.                       */

section.about { max-width: 68ch; }
section.about h2 { margin-top: calc(var(--cpad) * 2.5); }

dl.identity { font-size: var(--fs-sm2); margin: var(--cpad) 0; }
dl.identity dt { margin-top: 8px; }
dl.identity dd { margin: 2px 0 0; color: var(--dim); }

/*
  ── ecosystem dashboard & rankings ──

  The bars are illustrations and are styled as such: the accent means "measured" site-wide, so a
  measured share is drawn in it and a declared figure is left in plain text — an amber bar beside a
  green one would invite the two columns to be compared as if they were one measurement over one
  set, which is the exact misreading their separate denominators exist to prevent.

  No bar carries a fact of its own. Every one of them sits beside the count and the denominator it
  illustrates, and is aria-hidden, because a screen reader announcing "39.4 per cent" twice is worse
  than announcing it once and a bar cannot say "of 310 games".
*/

ul.shares { list-style: none; margin: var(--cpad) 0; padding: 0; }
ul.shares li {
    display: grid;
    grid-template-columns: minmax(0, 12rem) minmax(0, 1fr) auto;
    gap: var(--cpad);
    align-items: center;
    padding: var(--row-pad) 0;
    border-bottom: 1px solid var(--line);
}
ul.shares .share-label { color: var(--text); }
ul.shares .share-figure { color: var(--dim); white-space: nowrap; }

.share-track {
    display: block;
    height: 10px;
    background: var(--recessed);
    border-radius: 2px;
    overflow: hidden;
}
.share-track.narrow { height: 6px; margin-bottom: 5px; max-width: 14rem; }
.share-fill { display: block; height: 100%; background: var(--accent); }

/* A lineage bar is our own reading rather than a measurement, and the panel says so in words. The
   ink agrees with the words, so the two halves of the page cannot be read as one kind of fact. */
.share-fill.derived { background: var(--derived); }

/* The codebases exactly one game runs. Folded because a share of one is a name and not a share —
   live, fifty of them at 0.5% were two-thirds of the chart above — and a disclosure rather than a
   deletion because the fold's arithmetic has to be checkable against what it folded. The summary
   carries the whole sentence, so a reader who never opens it has still read the number. */
details.sole-use { margin: var(--cpad) 0; font-size: var(--fs-sm2); }
details.sole-use > summary { cursor: pointer; color: var(--dim); max-width: 68ch; }
ul.sole-use-names {
    list-style: none;
    margin: var(--cpad) 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 6px var(--cpad);
}
ul.sole-use-names li { color: var(--dim); }

/* Wide content scrolls inside its own box; the page body never scrolls sideways. */
/*
  A scroller contains its own absolutely positioned descendants — and until it did, the game page
  scrolled sideways.

  `.sr-only` is `position: absolute`, and with no positioned ancestor its containing block was the
  initial one: the text a screen reader reads out of the capability matrix was laid out at the
  matrix's own width, 463px in German, *outside* this box and outside `.shell`'s `overflow-x: clip`.
  So five spans nobody can see pushed the document 59px wider than a 360px phone, and the whole page
  scrolled to reveal nothing. Visually-hidden is not the same as out of the layout, and one
  `position: relative` is what makes the difference invisible in both directions.
*/
.table-wrap { position: relative; overflow-x: auto; margin: var(--cpad) 0; }

/* Inside a section that already spaces its children, the wrapper carries no margin of its own —
   and it keeps the table's rounded frame from being clipped square by the scroll box. */
.game-body .table-wrap { margin: 0; border-radius: var(--radius); }

table.protocols, table.ranking { border-collapse: collapse; width: 100%; }
table.protocols th, table.protocols td,
table.ranking th, table.ranking td {
    text-align: left;
    vertical-align: top;
    padding: var(--row-pad) var(--cpad) var(--row-pad) 0;
    border-bottom: 1px solid var(--line);
}
table.protocols thead th, table.ranking thead th {
    font: var(--fs-2xs)/1.4 var(--kick);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--faint);
    font-weight: 400;
}
table.protocols tbody th { font-weight: 400; white-space: nowrap; }
table.protocols td.declared { color: var(--amber); }
table.ranking td.place, table.ranking th.place { width: 3rem; color: var(--faint); }
table.ranking tbody th { font-weight: 400; }

@media (max-width: 56.25em) {
    /* The label stops competing with the bar for a width neither of them has. */
    ul.shares li { grid-template-columns: minmax(0, 1fr); gap: 6px; }
}

/* ══ faceted search ═════════════════════════════════════════════════════════
   The panel on /games. It is a plain GET form with no script, so everything
   here dresses controls the browser already knows how to operate — no rule
   below is load-bearing for the filter working, and every one of them survives
   being switched off.

   It reads as one card with three registers stacked in it: the bar a reader
   touches on every visit, the facets they touch on some, and a key they read
   once. That shape is the point. The panel used to be a column of full-width
   label-and-select rows with a paragraph of prose under it — a database form,
   and one that explained itself at more length than it took to operate.

   Two things here are deliberate rather than decorative. The evidence chip
   beside each facet's name takes the site's own accent/amber split for measured
   versus declared, because a reader has to be able to see which half of the
   panel is evidence — and it says the word too, since colour is not a fact and
   the glyph is aria-hidden. And a count is never hidden at any width: a facet
   whose numbers disappear has stopped saying what a click will produce, which
   is the only reason they are there.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
  No box. It was a panel on a page — border, radius, its own surface — and inside the card it became
  a box within a box, fighting the hairline that already separates it from the rows. The column's
  own edge is the separation; the form is just its contents.
*/
form.facet-form { border: 0; background: none; padding: 0; margin: 0; }

/* ── the bar ─────────────────────────────────────────────────────────────── */

/*
  The archive's own search box is the same control with a legend on it, so it is styled here rather
  than growing a second look for one question asked on two pages.
*/
/* ── the panel, as one disclosure below 900px ──────────────────────────────
   Above that width the summary is hidden and the content is forced open, so the same markup is a
   column on a laptop and a disclosure on a phone with no second rendering and no script.

   Guarded on ::details-content: where a browser does not have it, forcing a closed <details> open
   from CSS is not possible, so the summary stays visible at every width — a "filters" disclosure at
   the top of the column rather than an unreachable panel behind a hidden control.              */

.facet-collapse { min-width: 0; }
.facet-collapse > summary {
    display: flex; align-items: center; gap: 8px;
    cursor: pointer; list-style: none;
    padding: 9px 8px; margin-bottom: 4px;
    border: 1px solid var(--line); border-radius: var(--radius);
    font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--dim);
}
.facet-collapse > summary::-webkit-details-marker { display: none; }
.facet-collapse > summary:hover { color: var(--text); border-color: var(--dim); }
.facet-collapse > summary::after {
    content: "▾"; margin-left: auto; font-size: var(--fs-xs); letter-spacing: normal;
}
.facet-collapse[open] > summary::after { content: "▴"; }
.facet-collapse .summary-count {
    font-family: var(--mono); font-size: var(--fs-xs); letter-spacing: normal; color: var(--accent);
}
.facet-body { display: flex; flex-direction: column; gap: 24px; }

@supports selector(::details-content) {
    @media (min-width: 56.3125em) {
        .facet-collapse > summary { display: none; }
        .facet-collapse::details-content { content-visibility: visible; block-size: auto; }
    }
}

/* One field, and nothing else — the sort and the two inclusion switches that used to share this row
   are a toolbar switch and two facet rows now. */
.filter-bar { display: block; margin: 0; }
.filter-bar .search-field { padding: 0 8px 0 12px; }
.filter-bar .search-field input[type="search"] { padding: 7px 0; font-size: var(--fs-sm2); }

/* The submit, inside the field. A 268px column has no room for a second box on that row, and a
   submit that exists only as the return key is one a pointer-only reader cannot press. */
.search-go {
    flex: none; display: flex; align-items: center; justify-content: center;
    width: 2.2rem; height: 2.2rem; padding: 0;
    background: none; border: 0; border-radius: 6px;
    color: var(--faint); font-size: var(--fs-sm2); line-height: 1; cursor: pointer;
}
.search-go:hover { color: var(--text); background: var(--raised); }

fieldset.facets {
    display: flex;
    gap: 10px;
    align-items: center;
    flex-wrap: wrap;
}

fieldset.facets {
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface);
    padding: var(--cpad);
    margin: 0 0 var(--cpad);
}

fieldset.facets legend {
    font: var(--fs-2xs)/1 var(--kick);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--faint);
}

.filter-bar input[type="search"] { min-width: 0; width: 100%; }
fieldset.facets input[type="search"] { flex: 1 1 16rem; min-width: 0; }

.bar-field { display: inline-flex; align-items: center; gap: 6px; }
.bar-label { font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }

.filter-bar button { flex: none; }

/* ── one facet ───────────────────────────────────────────────────────────── */

/*
  auto-fit at a narrow floor, so eight facets pack four to a row on a laptop, two on a tablet and one
  on a phone without a breakpoint being written for any of them.
*/
/* ── the listing's two columns ─────────────────────────────────────────────
   What you are asking on the left, what came back on the right. `stretch` so the filter column's
   own edge runs the full height of the card: aligned to its content it stops under a short panel
   and leaves a well beside the rows.                                                            */

.listing-grid {
    display: grid;
    grid-template-columns: minmax(0, 268px) minmax(0, 1fr);
    align-items: stretch;
    gap: 0;
    margin: 0;
}

/*
  Placed explicitly, and that is load-bearing: the panel renders TWO siblings into this grid — the
  form, and the row of active-filter chips that sits outside it because a chip is a link and not a
  control. Left to auto-placement the chips took the second column and the listing dropped to the
  first, under a filter column a thousand pixels tall — so applying a filter looked like a page that
  had lost its results.
*/
/*
  The filter column is a surface a step below the card, so it reads as a column even where its rows
  run out — the border alone left it as an unmarked area of the same fill as the results beside it.
*/
.listing-grid > .facet-form {
    grid-column: 1;
    grid-row: 1 / span 99;
    background: var(--recessed);
    border-right: 1px solid var(--line-soft);

    /* Less 8px than the gutter, because every row inside pads itself by that — which is what puts
       a facet label on exactly the same left edge as the wordmark above it. */
    padding: 20px max(6px, calc(var(--gutter) - 8px));
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.listing-grid > * { grid-column: 2; min-width: 0; }
.listing-side { min-width: 0; }
.listing-grid > .active-filters { padding: 12px var(--gutter) 0; margin: 0; }

/*
  ── the filter column, narrow ──────────────────────────────────────────────
  A disclosure above the listing rather than eleven hundred pixels of open panel to scroll past
  before the first game. Closed by default and open whenever the reader has actually asked
  something, because a filter in force that nothing shows is the defect this control replaces.
*/
@media (max-width: 56.25em) {
    .listing-grid { grid-template-columns: minmax(0, 1fr); }
    .listing-grid > * { grid-column: 1; }
    .listing-grid > .facet-form {
        grid-column: 1; grid-row: auto;
        border-right: 0;
        border-bottom: 1px solid var(--line-soft);
    }
    .listing-grid > .active-filters { padding: 12px var(--gutter) 0; }

    /* Two columns of groups where there is room for them, so the opened panel is a band rather
       than a column a reader has to scroll the length of. */
    .facet-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 24px; }
}

/* In a 268px column the facets stack, one group per row. Twenty-four pixels between groups, which
   is the panel's own rhythm and twice the gap between a group's header and its rows. */
.facet-grid {
    display: flex;
    flex-direction: column;
    gap: 24px;
    margin: 0;
}

/* A facet is a heading, a note and a list of rows. */
.facet {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 9px;
    border: 0;
    padding: 0;
    margin: 0;
}

/*
  The header block sits at the rows' own left padding, so the group name, the note and every row
  label share one left edge — which is the whole reason the rows are padded rather than the panel.
*/
.facet > label, .facet > legend {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 0;
    padding: 0 8px;
    width: 100%;
    box-sizing: border-box;
}

.facet .facet-head {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
}

.facet .facet-name {
    font: var(--fs-2xs)/1.4 var(--kick);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--dim);
}

/*
  The one sentence a group needs at the moment somebody uses it, under its own heading rather than
  once at the foot of the panel — the three groups say three different things and the line that
  used to serve all of them contradicted the first.
*/
.facet-note {
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.5;
    color: var(--faint);
}

select {
    background: var(--recessed);
    border: 1px solid var(--line);
    color: var(--text);
    font: var(--fs-sm2) var(--sans);
    padding: 5px 7px;
    border-radius: 6px;
}

.facet select { width: 100%; max-width: 100%; }

/* ── the three-state facet row ─────────────────────────────────────────────
   Off, only, anything but — one row per value, and the state legible without opening anything. The
   two radios are hidden and the glyph is drawn, but they are real radios: the browser's own group
   behaviour is what makes this work with no script, and every state is a word in the accessible
   name as well as a shape and a tint.                                                          */

.facet .rows { display: grid; gap: 2px; margin: 0; }

/*
  The row's geometry, and it is a grid rather than a flex run for one reason: every column has to
  land in the same place on every row of every group. As a flex run the widest label in a group
  pushed that group's count and exclude columns ten pixels right of every other group's — and past
  the panel's own edge, which is what the audit measured as a 253px row inside a 243px box.

    15px   the state glyph
    1fr    the value, which is the one cell allowed to give up width
    auto   the count, right-aligned against the 1fr's slack so every count shares an edge
    18px   the exclusion, where a "remove" belongs
*/
.facet-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 18px;
    align-items: center;
    gap: 9px;
    min-height: 30px;
    padding: 0 8px;
    border-radius: 9px;
    font-size: var(--fs-sm2);
    color: var(--dim);
    border: 1px solid transparent;
}

/* One ordered scale: no exclusion, so no column for one. */
.facet-row.single { grid-template-columns: minmax(0, 1fr); }

.facet-row .pick {
    display: grid;
    grid-template-columns: 15px minmax(0, 1fr) auto;
    align-items: center;
    gap: 9px;
    min-width: 0;
    cursor: pointer;
}
.facet-row .tick { text-align: center; color: var(--faint); font-family: var(--mono); font-size: var(--fs-sm); }
.facet-row .row-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/*
  `--dim` rather than `--faint`, because the panel is not the page.

  `--faint` was raised to 4.8:1 once already, measured against the page background — and the facet
  panel is a more recessed surface than that, where the same token falls to 4.09:1 in dark and
  4.37:1 in light. Both are under the 4.5:1 this site holds itself to, and both are carried by a
  12px number, which is the smallest text in the panel and the one a reader most needs to read.
  Raising the token instead would have moved every age and provenance line on the site to fix a
  contrast the page itself does not have.
*/
.facet-row .count {
    font-family: var(--mono); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; color: var(--dim);
}

/* The exclusion affordance, at the far edge where a "remove" belongs. Quiet until it is the state
   the row is in. */
.facet-row .drop {
    display: block; text-align: center; cursor: pointer;
    color: var(--faint); font-family: var(--mono); font-size: var(--fs-sm);
}
.facet-row .drop:hover { color: var(--text); }
/* Links, so a click applies the filter rather than arming a control that waits for a button. */
.facet-row a { text-decoration: none; color: inherit; border-radius: var(--radius); }

.facet-row:hover { background: var(--raised); }

/*
  A rung of a scale that this filter leaves empty. It stays — a bounded facet is four named
  thresholds a reader picks between, and deleting the empty ones meant narrowing the codebase
  silently removed two of activity's four rows and shifted the panel under the reader's pointer. It
  is dimmed rather than disabled, because 0 is an answer: "no Evennia game was active this week" is
  a fact about the catalogue, and clicking it lands on the listing's own empty state.
*/
.facet-row.empty { opacity: 0.55; }
.facet-row.empty:hover { opacity: 1; }

/* Included: the accent as a tint and a ring, and the count goes accent with the label — the number
   is the part of the row that says what the filter will return, so it is the part that has to look
   chosen. */
.facet-row.on {
    color: var(--accent-ink);
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.facet-row.on .tick, .facet-row.on .count { color: var(--accent-ink); }
.facet-row.on:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }

/*
  Excluded: the danger tint, ring and strikethrough the handoff draws.

  Red is not a provenance colour here and never becomes one — it marks a state the reader put the
  control into, not a state of the data. That is why nothing else on the site is red: the accent
  means measured, amber means declared, and this means refused.
*/
.facet-row.excluded {
    color: var(--danger);
    background: color-mix(in srgb, var(--danger) 10%, transparent);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}
.facet-row.excluded .tick, .facet-row.excluded .count { color: var(--danger); }
.facet-row.excluded .row-name {
    text-decoration: line-through;
    text-decoration-color: color-mix(in srgb, var(--danger) 50%, transparent);
}
.facet-row.excluded .drop { color: var(--danger); }
.facet-row.excluded:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); }

/* The radios are hidden, so the focus ring belongs to the row that holds them. */
.facet-row a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The tail. A summary is a control, so it gets the row's own height and hit area. Ordered below the
   revealed rows (order: 2 against the rows' order: 1) rather than above them — order has no effect
   while closed, since a closed <details> renders nothing but the summary regardless, but once open
   the control that just revealed a run of rows sits right after them, where a reader's eye already
   is, not still pinned above where it was before they clicked. The label swaps for "show fewer" the
   same way the arrow above flips — an attribute selector on [open], no script. */
.facet .facet-tail { display: flex; flex-direction: column; margin-top: 2px; }
.facet .facet-tail summary {
    order: 2;
    cursor: pointer; padding: 4px 8px; font-size: var(--fs-sm); color: var(--faint); border-radius: 9px;
}
.facet .facet-tail summary:hover { color: var(--text); background: var(--raised); }
.facet .facet-tail .rows { order: 1; }
.facet .facet-tail .tail-fewer { display: none; }
.facet .facet-tail[open] .tail-more { display: none; }
.facet .facet-tail[open] .tail-fewer { display: inline; }

/* Said once, under the groups it applies to. */
.facet-hint { margin: 12px 0 0; font-size: var(--fs-xs); color: var(--dim); }

/* ── the presence facet ──────────────────────────────────────────────────── */

/*
  Real checkboxes in pill-shaped labels. The box stays visible rather than being replaced by a tinted
  chip: the tick is what a screen reader reads and what the keyboard operates, and a chip that
  carried the state in a background alone would carry it in colour alone.
*/
fieldset.facet.presence {
    border: 0;
    padding: 0;
    margin: 0;
    min-width: 0;
}

fieldset.facet.presence .checks {
    display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; padding: 0 8px;
}
fieldset.facet.presence .check { max-width: 100%; }
fieldset.facet.presence .check .check-name { overflow-wrap: anywhere; min-width: 0; }

fieldset.facet.presence .check {
    display: inline-flex;
    gap: 6px;
    align-items: center;
    font-size: var(--fs-sm);
    border: 1px solid var(--line);
    border-radius: 99px;
    padding: 3px 10px 3px 7px;
    background: var(--recessed);
    cursor: pointer;
}

fieldset.facet.presence .check:hover { border-color: var(--dim); }
fieldset.facet.presence .check:has(:checked) { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
fieldset.facet.presence .check .count { color: var(--faint); font-variant-numeric: tabular-nums; }


/* Measured, declared, and the one thing we concluded ourselves — three registers, never the only
   carrier of the difference, because the chip spells each out in words as well and colour is not a
   fact. Derived is the odd hue on purpose: it is the only ink on the site that marks something no
   game said and no socket showed, so it may not be mistaken for either at a glance. */
.evidence {
    font: var(--fs-2xs)/1.4 var(--kick);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
}

.evidence.measured { color: var(--accent); }
.evidence.declared { color: var(--amber); }
.evidence.derived  { color: var(--derived); }

/* ── the key ─────────────────────────────────────────────────────────────── */

/*
  What the panel used to say in a paragraph. Three short statements on one line, and one disclosure
  holding the readings a reader would otherwise have to guess at. Deliberately not a tooltip and not
  a title attribute: this is the difference the whole site exists to publish, and it stays in the
  document whether or not a pointer ever hovers anything.
*/
/*
  One line and one disclosure. The three-row provenance legend that used to stand here is behind the
  disclosure now: every group wears its own evidence badge, which is where the word is actually
  needed — before a filter is applied rather than after the panel has been read to the bottom. It
  also wrapped its descriptions right-aligned, which is what a flex row does to a text node it did
  not expect to break.
*/
.facet-key {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0 8px;
    padding-top: 14px;
    border-top: 1px solid var(--line-soft);
    font-size: var(--fs-sm);
    color: var(--faint);
    text-align: left;
}

.facet-key summary { cursor: pointer; color: var(--dim); font-size: var(--fs-xs); }
.facet-key .facet-more ul { margin: 8px 0 0; padding-left: 1.2em; }
.facet-key .facet-more li { margin-bottom: 6px; color: var(--dim); line-height: 1.5; }

/* ── what the query is currently asking for ──────────────────────────────── */

/*
  One chip per selection, each a link that removes itself. This is the only place the third state is
  legible at rest: a <select> sitting on an option inside its "anything but" optgroup looks like any
  other select until it is opened, and "codebase · anything but Evennia" does not.
*/
.active-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 8px;
    align-items: center;
    margin: 0 0 var(--cpad);
}

.filter-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line));
    border-radius: 99px;
    padding: 3px 9px;
    font-size: var(--fs-sm);
    text-decoration: none;
    background: var(--recessed);
}

.filter-chip:hover { border-color: var(--accent); background: var(--raised); }
.filter-chip .chip-facet { font: var(--fs-2xs)/1.4 var(--kick); letter-spacing: 0.12em; text-transform: uppercase; color: var(--faint); }
.filter-chip .chip-value { color: var(--text); }
.filter-chip .chip-x { color: var(--faint); font-size: var(--fs-2xs); }
.filter-chip:hover .chip-x { color: var(--text); }

.active-filters .clear-all { font-size: var(--fs-sm); color: var(--dim); }

/* A filter we could not read is refused out loud rather than dropped, so it needs somewhere loud
   to be said. Amber, like every other "we are not showing you a measurement" state here. */
.problem {
    padding: var(--cpad);
    border-left: 3px solid var(--amber);
    background: color-mix(in srgb, var(--amber) 10%, transparent);
}

/* ── the panel and the rows, narrow ──────────────────────────────────────── */

/*
  Narrow rows keep all three columns and all three stay aligned. They used to collapse into a flex
  run under the name, which put the age beside the count on some rows and under it on others — the
  same list drawn two ways down one screen. The name gives up width first, which is the one cell
  that can spare it, and the two right-hand columns hold their edge.
*/
@media (max-width: 38.75em) {
    .row-main { gap: 3px 12px; }
    .row-count { font-size: var(--fs-base); }
    .row-count.unknown { font-size: var(--fs-sm); }
}
/* ── reference and orientation pages ───────────────────────────────────────
   The hand-written section (spec §9). Two things it has to communicate that no
   other page does.

   First, that the prose is ours and the figures are not. A measured figure sits
   in its own panel with the site's accent on it — the accent means "measured"
   everywhere on this site and is spent nowhere else — while the prose that
   surrounds it is styled as body copy and carries no accent at all. A reader who
   knows nothing about how this site works can still see that the number and the
   paragraph are different kinds of thing.

   Second, that a client capability table is not a game's capability matrix. It
   reuses table.matrix so the two read alike structurally, and deliberately does
   NOT reuse the disagreement tint: there is only one column of claims here, so
   there is nothing to disagree with, and an amber row would imply a conflict
   that cannot exist. */

article.reference { max-width: 72ch; }
article.reference header { margin-bottom: calc(var(--cpad) * 2); }
article.reference header .lede { font-size: var(--fs-md); margin: 4px 0 8px; }

.reference-index { list-style: none; margin: 0; padding: 0; }
.reference-index li { padding: var(--row-pad) 0; border-bottom: 1px solid var(--line); }
.reference-index .name { font-weight: 600; text-decoration: none; display: inline-block; margin-right: 8px; }
.reference-index .dim { font-size: var(--fs-sm2); }

/* The measured half of a codebase or protocol page, boxed away from the prose. */
.measured-panel {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 4px;
    padding: var(--cpad);
    margin: 0 0 calc(var(--cpad) * 2);
}
.measured-panel h2 { margin-top: 0; }
.measured-panel .figure { font-size: var(--fs-2xl); margin: 0 0 6px; }
.measured-panel .figure a { text-decoration: none; }
.measured-panel .dim { font-size: var(--fs-sm2); margin: 0 0 6px; max-width: 62ch; }
.measured-panel table.matrix { margin-top: var(--cpad); }

/* The accessibility row is first and stays visually first, because it is the row
   this section exists to publish and the one nobody else does. A left rule, not
   a fill: a filled row would read as the warning state the game pages use. */
table.client-matrix tr.accessibility th { border-left: 2px solid var(--accent); padding-left: 8px; }
table.client-matrix td a { color: var(--faint); }

/* Prose. Measure capped at 62ch for the same reason the plain surface stops at
   eighty columns — a line a reader loses their place in is a line they re-read. */
.prose { max-width: 62ch; }
.prose h2 { font-size: var(--fs-xl); }
.prose h3 { font-size: var(--fs-md); font-weight: 600; margin: calc(var(--cpad) * 1.5) 0 6px; }
.prose p, .prose li { color: var(--dim); }
.prose strong { color: var(--text); font-weight: 600; }
.prose code { font-family: var(--mono); font-size: var(--fs-sm2); color: var(--text); }
.prose pre {
    background: var(--recessed);
    border: 1px solid var(--line);
    border-radius: 3px;
    padding: var(--cpad);
    overflow-x: auto;
}
.prose pre code { font-size: var(--fs-sm2); }
.prose ul, .prose ol { padding-left: 1.4em; }
.prose li { margin-bottom: 4px; }

/* ── submit a game ─────────────────────────────────────────────────────────
   Two boxes and a button, laid out as a stack rather than a row: the host is
   long and the port is three characters, and a flex row makes that pairing
   read as one field somebody has been asked to split. The hint sits under the
   box it belongs to, where the mistake it prevents is about to be made. */
form.submit-address { display: grid; gap: 6px; max-width: 44ch; margin: var(--cpad) 0; }
form.submit-address label { font: var(--fs-2xs)/1 var(--kick); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }
form.submit-address label + input { margin-bottom: 2px; }
form.submit-address input[type="text"] { width: 100%; }
form.submit-address .faint { font-size: var(--fs-sm); margin-bottom: 8px; }
form.submit-address button { justify-self: start; margin-top: 4px; }

/* The answer. A left rule and nothing else — no colour carries the difference
   between "we took it" and "we will not dial that", because the sentence does,
   and a red panel would read as an error the submitter had made rather than a
   policy of ours. */
.submission-answer { border-left: 2px solid var(--line); padding-left: var(--cpad); }
.submission-answer h2 { margin-top: 0; }

/* ── the owner dashboard ───────────────────────────────────────────────────
   The one place on this site with a writable field, and it is styled to look
   like what it is. No accent anywhere: the accent means "measured" everywhere
   else here, and an owner's answer is declared — putting it on the one form
   that produces declared values would spend the site's only provenance colour
   on the opposite of what it signals.

   A claimed game is a details block rather than a row, because an operator
   holds a handful of games and five open forms is a page nobody reads. It is
   details/summary rather than script for the same reason the rest of the write
   path is a plain POST: §8.2 draws the JavaScript boundary around the WebAuthn
   ceremony and nowhere else. */

details.claim { border-bottom: 1px solid var(--line); padding: var(--row-pad) 0; }
details.claim > summary { cursor: pointer; }
details.claim > summary .faint { margin-left: 8px; font-size: var(--fs-sm2); }

.owner-panel { margin: var(--cpad) 0 calc(var(--cpad) * 2); max-width: 62ch; }
.owner-panel h3 { margin-top: var(--cpad); }
.owner-panel p { color: var(--dim); }

form.enrichment label { display: block; margin-bottom: var(--cpad); }
form.enrichment label .kicker { display: block; margin-bottom: 4px; }
form.enrichment input[type="text"] { display: block; width: 100%; }
form.enrichment label .faint { display: block; font-size: var(--fs-sm); margin-top: 4px; }

/* Saved and refused both say so on the page an owner is already looking at: a
   write that reported nothing would teach them the site is broken (§8.5). */
p.verified strong { color: var(--text); }
p.refused { border-left: 2px solid var(--amber); padding-left: 10px; }

/* ── the MSSP scorecard ────────────────────────────────────────────────────
   §8.5's linter. No colour coding by severity: a red row would read as a
   fault, and most of what this page says is that a value is unlisted rather
   than wrong. The kicker carries the kind and the prose carries the reason. */

dl.lint dt { font-size: var(--fs-sm2); margin-top: var(--cpad); }
dl.lint dd { margin: 2px 0 0; color: var(--dim); }
dl.lint dd .kicker { display: block; margin-bottom: 2px; }

/* ── owner-published outputs ───────────────────────────────────────────────
   §8.5's badge snippet, shown where the person who pastes it already is. The
   pre wraps rather than scrolls: a line somebody has to copy should be visible
   in one piece. */

details.publish { margin-top: 6px; }
details.publish > summary { cursor: pointer; font-size: var(--fs-sm); }
details.publish pre {
    background: var(--recessed);
    border: 1px solid var(--line);
    border-radius: 3px;
    padding: 8px 10px;
    font-size: var(--fs-sm);
    white-space: pre-wrap;
    word-break: break-all;
}

/* ── ownership: co-owners, the audit log, and giving up a claim ────────────
   §8.5's several owners and §8.4's explicit revocation. The history is a
   details block because it is the sort of thing somebody reads once, when
   something has happened that they want explained.

   The resign form is deliberately unstyled as a danger: no red, no warning
   triangle. Giving up a claim is recoverable — publish a fresh token — and
   dressing it as destruction would overstate what it does. The typed
   confirmation is the guard, not the colour. */

details.history, details.resign { margin-top: 6px; }
details.history > summary, details.resign > summary { cursor: pointer; font-size: var(--fs-sm); }

ol.events { list-style: none; margin: 6px 0 0; padding: 0; font-size: var(--fs-sm); }
ol.events li { padding: 2px 0; color: var(--dim); }
ol.events .faint { margin-right: 8px; }

details.resign form { margin-top: 6px; }
details.resign input[type="text"] { display: block; margin: 6px 0; max-width: 16ch; }

/* ── find a game ───────────────────────────────────────────────────────────
   The questions on the left, the count on the right, and the count still on
   screen when the last question is answered — which is the whole premise of
   this page. Until this block existed the page had no rule of its own at all:
   it was the one surface left on user-agent form styling, drawn full-bleed
   with 2px groove borders and 5px of padding.                              */

.find-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 310px);
    align-items: stretch;
    gap: 0;
    margin: 0;

    /*
      `clip`, never `hidden`. An ancestor with overflow: hidden is a scroll
      container, and a sticky element can only travel inside its own — so the
      panel would pin for a few hundred pixels and then scroll away for the
      rest, leaving the count invisible while questions four to six were
      answered. That is the exact failure this page was redrawn to fix, and
      `clip` clips without establishing the container.
    */
    overflow: clip;
}

.find-questions { min-width: 0; grid-column: 1; }

/* ── one question ─────────────────────────────────────────────────────────
   24 above the heading, 14 under it, 24 to the next rule. Symmetric outside,
   tighter inside, so the heading binds to the options it labels and the
   questions separate from each other. The numbers are on the elements
   themselves rather than on a container: a <legend> renders on the border
   edge and outside the padding box, which is why the drawing's own first
   attempt produced 13px above the heading and 2px below it.                */

.find-q {
    padding: 24px var(--gutter);
    border-top: 1px solid var(--line-soft);
}

.find-q-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin: 0 0 14px;
}

/* The question is a heading, so a screen reader can jump between the six and
   the page has an outline. h2's own rhythm belongs to prose sections and
   would fight the 24/14 above. */
.find-q-head h2 { font-size: var(--fs-lg); font-weight: 600; margin: 0; color: var(--text); }

.find-opts { display: flex; flex-wrap: wrap; gap: 10px 8px; }

/*
  An option chip. 40px is the floor, not the height: a long gloss wraps and
  the chip grows rather than clipping it. The row gap is deliberately larger
  than the column gap — equal gaps make wrapped rows read as a grid instead of
  as rows.
*/
.find-opt {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    min-height: 40px;
    padding: 8px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--raised);
    color: var(--dim);
    font-size: var(--fs-sm2);
    text-decoration: none;
    transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

.find-opt .tick { font-family: var(--mono); font-size: var(--fs-sm); color: var(--faint); }
.find-opt .count { font-size: var(--fs-sm); color: var(--faint); }
.find-opt:hover { background: color-mix(in srgb, var(--accent) 8%, var(--raised)); }
.find-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Chosen: the tint, the ring and the count in the accent, because the number
   is the part of the chip that says what answering will return. */
.find-opt.on {
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.find-opt.on .tick, .find-opt.on .count { color: var(--accent); }

/* The long tail. A summary is a control, so it gets a control's hit area. */
.find-tail { margin-top: 10px; }
.find-tail > summary {
    cursor: pointer;
    display: inline-block;
    padding: 6px 10px;
    font-size: var(--fs-sm);
    color: var(--faint);
    border-radius: var(--radius);
}
.find-tail > summary:hover { color: var(--text); background: var(--raised); }
.find-tail .find-opts { margin-top: 10px; }

/* ── the name field ───────────────────────────────────────────────────────
   Label above the field, not beside it: beside it the field lost half the
   column and a typed name could not be read back.                          */

.find-name { padding: 0 var(--gutter); margin: 0; }
.find-name label { display: block; margin-bottom: 8px; }
.find-name .search-field { max-width: 460px; }

/* ── the count ────────────────────────────────────────────────────────────
   The outer cell carries the fill and the edge at full row height; the inner
   wrapper is what sticks. Splitting the two is what lets the column read as a
   column even where the panel is short.                                    */

.find-panel {
    grid-column: 2;
    background: var(--recessed);
    border-left: 1px solid var(--line-soft);
    min-width: 0;
}

.find-panel-inner {
    position: sticky;
    top: 20px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    /* 24 at the top so the first line lands on the first question's heading. */
    padding: 24px 22px;
}

.find-count { display: flex; align-items: baseline; gap: 8px; margin: 0; }

.find-n {
    font-size: var(--fs-6xl);
    font-weight: 600;
    line-height: 1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

.find-noun { font-size: var(--fs-sm2); color: var(--dim); }
.find-basis { margin: 0; font-size: var(--fs-sm); color: var(--faint); }

.find-chips { display: flex; flex-wrap: wrap; gap: 6px; }

.find-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 11px;
    border: 1px solid var(--line);
    border-radius: var(--radius-pill);
    background: var(--raised);
    color: var(--dim);
    font-size: var(--fs-sm);
    text-decoration: none;
    max-width: 100%;
}
.find-chip .chip-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.find-chip .chip-x { color: var(--faint); }
.find-chip:hover { color: var(--text); border-color: var(--dim); }

/*
  The way out of a combination that returned almost nothing. Quiet on purpose:
  no amber, no warning box, no sentence about the result being narrow. The
  reader can see the count; this is the one thing they cannot work out.
*/
.find-loosen {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 12px;
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    color: var(--dim);
    font-size: var(--fs-sm);
    text-decoration: none;
}
.find-loosen:hover { color: var(--text); border-color: var(--dim); background: var(--raised); }

/* The one accented control on the page, and it is a link: it navigates to the
   listing with this question asked, and nothing here is a form to fire. */
.find-go {
    display: block;
    padding: 11px 16px;
    border-radius: var(--radius);
    border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    box-shadow: var(--glow);
    color: var(--accent);
    font-size: var(--fs-base);
    font-weight: 600;
    text-align: center;
    text-decoration: none;
}
.find-go:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.find-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.find-clear { margin: 0; font-size: var(--fs-sm); }
.find-clear a { color: var(--faint); }
.find-clear a:hover { color: var(--text); }

/*
  ── the panel, narrow ─────────────────────────────────────────────────────
  The drawing has no responsive treatment and cannot be followed here: at
  430px its own grid crushes the question column to 139px while the panel
  keeps all 310. So the panel stacks — above the questions, because the count
  is the thing the page exists to show and putting it under six questions
  would mean scrolling past every one of them to find out what they returned.
  It stays sticky at the top of the viewport, which is the same promise the
  side column makes at a width that has room for one.
*/
@media (max-width: 53.75em) {
    .find-grid { grid-template-columns: minmax(0, 1fr); }
    .find-questions, .find-panel { grid-column: 1; }

    /*
      Below the questions in the visual order because it is below them in the
      DOM, and a column that disagrees with the tab order is a worse answer
      than a long page. It sticks to the *bottom* instead of the top, which
      keeps the same promise from the other end: the count and the submit are
      on screen while the last question is being answered, without a reader
      having to find them again afterwards.
    */
    .find-panel {
        border-left: 0;
        border-top: 1px solid var(--line-soft);
        position: sticky;
        bottom: 0;
    }

    /*
      Laid across rather than down, because a bar pinned to the bottom of a
      phone spends every pixel it takes: short controls share a line, so the
      whole panel costs four rows with two answers given rather than seven.

      A wrapping row *in document order*, and that part is not cosmetic. This
      was a two-column bar with the call to action pinned right and spanning
      three rows, which put it first to the eye and fourth to the keyboard:
      tabbing ran chips, loosen, then 114px back up the screen to the button.
      Flex-wrap packs the same controls in the order they are written, so the
      focus ring only ever travels forwards.
    */
    .find-panel-inner {
        position: static;
        display: flex;
        /* The base rule stacks; this is the one place the direction changes, and
           leaving it out gives a centred column that reads as a broken card. */
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px 12px;
        padding: 12px var(--gutter);
    }

    /*
      The kicker labels a column, and this is a bar. The sub-line beside the
      count says the same thing in more words, and the sentence a screen reader
      is given — "2 games match every answer" — is untouched, so nothing is
      lost that was only being carried here.
    */
    .find-panel-inner > .kicker { display: none; }

    .find-chips { flex: 0 1 auto; }
    .find-go { padding: 9px 14px; }
    .find-n { font-size: var(--fs-5xl); }

    /*
      The bar is pinned over the foot of the viewport, and a browser scrolling a
      newly focused option into view does not know it is there — so tabbing down
      the last question lands the focus ring behind the panel. `scroll-padding`
      is the scriptless way to tell it, and `:has` is what keeps the rule on the
      one page that has a bar rather than on every scroll on the site.
    */
    html:has(.find-page) { scroll-padding-bottom: 200px; }
}
