/* ===========================================================================
   THE STYLESHEET FOR THE PAGES THAT ARE NOT THE GAME.
   ===========================================================================

   /about and /assets are static documents. They must LOOK like the menu the
   player just left, but they cannot be built the way the menu is built: the
   menu's chrome lives inside a 227 KB style.css that also carries the HUD, the
   killcam, the scoreboard, the gunsmith and eleven screens' worth of state
   classes, and none of that is needed to render a paragraph.

   So the tokens are RESTATED here rather than imported, and that is a decision
   with a cost worth naming. Two files now declare `--gold`. The alternative —
   linking style.css from a document page — would make every marketing page pay
   for the entire game's CSS on first paint, on a phone, before a single word
   appears. A duplicated hex is a cosmetic drift risk; a 227 KB render-blocking
   download on a 4G phone is a bounce. scripts/validate-site-pages.mjs re-reads
   the `:root` block of style.css and fails when the two palettes disagree, so
   the drift is caught by the build rather than by a person noticing that the
   gold looks slightly off on one page.

   EVERYTHING HERE IS SQUARE. No border-radius above 2px anywhere in the game's
   menu, and the one place that has it (kbd) keeps it. A rounded card on these
   pages would read as a different product.
   ========================================================================== */

/* The display face, self-hosted, same two files the game loads. `swap` because
   the first paint of a document page is text and a blocked paint is worse than
   a flash of the fallback — which is why the fallback stack is a condensed
   grotesque rather than a serif, so the reflow is small. */
@font-face {
  font-family: 'Staatliches';
  src: url('/assets/fonts/staatliches-latin.woff2') format('woff2');
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+2000-206F, U+2074, U+20AC, U+2122, U+2212;
}
@font-face {
  font-family: 'Staatliches';
  src: url('/assets/fonts/staatliches-latin-ext.woff2') format('woff2');
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ===========================================================================
   TWO THEMES, AND LIGHT IS THE DEFAULT.
   ===========================================================================

   Asked for directly: these two routes get a light and a dark theme, light by
   default, "bc its hard to see everything otherwise". The game itself is
   untouched — a shooter is played in the dark and always will be.

   THE DEFAULT LIVES IN `:root` AND DARK IS THE OVERRIDE, AND THAT ORDERING IS
   THE WHOLE ANTI-FLASH STRATEGY. These pages carry no inline <script>: the CSP
   sets `script-src-attr 'none'`, scripts/validate-csp-inline.mjs keeps a census
   of inline scripts, and scripts/validate-site-pages.mjs fails the build if one
   appears here. So the usual head-blocking theme guard is not available, and any
   theme applied by a deferred module paints one frame of the wrong colour first.

   Putting LIGHT in `:root` means the majority case — no stored preference —
   paints correctly with zero JavaScript and therefore cannot flash at all. Only
   a visitor who has explicitly chosen dark can see one frame, and that frame is
   light-on-its-way-to-dark rather than a white flash in a dark room, which is
   the version that actually hurts.

   `prefers-color-scheme` is deliberately NOT consulted for the default. It was
   considered and rejected: the request was "light by default", and a page that
   silently obeys the OS instead would leave the person who asked staring at the
   dark one wondering whether the work happened.

   NOT A CREAM PAGE. The light surface is a cool grey with a trace of the game's
   own blue, not the warm near-white that every generated site defaults to. Warmth
   here would fight the gold rather than support it.

   CONTRAST, MEASURED, NOT ASSUMED — every ratio below is against --s1 #f3f5f6:
     --ink    #10161a  16.4:1   body and headings
     --ink-2  #3d474e   8.7:1   secondary prose
     --ink-3  #58636a   5.6:1   the QUIETEST TEXT STEP — every small label,
                                caption, placeholder and meta line
     --ink-4  #79848b   3.5:1   STRUCTURAL ONLY — never a glyph
     --gold-ink #8a5e0d  4.6:1  the accent when it is TEXT
     --blue-ink #1c5fbf  5.4:1  the meter when it is TEXT

   `--ink-4` IS NOT A TEXT COLOUR, AND IT TOOK A MEASUREMENT TO ADMIT IT.
   It shipped as one anyway: 30 rules across these two stylesheets painted labels,
   captions, placeholders and the footer attribution with it, and it measured
   3.5:1 in light and 2.6:1 in dark against the surfaces it actually sat on. Not
   one of the 30 was a hairline. That is the single most common reason a generated
   page reads as hard work — grey text that looks refined in a mockup and is a
   struggle on a real screen — and this palette had it everywhere while carrying a
   comment claiming the opposite.
   Every one now uses `--ink-3`, which cost a step of hierarchy and bought a page
   that can be read. `--ink-4` is kept because the game declares it and the dark
   palette is asserted equal to style.css's, but nothing here may put a glyph in
   it; scripts/validate-site-responsive.mjs measures the real thing in a real
   browser and fails under 4.5:1.

   `--ink-3` is a shade darker in light than the game's own step, so it clears
   4.5:1 on --s2 and --s3 (the search fields and hover rows) and not only on the
   panel it was first checked against.
   ========================================================================== */
:root {
  /* Surfaces, lightest floor to most-raised. */
  --s0: #e7eaec;
  --s1: #f3f5f6;
  --s2: #dde2e5;
  --s3: #d0d6da;
  --s4: #c2cad0;

  /* Text, strongest to faintest. */
  --ink: #10161a;
  --ink-2: #3d474e;
  --ink-3: #58636a;
  --ink-4: #79848b;

  /* THE ACCENT IS TWO TOKENS, AND THIS IS THE ONE THING THE LIGHT THEME COULD
     NOT REUSE. #e0a437 on a light surface is 2.1:1 — fine as a FILL under dark
     text, unreadable as text itself. So the fill and the ink are separated:
     `--gold` stays the game's exact gold and is only ever painted as a surface;
     `--gold-ink` is what a small gold label is allowed to be. In dark mode the
     two collapse back to the same value, which is why every rule in these
     stylesheets can name one or the other without ever asking which theme it is
     in. Same split for the archive's cold light. */
  --gold: #e0a437;
  --gold-ink: #8a5e0d;
  --gold-d: #a2741f;
  --blue: #2f6fd0;
  --blue-ink: #1c5fbf;
  --red: #c02a3e;
  --green: #17683c;

  --rule: rgba(16, 22, 26, 0.11);
  --rule-strong: rgba(16, 22, 26, 0.24);

  /* The inset hairline along the top of every raised panel — a light above the
     surface in dark mode, and the sheen off a light one here. */
  --lip: inset 0 1px 0 rgba(255, 255, 255, 0.85);

  --display: 'Staatliches', 'Oswald', 'Arial Narrow', system-ui, sans-serif;
  --ui: 'DIN Alternate', 'Roboto Condensed', 'Archivo Narrow', 'Segoe UI',
        'Helvetica Neue', Inter, Roboto, 'DejaVu Sans', system-ui, -apple-system, sans-serif;
  --mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --fast: 120ms;
  --med: 200ms;

  --nav-h: 62px;
  --wrap: 1400px;

  /* Tells the browser to draw scrollbars, form controls and the canvas
     underneath the page in the matching mode. Without it a light page gets dark
     scrollbars and the `<input type=range>` in the playhead renders in the wrong
     one, which is the detail that makes a themed page feel half-done. */
  color-scheme: light;
}

/* ---------------------------------------------------------------------------
   DARK — the game's own palette, restored exactly.
   Every value here is the literal token from style.css's `:root`, which is what
   scripts/validate-site-pages.mjs compares against: in dark mode these pages are
   the same colours as the menu, to the digit.
   --------------------------------------------------------------------------- */
:root[data-theme='dark'] {
  --s0: #06080a;
  --s1: #0b0e10;
  --s2: #12171a;
  --s3: #1a2126;
  --s4: #232c32;

  --ink: #eef1f3;
  --ink-2: #a7b0b6;
  --ink-3: #78838a;
  --ink-4: #4d565c;

  /* The fill and the ink collapse back together: on a near-black surface the
     game's gold is 8.4:1 and needs no darkening. */
  --gold: #e0a437;
  --gold-ink: #e0a437;
  --gold-d: #a2741f;
  --blue: #7db0ff;
  --blue-ink: #7db0ff;
  --red: #e4596c;
  --green: #74d195;

  --rule: rgba(238, 241, 243, 0.085);
  --rule-strong: rgba(238, 241, 243, 0.17);
  --lip: inset 0 1px 0 rgba(255, 255, 255, 0.055);

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   Reset
   -------------------------------------------------------------------------- */
* { box-sizing: border-box; }

html {
  background: var(--s0);
  /* Anchor links land below the sticky bar rather than under it. Declared on
     :root so every `[id]` inherits it without a per-element rule. */
  scroll-padding-top: calc(var(--nav-h) + 18px);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  min-width: 320px;
  overflow-x: clip;
  background: var(--s0);
  color: var(--ink);
  font: 400 15px/1.6 var(--ui);
  letter-spacing: 0.2px;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, canvas, svg { display: block; max-width: 100%; }
a { color: inherit; }
button, input, select { font: inherit; color: inherit; }
h1, h2, h3, h4 { margin: 0; font-weight: 400; }
p { margin: 0; }

/* NO SYNTHETIC BOLD ON THE DISPLAY FACE. Staatliches ships one weight; asking
   for 700 makes the browser smear the 400 outward, which thickens the stems
   unevenly and is exactly what a "cheap game site" looks like. */
:where(h1, h2, h3, .display) { font-weight: 400; }

:focus-visible {
  outline: 2px solid var(--gold-ink);
  outline-offset: 2px;
}
/* A mouse click on a card should not leave a ring behind. `:focus-visible`
   already handles this; the rule exists to defeat UA defaults on <summary>. */
:focus:not(:focus-visible) { outline: none; }

::selection { background: color-mix(in srgb, var(--gold) 34%, transparent); color: var(--ink); }

/* --------------------------------------------------------------------------
   The page backdrop — the menu's own sky, minus the renderer.

   The menu paints a live Three.js map behind itself. A document page cannot
   afford that: it would mean shipping the renderer, four maps' geometry and
   their textures in order to read an About page. But it does not have to,
   because the menu's backdrop was already built in two halves — a painted CSS
   stack (sun, haze, skyline, grain) and a WebGL plate that fades in ON TOP of
   it. The painted half is self-contained and needs no JavaScript at all.

   So these are menu-bg.css's first three layers and its skyline, carried over
   verbatim. Same gradients, same clip-path, same drift period. What is dropped
   is everything from the poster down — the `<img>`, the live plate, the scrim
   and the dissolve — which js/cinematics/menu.js owns and which a static page
   has nothing to fade into.

   The markup is `<div class="page-bg"><span></span><span></span><span></span></div>`,
   the same three spans, so a rule copied from menu-bg.css lands unchanged.

   `position: fixed` rather than `background-attachment: fixed`: the latter
   forces a full-page repaint on every scroll frame in Safari and is the classic
   cause of a "why is scrolling janky on iOS" bug. `contain: layout paint` for
   the same reason — it promises the compositor nothing inside can affect
   anything outside.
   -------------------------------------------------------------------------- */
.page-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background-color: var(--s0);
  contain: layout paint;
}

/* 1 — the sun, low and to the right, blurred past any visible banding. */
.page-bg > span:nth-child(1) {
  position: absolute;
  top: -30vh;
  right: -18vw;
  width: 92vw;
  height: 92vw;
  background: radial-gradient(circle,
    rgba(255, 214, 140, 0.30) 0%,
    rgba(226, 154, 72, 0.16) 32%,
    rgba(120, 82, 46, 0.06) 56%,
    transparent 72%);
  filter: blur(12px);
}

/* 2 — haze. Two warm lobes and one dark floor, drifting on a 68s cycle that is
       coprime with everything else on the page so nothing ever pulses in step. */
.page-bg > span:nth-child(2) {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(60% 40% at 20% 70%, rgba(180, 172, 150, 0.10), transparent 70%),
    radial-gradient(50% 34% at 76% 32%, rgba(214, 176, 118, 0.10), transparent 70%),
    radial-gradient(70% 50% at 50% 100%, rgba(30, 26, 22, 0.55), transparent 72%);
}

/* 3 — the veil: 1-in-3 scanlines plus a vignette, screened over everything.
       This is the layer shared with the in-game post stack and it is what stops
       a flat near-black from banding into visible steps on an 8-bit panel. */
.page-bg > span:nth-child(3) {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(0deg,
      rgba(255, 255, 255, 0.018) 0 1px,
      transparent 1px 3px),
    radial-gradient(120% 100% at 50% 40%, transparent 40%, rgba(4, 6, 8, 0.72) 100%);
  mix-blend-mode: screen;
}

/* The skyline — the single hard edge in an otherwise entirely soft composition,
   and the thing that makes the gradient read as a place rather than a colour. */
.page-bg::after {
  content: '';
  position: absolute;
  left: -5%;
  right: -5%;
  top: 62%;
  height: 34vh;
  background: linear-gradient(180deg,
    rgba(28, 24, 20, 0) 0%,
    rgba(20, 17, 14, 0.72) 22%,
    rgba(11, 13, 15, 0.96) 100%);
  clip-path: polygon(
    0% 26%, 6% 24%, 9% 12%, 12% 24%, 22% 22%, 26% 30%, 34% 27%,
    41% 8%, 44% 27%, 55% 25%, 60% 33%, 68% 29%, 74% 16%, 78% 30%,
    88% 27%, 94% 31%, 100% 28%, 100% 100%, 0% 100%);
}

/* THE SAME SKY AT NOON.
   The layers above are a night composition — a warm sun over a dark horizon with
   a screen-blended scanline veil. Every one of those moves is wrong on a light
   page: `screen` over near-white is invisible, and a 0.55-alpha dark floor under
   a light surface is a grey smear.

   So light mode keeps the STRUCTURE and re-lights it. The sun becomes a soft
   warm bloom instead of a glow, the haze inverts to a cool wash that grounds the
   page rather than darkening it, the veil stops blending and drops to almost
   nothing, and the skyline becomes a pale silhouette — still the same ridge in
   the same place, read against a bright sky instead of a dark one. */
:root:not([data-theme='dark']) .page-bg > span:nth-child(1) {
  background: radial-gradient(circle,
    rgba(224, 164, 55, 0.16) 0%,
    rgba(224, 164, 55, 0.07) 34%,
    rgba(224, 164, 55, 0.02) 58%,
    transparent 74%);
  filter: blur(18px);
}
:root:not([data-theme='dark']) .page-bg > span:nth-child(2) {
  background:
    radial-gradient(60% 40% at 18% 68%, rgba(47, 111, 208, 0.05), transparent 70%),
    radial-gradient(50% 34% at 78% 30%, rgba(224, 164, 55, 0.06), transparent 70%),
    radial-gradient(70% 50% at 50% 100%, rgba(16, 22, 26, 0.06), transparent 72%);
}
:root:not([data-theme='dark']) .page-bg > span:nth-child(3) {
  mix-blend-mode: normal;
  background:
    repeating-linear-gradient(0deg, rgba(16, 22, 26, 0.012) 0 1px, transparent 1px 3px),
    radial-gradient(120% 100% at 50% 38%, transparent 46%, rgba(16, 22, 26, 0.05) 100%);
}
:root:not([data-theme='dark']) .page-bg::after {
  background: linear-gradient(180deg,
    rgba(16, 22, 26, 0) 0%,
    rgba(16, 22, 26, 0.045) 30%,
    rgba(16, 22, 26, 0.085) 100%);
}

/* Motion lives in the no-preference branch and the keyframes have no `from`
   fill, so if the animation never runs the haze is simply already where it
   belongs. Nothing here is load-bearing on layout. */
@media (prefers-reduced-motion: no-preference) {
  .page-bg > span:nth-child(2) { animation: pageHaze 68s ease-in-out infinite alternate; }
}
@keyframes pageHaze {
  0% { transform: translate3d(-2%, 1%, 0) scale(1.02); }
  100% { transform: translate3d(3%, -2%, 0) scale(1.06); }
}

/* --------------------------------------------------------------------------
   Navigation
   -------------------------------------------------------------------------- */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: var(--nav-h);
  padding: 0 clamp(16px, 3.4vw, 48px);
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--s0) 88%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
}

.site-brand {
  display: flex;
  align-items: baseline;
  gap: 10px;
  text-decoration: none;
  white-space: nowrap;
}
.site-brand b {
  font: 400 24px/1 var(--display);
  letter-spacing: 0.6px;
  text-transform: uppercase;
}
.site-brand b em { font-style: normal; color: var(--gold); }
.site-brand span {
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}

.site-links {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2.2vw, 30px);
}
.site-links a {
  position: relative;
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--fast) var(--ease);
}
.site-links a:hover { color: var(--ink); }
.site-links a[aria-current='page'] { color: var(--ink); }
.site-links a[aria-current='page']::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -21px;
  height: 2px;
  background: var(--gold);
}

/* The one thing on the bar that is an action rather than a destination, so it
   is the one thing that is filled. */
.nav-cta {
  padding: 9px 16px;
  background: var(--gold);
  color: #14100a !important;
  font-family: var(--display);
  font-size: 15px;
  letter-spacing: 1.2px;
  transition: background var(--fast) var(--ease);
}
.nav-cta:hover { background: #efb44b; }
.nav-cta::after { content: none !important; }

/* THE MUSIC BUTTON, AND WHY IT HAS THREE STATES RATHER THAN TWO.
   "off" and "the browser will not let us start yet" are different facts. A
   button that reads OFF when the truth is BLOCKED is a button people press
   twice and then give up on, because the first press appears to do nothing.
   js/site-chrome.js writes `data-state`; the bars below are what it looks like.
   The icon is three bars of an equaliser — CSS only, so no icon font and no
   request for a 12px mark. */
.music-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--rule);
  background: var(--s1);
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.music-toggle:hover { color: var(--ink); border-color: var(--rule-strong); }

.music-toggle-icon {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  width: 13px;
  height: 12px;
}
.music-toggle-icon::before,
.music-toggle-icon::after {
  content: '';
  width: 3px;
  background: currentColor;
}
.music-toggle-icon::before { height: 100%; }
.music-toggle-icon::after { height: 55%; }

.music-toggle[data-state='on'] { color: var(--gold-ink); border-color: color-mix(in srgb, var(--gold-ink) 40%, transparent); }
/* Bars only animate while sound is actually coming out, so the control is an
   indicator and not just a label. Two bars on coprime periods so they never
   bounce in lockstep, which is what makes a two-bar equaliser look fake. */
@media (prefers-reduced-motion: no-preference) {
  .music-toggle[data-state='on'] .music-toggle-icon::before { animation: eqA 1.1s ease-in-out infinite alternate; }
  .music-toggle[data-state='on'] .music-toggle-icon::after { animation: eqB 0.74s ease-in-out infinite alternate; }
}
@keyframes eqA { from { height: 40%; } to { height: 100%; } }
@keyframes eqB { from { height: 100%; } to { height: 35%; } }

.music-toggle[data-state='off'] { color: var(--ink-3); }
.music-toggle[data-state='off'] .music-toggle-icon::before { height: 40%; }
.music-toggle[data-state='off'] .music-toggle-icon::after { height: 40%; }

/* The theme switch. Same chassis as the music button so the nav reads as one
   row of controls rather than two designs. The icon is a circle half-filled by
   an inset shadow — a moon at one angle, a sun at the other — drawn in CSS,
   because a nav that costs an SVG request for a 13px mark is a nav that paints
   late on the connection where it matters. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--rule);
  background: var(--s1);
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.theme-toggle:hover { color: var(--ink); border-color: var(--rule-strong); }
.theme-toggle-icon {
  width: 13px;
  height: 13px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  box-shadow: inset 5px 0 0 0 currentColor;
  transition: box-shadow var(--med) var(--ease);
}
:root[data-theme='dark'] .theme-toggle-icon { box-shadow: inset -5px 0 0 0 currentColor; }

.site-menu-toggle { display: none; }

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
.site-main { position: relative; z-index: 1; }
.wrap { width: min(var(--wrap), calc(100% - clamp(28px, 6vw, 96px))); margin-inline: auto; }

/* --------------------------------------------------------------------------
   The masthead
   --------------------------------------------------------------------------
   Not a hero, and deliberately not the shape of one. There is no small tracked
   uppercase label above the heading, because an eyebrow on every section is the
   grammar that marks a page as machine-authored, and one at the top of the page
   is where that habit starts.

   What sits under the heading instead is a COUNT of real things, which is the
   one piece of furniture both pages have earned: /assets counts what it is
   about to show you, /about counts what was made. Every figure in it is a fact
   the page then goes on to demonstrate.
   -------------------------------------------------------------------------- */
.masthead {
  padding: clamp(48px, 7vw, 104px) 0 clamp(28px, 3.5vw, 48px);
  border-bottom: 1px solid var(--rule);
}
.masthead h1 {
  max-width: 15ch;
  font: 400 clamp(52px, 10vw, 138px)/0.84 var(--display);
  /* −0.04em is the documented floor and −0.015em is well inside it. Tighter and
     the counters close up: the word stops being read and starts being
     recognised as a shape, which is the "designed until it is cramped" tell. */
  letter-spacing: -0.015em;
  text-transform: uppercase;
  text-wrap: balance;
}
.masthead h1 em { font-style: normal; color: var(--gold-ink); }
.masthead-lede {
  max-width: 62ch;
  margin-top: clamp(20px, 2.6vw, 32px);
  color: var(--ink-2);
  font-size: clamp(15px, 1.25vw, 18px);
  line-height: 1.7;
  text-wrap: pretty;
}

.tally {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  margin-top: clamp(28px, 3.5vw, 44px);
  border: 1px solid var(--rule);
  background: var(--rule);
}
.tally > div { flex: 1 1 118px; padding: 13px 15px 14px; background: var(--s0); }
.tally b {
  display: block;
  color: var(--ink);
  font: 400 clamp(23px, 2.5vw, 32px)/1 var(--display);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}
.tally span {
  display: block;
  margin-top: 5px;
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   Sections and prose
   --------------------------------------------------------------------------
   One measure, no aside rail, no numbered marker. The previous version put
   `01 · The game` in a left column beside every section, which is the numbered
   -eyebrow habit one tier deeper: scaffolding reached for because pages do
   that, carrying no information the heading did not already carry.
   -------------------------------------------------------------------------- */
.section { padding: clamp(52px, 7vw, 100px) 0; border-bottom: 1px solid var(--rule); }

.prose { max-width: 68ch; }
.prose > h2 {
  margin-bottom: 4px;
  font: 400 clamp(30px, 4.2vw, 56px)/0.96 var(--display);
  letter-spacing: -0.01em;
  text-transform: uppercase;
  text-wrap: balance;
}
.prose h3 {
  margin-top: 40px;
  font: 400 23px/1.1 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
}
.prose p { margin-top: 18px; color: var(--ink-2); line-height: 1.8; text-wrap: pretty; }
.prose p.lede {
  margin-top: 20px;
  color: var(--ink);
  font-size: clamp(17px, 1.7vw, 21px);
  line-height: 1.62;
}
/* Links are brighter than their sentence with a translucent underline that goes
   solid on hover — the same treatment the game's own footer credit uses. NOT
   gold: gold means "primary action or current selection" everywhere else in
   this product, and a paragraph full of gold words spends that meaning. */
.prose a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--ink) 34%, transparent);
  transition: text-decoration-color var(--fast) var(--ease);
}
.prose a:hover, .prose a:focus-visible { text-decoration-color: var(--ink); }
.prose ul { margin: 18px 0 0; padding-left: 18px; color: var(--ink-2); line-height: 1.8; }
.prose li { margin-top: 7px; }
.prose li::marker { color: var(--gold-ink); }
.prose strong { color: var(--ink); font-weight: 600; }

/* --------------------------------------------------------------------------
   The bill of materials
   --------------------------------------------------------------------------
   The About page's centrepiece, and the reason it is a table rather than a grid
   of credit cards: the question people actually arrive with is "which tool made
   which part", and that is a two-column lookup. A card grid answers it by
   making the reader scan; a table answers it by letting them read down one
   column. It is also the honest shape — this is a parts list.
   -------------------------------------------------------------------------- */
.bom { width: 100%; margin-top: clamp(26px, 3.4vw, 40px); border-collapse: collapse; text-align: left; }
.bom caption {
  padding-bottom: 12px;
  color: var(--ink-3);
  font-size: 12px;
  line-height: 1.6;
  text-align: left;
}
.bom th {
  padding: 0 14px 10px 0;
  border-bottom: 1px solid var(--rule-strong);
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}
.bom td { padding: 14px 14px 14px 0; border-bottom: 1px solid var(--rule); color: var(--ink-2); font-size: 14px; line-height: 1.55; }
.bom td:last-child, .bom th:last-child { padding-right: 0; }
.bom tbody tr:hover td { color: var(--ink); }
/* The source column carries the display face, so the four names that made this
   game read as names and the rest of the row reads as description. */
.bom td:first-child {
  width: 27%;
  color: var(--ink);
  font: 400 19px/1.1 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
  vertical-align: top;
}
.bom td:first-child a { text-decoration-color: color-mix(in srgb, var(--gold-ink) 70%, transparent); }
.bom td:last-child {
  width: 22%;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  vertical-align: top;
}

/* --------------------------------------------------------------------------
   Controls table and keycaps
   -------------------------------------------------------------------------- */
kbd {
  display: inline-block;
  min-width: 26px;
  margin: 2px 3px 2px 0;
  padding: 4px 7px;
  border-radius: 2px;
  background: var(--s3);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 11px;
  text-align: center;
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ink) 26%, transparent), 0 1px 0 var(--lip-edge, rgba(255, 255, 255, 0.06));
}
.controls { width: 100%; margin-top: 24px; border-collapse: collapse; }
.controls th {
  padding: 0 12px 10px 0;
  border-bottom: 1px solid var(--rule-strong);
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-align: left;
  text-transform: uppercase;
}
.controls td { padding: 11px 12px 11px 0; border-bottom: 1px solid var(--rule); color: var(--ink-2); font-size: 14px; }
.controls td:first-child { width: 44%; }

/* --------------------------------------------------------------------------
   The fan-project notice
   --------------------------------------------------------------------------
   Bordered and tinted rather than a plain paragraph, because it is the one
   block on the site with legal weight and a reader skimming for it should be
   able to find it without reading. Gold at 30% on the border and 4.5% on the
   fill: enough to separate, nowhere near enough to shout.
   -------------------------------------------------------------------------- */
.notice {
  padding: clamp(24px, 4vw, 46px);
  border: 1px solid color-mix(in srgb, var(--gold-ink) 34%, transparent);
  background: color-mix(in srgb, var(--gold) 8%, transparent);
}
.notice h2 {
  font: 400 clamp(26px, 3.4vw, 44px)/1 var(--display);
  letter-spacing: -0.005em;
  text-transform: uppercase;
}
.notice p { max-width: 88ch; margin-top: 14px; color: var(--ink-2); font-size: 13.5px; line-height: 1.8; }
.notice a { color: var(--ink); text-decoration-color: color-mix(in srgb, var(--ink) 34%, transparent); text-underline-offset: 3px; }

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */
.site-footer {
  position: relative;
  z-index: 1;
  padding: 44px clamp(16px, 3.4vw, 48px) 96px;
}
.footer-inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 24px 40px;
  align-items: start;
  padding-top: 24px;
  border-top: 1px solid var(--rule);
}
.footer-brand b {
  display: block;
  font: 400 20px/1 var(--display);
  letter-spacing: 0.6px;
  text-transform: uppercase;
}
.footer-brand b em { font-style: normal; color: var(--gold); }
.footer-brand p { margin-top: 10px; max-width: 56ch; color: var(--ink-3); font-size: 12px; line-height: 1.8; }
.footer-brand a { color: var(--ink-2); text-decoration-color: var(--gold-d); text-underline-offset: 3px; }
.footer-links { display: flex; flex-wrap: wrap; gap: 10px 22px; justify-content: flex-end; }
.footer-links a {
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--fast) var(--ease);
}
.footer-links a:hover { color: var(--ink); }

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */

/* The nav collapses on its own breakpoint, above the content grid's. The row is
   nowrap — brand, four links, CTA — so it does not degrade, it overflows and
   gives the whole document a horizontal scrollbar. The decorative brand tagline
   goes first, which buys ~140px and keeps the full row alive down to 900px. */
@media (max-width: 1080px) {
  .site-brand span { display: none; }
}

@media (max-width: 900px) {
  .site-menu-toggle {
    display: grid;
    place-content: center;
    gap: 5px;
    width: 44px;
    height: 44px;
    flex: 0 0 auto;
    border: 1px solid var(--rule);
    background: var(--s1);
    cursor: pointer;
  }
  .site-menu-toggle span {
    width: 19px;
    height: 1.5px;
    background: var(--ink);
    transition: transform var(--med) var(--ease), opacity var(--fast) linear;
  }
  .nav-open .site-menu-toggle span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
  .nav-open .site-menu-toggle span:nth-child(2) { opacity: 0; }
  .nav-open .site-menu-toggle span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

  .site-links {
    position: absolute;
    top: var(--nav-h);
    left: 0;
    right: 0;
    display: grid;
    gap: 0;
    padding: 6px clamp(16px, 3.4vw, 48px) 18px;
    border-bottom: 1px solid var(--rule-strong);
    background: color-mix(in srgb, var(--s0) 97%, transparent);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    box-shadow: 0 18px 40px color-mix(in srgb, var(--ink) 22%, transparent);
    /* Collapsed by height rather than `display:none` so the open/close is
       animatable and the links stay in the DOM for a find-in-page. */
    visibility: hidden;
    opacity: 0;
    transform: translateY(-8px);
    transition: opacity var(--med) var(--ease), transform var(--med) var(--ease),
      visibility 0s linear var(--med);
  }
  .nav-open .site-links {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition-delay: 0s;
  }
  .site-links a {
    display: flex;
    align-items: center;
    min-height: 54px;
    border-bottom: 1px solid var(--rule);
    font-size: 14px;
    letter-spacing: 1.8px;
  }
  .site-links a[aria-current='page']::after {
    left: auto;
    right: 0;
    bottom: 50%;
    width: 3px;
    height: 22px;
    transform: translateY(50%);
  }
  .nav-cta {
    justify-content: center;
    margin-top: 14px;
    min-height: 50px;
    border-bottom: 0 !important;
    font-size: 17px;
  }
}

@media (max-width: 860px) {
  .footer-inner { grid-template-columns: 1fr; }
  .footer-links { justify-content: flex-start; }
  /* THE PARTS LIST STOPS BEING A TABLE.
     Three columns at 360px gives each about 100px, which breaks "Sound effects
     and voice design" onto five lines and makes the whole thing unreadable. So
     each row becomes a block: the source name on its own line, the description
     under it, the amount as a quiet footer. The <th>s go, because a header row
     labelling columns that no longer exist is worse than none — the content is
     self-describing in this form. */
  .bom, .bom tbody, .bom tr, .bom td { display: block; width: auto; }
  .bom thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .bom tr { padding: 16px 0; border-bottom: 1px solid var(--rule); }
  .bom td { padding: 0; border: 0; }
  .bom td:first-child { width: auto; font-size: 21px; }
  .bom td:nth-child(2) { margin-top: 7px; }
  .bom td:last-child { width: auto; margin-top: 7px; }
}

@media (max-width: 560px) {
  .wrap { width: calc(100% - 28px); }
  .site-brand b { font-size: 21px; }
  .masthead { padding-top: 40px; }
  .tally > div { flex-basis: 42%; }
  .controls td:first-child { width: 46%; }
  .controls td, .controls th { padding-inline: 8px 8px; font-size: 13px; }
}

/* Motion is decoration here, never information: every transition above only
   changes colour or position, so switching them off loses nothing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===========================================================================
   THE FEEDBACK DIALOG
   ===========================================================================

   Same markup and the same js/feedback.js the game uses — one module, one
   endpoint, one place reports arrive. What is NOT shared is the styling: the
   game's `.fb*` rules live in style.css alongside the HUD, and these pages do
   not load it. Restating them here is the same trade the palette makes at the
   top of this file, and it buys something extra: a dialog that follows the light
   theme, which the game's copy has no concept of.

   `.hidden`, `.btn` and `.field` are declared here too, scoped to `.fb`. They
   are style.css's vocabulary, and the markup was copied verbatim so that a
   future extraction into one shared partial is a move rather than a rewrite —
   so the classes came with it and need somewhere to land.
   ========================================================================== */

/* js/feedback.js toggles `.hidden` on the dialog, the form and the sent panel.
   Declared with the attribute-strength guard the dock needed for the same
   reason: any class that sets `display` on the same element would beat it. */
.fb.hidden, .fb-sent.hidden, #fb-form.hidden { display: none !important; }

.fb {
  position: fixed;
  inset: 0;
  /* Above the playhead (40) and the nav (30). The dialog is modal — nothing on
     these pages is allowed over it. */
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 4vh 4vw;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.fb-backdrop {
  position: fixed;
  inset: 0;
  appearance: none;
  border: 0;
  /* Mixed from `--ink` rather than a black literal, so the scrim darkens a light
     page and a dark one by the same amount instead of vanishing on one. */
  background: color-mix(in srgb, var(--ink) 62%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  cursor: default;
}
.fb-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 15px;
  width: min(520px, 100%);
  max-height: 92vh;
  padding: clamp(20px, 3vw, 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--s1);
  box-shadow: 0 30px 90px color-mix(in srgb, var(--ink) 38%, transparent), var(--lip);
}
.fb-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.fb-head h2 {
  font: 400 clamp(24px, 3.2vw, 30px)/1 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
}
.fb-x {
  appearance: none;
  border: 0;
  padding: 2px 6px;
  background: none;
  color: var(--ink-3);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--fast) var(--ease);
}
.fb-x:hover { color: var(--ink); }
.fb-sub { color: var(--ink-2); font-size: 13.5px; line-height: 1.65; }

.fb-types { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; border: 0; }
.fb-types legend {
  padding: 0 0 8px;
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}
.fb-type { flex: 1 1 auto; }
.fb-type input { position: absolute; opacity: 0; pointer-events: none; }
.fb-type span {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 8px 12px;
  border: 1px solid var(--rule);
  color: var(--ink-3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.fb-type span:hover { color: var(--ink); border-color: var(--rule-strong); }
.fb-type input:checked + span {
  border-color: color-mix(in srgb, var(--gold-ink) 55%, transparent);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  color: var(--ink);
}
/* The input is visually hidden, so the focus ring has to be moved onto the thing
   that IS visible or a keyboard user cannot see where they are. */
.fb-type input:focus-visible + span { outline: 2px solid var(--gold-ink); outline-offset: 2px; }

.fb-field { display: grid; gap: 7px; }
.fb-field > span {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}
.fb-field textarea, .fb-field input {
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-bottom: 1px solid var(--rule-strong);
  border-radius: 0;
  background: var(--s2);
  color: var(--ink);
  font: 400 14px/1.55 var(--ui);
  resize: vertical;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.fb-field textarea::placeholder, .fb-field input::placeholder { color: var(--ink-3); }
.fb-field textarea:focus, .fb-field input:focus {
  outline: none;
  background: var(--s3);
  border-bottom-color: var(--gold-ink);
}
.fb-count { font-weight: 600; font-variant-numeric: tabular-nums; }
.fb-count[data-state='near'] { color: var(--gold-ink); }
.fb-count[data-state='full'] { color: var(--red); }

.fb-status { min-height: 18px; font-size: 12.5px; line-height: 1.5; }
.fb-status[data-state='error'] { color: var(--red); }
.fb-status[data-state='busy'] { color: var(--ink-2); }
.fb-status:empty { min-height: 0; }
.fb-context { color: var(--ink-3); font-size: 11.5px; line-height: 1.5; }

.fb-actions { display: flex; flex-wrap: wrap; gap: 10px; }
/* style.css's button vocabulary, restated for this dialog only. */
.fb .btn {
  appearance: none;
  flex: 1 1 160px;
  min-height: 44px;
  padding: 12px 18px;
  border: 1px solid var(--rule);
  background: var(--s2);
  color: var(--ink);
  font: 400 16px/1 var(--display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.fb .btn:hover { background: var(--s3); border-color: var(--rule-strong); }
.fb .btn.primary { border-color: transparent; background: var(--gold); color: #14100a; }
.fb .btn.primary:hover { background: #efb44b; color: #14100a; }
.fb .btn.ghost { background: transparent; color: var(--ink-2); }
.fb .btn.ghost:hover { background: transparent; color: var(--ink); }
.fb .btn:disabled { opacity: 0.45; cursor: not-allowed; }

.fb-sent { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.fb-sent h3 { font: 400 23px/1 var(--display); letter-spacing: 0; text-transform: uppercase; }
.fb-tick { font-size: 32px; line-height: 1; color: var(--green); }
.fb-sent .btn { align-self: stretch; flex: 0 0 auto; }

/* The nav entry. Same chassis as the other two nav controls so the row reads as
   one set, and a real <button> because it opens a dialog rather than going
   anywhere — `data-feedback-open` is the entire binding. */
.nav-feedback {
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--rule);
  background: var(--s1);
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.nav-feedback:hover { color: var(--ink); border-color: var(--rule-strong); }

@media (max-width: 900px) {
  /* In the drawer the controls are full-width rows like the links above them. */
  .nav-feedback, .theme-toggle, .music-toggle { justify-content: flex-start; min-height: 50px; font-size: 13px; }
}
@media (max-width: 560px) {
  .fb { padding: 0; }
  .fb-card { width: 100%; max-height: 100dvh; }
  .fb-actions .btn { flex: 1 1 100%; }
}
