/* ===========================================================================
   THE ASSET ARCHIVE — THE BENCH.
   ===========================================================================

   docs/ARCHIVE-DESIGN.md is the argument; this is the implementation. The short
   version: a weapons technician at a bench in a hangar at four in the morning,
   one work lamp overhead, laying each object down on a steel table.

   THE STRUCTURE IS ONE PERSISTENT INSPECTION VIEWPORT. The object holds a fixed
   region of the screen and the catalogue scrolls past it. On a phone the bench
   pins to the top and the index scrolls beneath — the same idea, not a degraded
   version of it, which is the whole reason this page is usable on a phone
   instead of merely not broken there.

   It is also the performance answer. js/archive/stage.js pauses a stage the
   moment it leaves the viewport, and because the benches are sticky, the leaving
   and arriving stages hand over at the section boundary. Exactly one WebGL
   context is ever drawing. Four simultaneously-rendering canvases is what makes
   an asset gallery unusable on a phone, and the layout is what prevents it.

   TWO WORLDS, ONE THEME. Objects are lit warm by the work lamp (--gold); medals
   and sound are lit cold by the meter (--blue). Both are already tokens in the
   game's style.css. The line down the middle of the page is drawn with light.

   NO EYEBROWS, NO NUMBERED SECTION MARKERS, NO ACCENT STRIPES, NO GHOST CARDS,
   NO RADIUS. See the standing rules in docs/ARCHIVE-DESIGN.md before adding to
   this file.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   THE TWO LIGHTS, AND WHY EACH IS TWO TOKENS.

   Objects are lit warm by the work lamp; medals and sound are lit cold by the
   meter. Every component paints with `--lit` rather than naming a colour, so a
   row, a chip and a play head all change worlds by inheritance when a section
   re-points it, instead of each growing a second rule.

   THE BENCH IS DARK IN BOTH THEMES, AND THAT IS THE REASON FOR THE SPLIT.
   A weapon model is lit for a dark studio — the float PMREM environment in
   js/archive/stage.js is a dark room with bright panels — and putting that model
   on a white card would mean re-lighting every asset for a theme. It is also
   simply wrong: a vitrine is dark, which is why product photography and every
   museum case in the world is. So the stage and its readout stay near-black
   whatever the page around them does.

   Which means a gold label INSIDE the bench and a gold label OUT on the page are
   on opposite backgrounds and cannot be the same colour:

     --lit       the bright value. Only ever used on the dark bench, where
                 #e0a437 is 8.4:1 and correct.
     --lit-ink   the theme-aware value. Used everywhere on the page surface,
                 where it resolves to a dark gold at 4.6:1 in light mode and back
                 to the same bright gold in dark mode.

   Getting this backwards is invisible in dark mode and unreadable in light,
   which is exactly the kind of mistake that ships.
   --------------------------------------------------------------------------- */
:root {
  --lamp: #e0a437;
  --lamp-ink: var(--gold-ink);
  --lamp-dim: color-mix(in srgb, #e0a437 15%, transparent);
  --lamp-edge: color-mix(in srgb, #e0a437 46%, transparent);
  --meter: #7db0ff;
  --meter-ink: var(--blue-ink);
  --meter-dim: color-mix(in srgb, var(--blue) 14%, transparent);
  --meter-edge: color-mix(in srgb, var(--blue) 45%, transparent);

  --lit: var(--lamp);
  --lit-ink: var(--lamp-ink);
  --lit-dim: var(--lamp-dim);
  --lit-edge: var(--lamp-edge);

  --bench-top: calc(var(--nav-h) + 18px);
}

/* THE BENCH CARRIES ITS OWN RAMP.
 *
 * The stage and its readout are near-black in BOTH themes, so in light mode they
 * are a dark island on a light page — and every control inside them (`.chip`,
 * `.act`, `.stat-grid`, the status line) paints with `--ink-2`, `--s2`,
 * `--rule`, which the page has just redefined to be dark-on-light. The result is
 * near-black text on near-black metal: the readout is legible in dark mode and
 * completely invisible in light, which is precisely the failure that ships
 * because it only appears in the theme the author was not looking at.
 *
 * Redeclaring the ramp on the bench fixes every one of them at once, and by
 * inheritance rather than by a light-mode twin of each rule. A component dropped
 * in here later is correct without knowing any of this exists.
 *
 * `.readout` is only included above the bench breakpoint: below it the wrapper
 * dissolves (`display: contents`) and the readout flows onto the PAGE surface,
 * where it should follow the page's theme like everything else around it. */
.stage, .stage-gate {
  --s0: #06080a;
  --s1: #0b0e10;
  --s2: #12171a;
  --s3: #1a2126;
  --s4: #232c32;
  --ink: #eef1f3;
  --ink-2: #a7b0b6;
  --ink-3: #78838a;
  --ink-4: #4d565c;
  --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: var(--ink);
}
@media (min-width: 1081px) {
  .readout {
    --s0: #06080a;
    --s1: #0b0e10;
    --s2: #12171a;
    --s3: #1a2126;
    --s4: #232c32;
    --ink: #eef1f3;
    --ink-2: #a7b0b6;
    --ink-3: #78838a;
    --ink-4: #4d565c;
    --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: var(--ink);
  }
}

/* The masthead and the tally live in site.css — both pages carry them, and one
   copy is what stops /about and /assets drifting into two different products.
   The only thing the archive adds is the accent: on this page the highlighted
   word in the heading takes the section light rather than the fixed gold. */
.masthead h1 em { color: var(--lamp-ink); }

/* --------------------------------------------------------------------------
   The spine
   --------------------------------------------------------------------------
   Replaces the "Jump to" chip rail, which is a landing-page component and read
   as one. This is a tool rail: a thin fixed column of section marks down the
   left edge that fills as you pass each one. It is navigation AND a position
   readout, which the chip rail was not.

   Hidden below the bench breakpoint — on a phone it would be a second sticky
   element competing with the bench for the same edge, and the bench wins. */
.spine {
  position: fixed;
  top: 50%;
  left: 18px;
  z-index: 25;
  display: none;
  flex-direction: column;
  gap: 2px;
  transform: translateY(-50%);
}
.spine a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 0;
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--fast) var(--ease);
}
.spine a::before {
  content: '';
  width: 14px;
  height: 1px;
  background: currentColor;
  transition: width var(--med) var(--ease), background var(--fast) var(--ease);
}
.spine a:hover { color: var(--ink-2); }
/* `aria-current` rather than a class: js/archive/index.js sets the attribute, so
   the picture and the accessibility tree cannot drift apart. */
.spine a[aria-current='true'] { color: var(--ink); }
.spine a[aria-current='true']::before { width: 26px; background: var(--lit-ink); }
/* THE SPINE ONLY APPEARS WHEN THE GUTTER CAN ACTUALLY HOLD IT.
 *
 * It is `position: fixed` at the left edge, so it does not participate in
 * layout and nothing pushes it out of the way — it simply draws on top of
 * whatever is there. The content column is `.wrap`, centred at
 * `min(1400px, 100% - clamp(28px, 6vw, 96px))`, so the free gutter beside it is
 * a function of the viewport and the 1400px cap.
 *
 * At 1440px that gutter is 43px, and the labelled rail needs about 100px. The
 * rail drew straight through the masthead's opening paragraph — every numeric
 * check passed, because nothing overflowed and nothing was unreadable on its
 * own; it was only visible in a screenshot.
 *
 * So the two states are gated on the width at which each actually fits:
 *   1180px   ticks only — 16px of rail into a 35px gutter
 *   1640px   ticks and labels — ~100px of rail into a 120px gutter
 * Below 1180 there is no gutter at all and the bench owns the left edge. */
@media (min-width: 1180px) {
  .spine {
    display: flex;
    /* ANCHORED TO THE CONTENT COLUMN, NOT TO THE VIEWPORT EDGE.
       Pinning it at `left: 12px` and picking breakpoints where that looked safe
       is guesswork, and the guess was wrong twice: the gutter beside `.wrap` is
       `(100% - min(1400px, 100% - clamp(28px, 6vw, 96px))) / 2`, which shrinks
       as the viewport grows toward the 1400px cap and then grows again after it.
       There is no single width where "12px is fine" holds.
       So the rail is anchored by its RIGHT edge to that same expression, 14px
       clear of the text. It cannot overlap at any width, because it is measured
       from the thing it must not overlap. */
    left: auto;
    right: calc(50% + min(700px, (100% - clamp(28px, 6vw, 96px)) / 2) + 14px);
    align-items: flex-end;
  }
  /* Ticks only. `gap` goes too — at `font-size: 0` the label is zero-width but
     the flex gap is not, and that 10px was most of the overlap. */
  .spine a { gap: 0; font-size: 0; letter-spacing: 0; }
  .spine a::before { width: 16px; }
  .spine a[aria-current='true']::before { width: 24px; }
}
/* Labels once the gutter is genuinely wide enough to read one in. */
@media (min-width: 1640px) {
  .spine a { gap: 10px; font-size: 10px; letter-spacing: 1.5px; }
  .spine a::before { width: 14px; }
  .spine a[aria-current='true']::before { width: 26px; }
}

/* --------------------------------------------------------------------------
   A section, and which light it is under
   -------------------------------------------------------------------------- */
.act-section { padding: clamp(56px, 7vw, 108px) 0; border-bottom: 1px solid var(--rule); }
.act-section[data-light='meter'] {
  --lit: var(--meter);
  --lit-ink: var(--meter-ink);
  --lit-dim: var(--meter-dim);
  --lit-edge: var(--meter-edge);
}

/* The section title. One word, at size, with the count beside it — no kicker
   above it and no number in front of it. */
.act-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px 28px;
  margin-bottom: clamp(24px, 3vw, 40px);
  padding-bottom: 18px;
  border-bottom: 1px solid var(--rule);
}
.act-head h2 {
  font: 400 clamp(32px, 4.6vw, 62px)/0.94 var(--display);
  letter-spacing: -0.01em;
  text-transform: uppercase;
}
.act-head p {
  max-width: 54ch;
  color: var(--ink-3);
  font-size: 13.5px;
  line-height: 1.65;
  text-wrap: pretty;
}
.act-head kbd { font-size: 10px; padding: 3px 5px; }

/* --------------------------------------------------------------------------
   THE BENCH
   --------------------------------------------------------------------------
   Index left, bench right, on desktop. The bench is DOM-first so that a phone —
   which gets the natural source order — pins it above the index without any
   `order` juggling, and so a screen reader meets the thing being described
   before the list that changes it. */
.bench-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  gap: clamp(20px, 2.6vw, 40px);
  align-items: start;
}
.bench-layout > .bench { grid-column: 2; grid-row: 1; }
.bench-layout > .index { grid-column: 1; grid-row: 1; }

/* THE STAGE HAS A FLOOR, AND THE READOUT HAS A CEILING.
 *
 * This was `minmax(0, 1fr) auto` inside a fixed-height slab with
 * `overflow: hidden`, and the `auto` row wins that negotiation outright. The
 * operator readout is nineteen state chips, eight weapon chips and six action
 * buttons — 836px of natural height against a 653px slab — so the `1fr` row
 * resolved to ZERO and the stage was 621x0. The camera took an aspect ratio of
 * 551, 65,114 triangles were drawn every frame, and not one pixel of them was on
 * screen. Nothing threw and nothing logged: an empty bench that is working
 * perfectly, which is the worst shape a bug can take.
 *
 * Two changes, and both are needed. `minmax(280px, 1fr)` gives the picture a
 * floor that the readout cannot argue with. The cap on `.readout` stops it
 * demanding the room in the first place, so the two are not fighting over a slab
 * neither can win — the controls scroll instead, which is the correct answer for
 * a list of chips.
 *
 * `max-height: 50%` does NOT work here and was tried: a percentage height
 * against a grid `auto` row is circular, so it is silently ignored. It has to be
 * a length or a viewport unit.
 *
 * Fixed on `.bench` rather than in the module that hit it, because all three
 * benches have this shape — the weapon readout is one finish row away from the
 * same collapse, and it would present identically. */
.bench {
  position: sticky;
  top: var(--bench-top);
  display: grid;
  grid-template-rows: minmax(280px, 1fr) auto;
  height: min(76vh, 700px);
  min-height: 460px;
  overflow: hidden;
  border: 1px solid var(--rule);
  background: linear-gradient(168deg, #131a1e 0%, #080b0d 62%);
}
/* Only on the slab layout. Below the bench breakpoint `.bench` is
   `display: contents`, the readout is an ordinary block in the page flow with
   nothing to overflow, and a cap there would clip the controls for no reason. */
@media (min-width: 1081px) {
  .readout {
    max-height: min(38vh, 350px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* The lamp. A single warm source at the top-right of the bench, which is the
   one thing that makes a flat dark rectangle read as a lit surface instead of a
   dark box. It re-points with the section's light. */
.stage::after {
  content: '';
  position: absolute;
  inset: -30% -20% 40% 10%;
  pointer-events: none;
  background: radial-gradient(60% 55% at 74% 12%,
    color-mix(in srgb, var(--lit) 16%, transparent), transparent 68%);
}

.stage {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  /* One-finger drag must turn the model, not scroll the page past it. The page
     stays scrollable everywhere else, and js/archive/stage.js deliberately
     leaves a bare wheel alone so a desktop reader is never trapped either. */
  touch-action: none;
  cursor: grab;
}
.stage.is-grabbing { cursor: grabbing; }
.stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.stage:focus-visible { outline: 2px solid var(--lit); outline-offset: -3px; }

/* THE RULE, AND WHY IT IS ALLOWED TO BE A GRID.
   A two-axis gradient grid as background texture is decoration and is banned on
   this page. This one is a measuring scale: fixed 40px divisions with ticks down
   the left edge, against which the model's real dimensions are printed in the
   readout below. It is the bench's steel rule, and it earns its lines by having
   a number attached to them. */
.stage-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.42;
  background-image:
    linear-gradient(rgba(238, 241, 243, 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(238, 241, 243, 0.05) 1px, transparent 1px);
  background-size: 40px 40px;
  -webkit-mask-image: radial-gradient(76% 66% at 50% 52%, #000 12%, transparent 82%);
  mask-image: radial-gradient(76% 66% at 50% 52%, #000 12%, transparent 82%);
}
.stage-grid::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 9px;
  background: repeating-linear-gradient(to bottom,
    rgba(238, 241, 243, 0.34) 0 1px, transparent 1px 20px);
}

.stage-status {
  position: absolute;
  top: 13px;
  left: 16px;
  z-index: 2;
  max-width: calc(100% - 32px);
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.4px;
  pointer-events: none;
}
.stage-status.is-hot { color: var(--lit); }

.stage-hint {
  position: absolute;
  right: 14px;
  bottom: 12px;
  z-index: 2;
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  pointer-events: none;
}
.stage-hint kbd { min-width: 0; padding: 2px 5px; font-size: 9px; }
/* There is no CTRL key and no wheel on a touchscreen, so the desktop hint is
   simply false there. Replaced, not supplemented. */
@media (pointer: coarse) {
  .stage-hint { font-size: 0; letter-spacing: 0; }
  .stage-hint::after { content: 'Drag to turn · pinch to zoom'; font-size: 10px; letter-spacing: 1.1px; }
  .stage-hint kbd { display: none; }
}

/* The curtain over the operator bench: the models are megabytes and nothing
   downloads until this is pressed. */
.stage-gate {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 8px;
  padding: 26px;
  text-align: center;
  background: rgba(6, 8, 10, 0.9);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  transition: opacity var(--med) var(--ease), visibility 0s linear var(--med);
}
.stage-gate.is-hidden { opacity: 0; visibility: hidden; pointer-events: none; }
.stage-gate-title {
  font: 400 clamp(19px, 2.1vw, 26px)/1.15 var(--display);
  letter-spacing: -0.005em;
  text-transform: uppercase;
  text-wrap: balance;
}
.stage-gate-note { max-width: 38ch; color: var(--ink-3); font-size: 12.5px; line-height: 1.6; }
.stage-gate .act { margin-top: 10px; }

/* --------------------------------------------------------------------------
   The readout — the bench's lower half
   --------------------------------------------------------------------------
   Named, measured, and operable. This is the part that makes the bench a bench
   rather than a picture frame. */
.readout {
  display: grid;
  gap: 14px;
  padding: 16px clamp(16px, 1.6vw, 22px) clamp(16px, 1.6vw, 20px);
  border-top: 1px solid var(--rule);
  background: rgba(6, 8, 10, 0.72);
}
.readout-id { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.readout-id h3 {
  min-width: 0;
  overflow-wrap: anywhere;
  font: 400 clamp(24px, 2.4vw, 34px)/0.96 var(--display);
  letter-spacing: -0.005em;
  text-transform: uppercase;
}
.stage-kicker {
  flex: 0 0 auto;
  color: var(--lit);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.stage-sub { color: var(--ink-3); font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.3px; }
.stage-note { color: var(--ink-3); font-size: 12px; line-height: 1.55; }
.stage-note:empty, .stage-sub:empty { display: none; }

/* Measurements. Monospaced and tabular so a column of figures is a column. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 1px;
  margin: 0;
  background: var(--rule);
  border: 1px solid var(--rule);
}
.stat-grid > div { padding: 9px 11px 10px; background: var(--s0); }
.stat-grid dt {
  color: var(--ink-3);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}
.stat-grid dd {
  margin: 5px 0 0;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}

.control-block { display: grid; gap: 8px; }
.chip-row { display: flex; flex-wrap: wrap; gap: 5px; }
.chip-row-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 5px; }
.chip-row-lead { margin-bottom: clamp(20px, 2.4vw, 30px); }
.act-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 5px; }
.act-row .act-lead { grid-column: 1 / -1; }
.field-label {
  color: var(--ink-3);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   Controls
   -------------------------------------------------------------------------- */
.chip {
  appearance: none;
  min-height: 30px;
  padding: 6px 10px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease),
    background var(--fast) var(--ease);
}
.chip:hover { color: var(--ink); border-color: var(--rule-strong); }
.chip[aria-pressed='true'], .chip.on {
  border-color: var(--lit-edge);
  background: var(--lit-dim);
  color: var(--ink);
}
/* A captured-motion state is a fact about the clip, so it is marked with a
   glyph rather than a colour — colour on this page already means "selected". */
.chip.is-mocap::after { content: '\00a0•'; color: var(--lit); }

.act {
  appearance: none;
  min-height: 40px;
  padding: 9px 11px;
  border: 1px solid var(--rule);
  background: var(--s2);
  color: var(--ink-2);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease),
    background var(--fast) var(--ease);
}
.act:hover { color: var(--ink); border-color: var(--rule-strong); background: var(--s3); }
.act:active { background: var(--s4); }
.act.live { border-color: var(--lit-edge); background: var(--lit-dim); color: var(--ink); }
.act:disabled { opacity: 0.38; cursor: not-allowed; }
/* The one filled control per bench — the single action the section wants. */
.act-lead { border-color: transparent; background: var(--lit); color: #14100a; }
.act-lead:hover { border-color: transparent; background: color-mix(in srgb, var(--lit) 84%, #fff); color: #14100a; }

.search {
  width: 100%;
  height: 38px;
  padding: 0 12px;
  border: 0;
  border-bottom: 1px solid var(--rule-strong);
  border-radius: 0;
  background: var(--s2);
  color: var(--ink);
  font-size: 14px;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.search::placeholder { color: var(--ink-3); }
.search:hover { background: var(--s3); }
.search:focus { outline: none; background: var(--s3); border-bottom-color: var(--lit); }
.search:focus-visible { outline: 2px solid var(--lit); outline-offset: 1px; }

/* --------------------------------------------------------------------------
   The index — the catalogue beside the bench
   -------------------------------------------------------------------------- */
.index { display: grid; gap: 14px; align-content: start; }
.index-controls { display: grid; gap: 12px; }
.index-count {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
}
.index-count strong { color: var(--ink-2); font-weight: 400; }

.rail-list { display: grid; }
.rail-list:focus-visible { outline: 2px solid var(--lit); outline-offset: 2px; }
.rail-group {
  margin: 18px 0 0;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--rule);
  color: var(--lit-ink);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.8px;
  text-transform: uppercase;
}
.rail-group:first-child { margin-top: 0; }

/* A catalogue row. The selected one is marked by its SURFACE and by its
   typography, never by an edge stripe — a coloured left border over 1px is the
   template tell this page exists to avoid. */
.rail-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 12px;
  width: 100%;
  padding: 11px 12px;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  text-align: left;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.rail-item:hover { background: var(--s2); }
.rail-item b {
  min-width: 0;
  overflow: hidden;
  color: var(--ink-2);
  font: 400 16px/1.05 var(--display);
  letter-spacing: 0;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color var(--fast) var(--ease);
}
.rail-item span {
  flex: 0 0 auto;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2px;
  text-transform: uppercase;
  white-space: nowrap;
}
.rail-item.is-active { background: var(--lit-dim); }
.rail-item.is-active b { color: var(--ink); }
.rail-item.is-active span { color: var(--lit-ink); }

.rail-empty { padding: 30px 12px; color: var(--ink-3); font-size: 13px; }

/* --------------------------------------------------------------------------
   MEDALS — a ladder, because thresholds are ordered
   --------------------------------------------------------------------------
   Nine multikills from 2 kills to 10; six sprees from 5 to 30. A grid of
   identical cards throws that ordering away and is the most template-shaped
   component on the internet. The rungs climb, the artwork grows with the
   threshold, and a hairline connector runs behind them so the sequence is
   legible as a sequence. */
.ladder { display: grid; }
.ladder-group { margin-top: 34px; }
.ladder-group:first-child { margin-top: 0; }
.ladder-group > h3 {
  margin-bottom: 4px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--rule);
  font: 400 20px/1 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
}
.ladder-group > h3 small {
  margin-left: 10px;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.3px;
}

.rung {
  position: relative;
  display: grid;
  grid-template-columns: 62px 64px minmax(0, 1fr) auto;
  align-items: center;
  gap: 16px;
  padding: 10px 12px 10px 0;
  border-bottom: 1px solid var(--rule);
  transition: background var(--fast) var(--ease);
}
.rung:hover { background: var(--s1); }
/* The connector, drawn behind the artwork column. Not on the last rung — a line
   leaving the final step implies a step that is not there. */
.rung::before {
  content: '';
  position: absolute;
  top: 50%;
  bottom: -50%;
  left: 94px;
  width: 1px;
  background: var(--rule);
}
.rung:last-child::before { display: none; }

.rung-at {
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
.rung-at b { display: block; color: var(--lit-ink); font-size: 17px; font-weight: 400; }

/* The art grows with the threshold. `--step` is 0..1, set inline by
   js/archive/sounds.js from the medal's own position in its ladder, so the
   growth is driven by the data rather than by an nth-child guess. */
.medal-art {
  position: relative;
  z-index: 1;
  width: calc(38px + 20px * var(--step, 0));
  height: auto;
  justify-self: center;
  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.6));
}

.rung-body { min-width: 0; }
.rung-body h4 {
  font: 400 18px/1.05 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
}
.medal-trigger { margin-top: 4px; color: var(--ink-3); font-size: 12px; line-height: 1.5; text-wrap: pretty; }
.medal-cat { display: none; }

.medal-vo {
  min-height: 34px;
  padding: 7px 12px;
  font-size: 10.5px;
  white-space: nowrap;
}
.medal-silent {
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   SOUND — drawn, not listed
   --------------------------------------------------------------------------
   The sibling project lists 965 recordings as 965 buttons. This one draws them:
   a row's waveform is its REAL decoded peak envelope, computed once on first
   play and cached, so the library fills in with truth as you explore it. A
   synthesised waveform keyed off a filename hash would look identical and be a
   lie, and it is the kind of lie a person eventually catches. */
.sound-controls {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 16px;
  align-items: end;
  margin-bottom: clamp(20px, 2.4vw, 30px);
}
.sound-search { display: block; max-width: 380px; }
.sound-count {
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
}

.sound-groups { display: grid; }
.sound-group {
  border-bottom: 1px solid var(--rule);
  /* Collapsed groups skip layout entirely. With 965 recordings this is the
     difference between a page that scrolls smoothly on a phone and one that
     stutters, and it costs one declaration. */
  content-visibility: auto;
  contain-intrinsic-size: auto 120px;
}
.sound-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  padding: 16px 2px;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
}
.sound-group-head h3 {
  font: 400 21px/1 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
  transition: color var(--fast) var(--ease);
}
.sound-group-head:hover h3 { color: var(--meter-ink); }
.sound-group-count {
  flex: 0 0 auto;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.3px;
}
.sound-group-head[aria-expanded]::after {
  content: '';
  align-self: center;
  width: 7px;
  height: 7px;
  border-right: 1px solid var(--ink-3);
  border-bottom: 1px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--med) var(--ease);
}
.sound-group-head[aria-expanded='true']::after { transform: rotate(225deg) translate(-2px, -2px); }

.sound-sub {
  margin: 0;
  padding: 14px 0 6px;
  color: var(--meter-ink);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.8px;
  text-transform: uppercase;
}
.sound-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1px 20px;
  padding-bottom: 20px;
}
.sound-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; align-items: center; }

.sound {
  position: relative;
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr);
  align-items: center;
  gap: 10px;
  min-width: 0;
  min-height: 42px;
  padding: 7px 0;
  border: 0;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  text-align: left;
  cursor: pointer;
  transition: color var(--fast) var(--ease);
}
/* The play mark: a triangle that becomes two bars. CSS, because this glyph
   appears several hundred times and an icon request per row is absurd. */
.sound::before {
  content: '';
  width: 0;
  height: 0;
  border-top: 4.5px solid transparent;
  border-bottom: 4.5px solid transparent;
  border-left: 7px solid var(--ink-4);
  transition: border-color var(--fast) var(--ease);
}
.sound:hover::before { border-left-color: var(--ink-2); }
.sound-name {
  display: block;
  overflow: hidden;
  color: var(--ink-2);
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color var(--fast) var(--ease);
}
.sound:hover .sound-name { color: var(--ink); }
.sound-key {
  display: block;
  margin-top: 2px;
  overflow: hidden;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 9.5px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sound.is-playing .sound-name { color: var(--meter-ink); }
.sound.is-playing::before {
  width: 7px;
  height: 9px;
  border: 0;
  border-left: 2.5px solid var(--meter-ink);
  border-right: 2.5px solid var(--meter-ink);
}

/* The envelope. Painted by js/archive/sounds.js into a canvas once the file has
   been decoded; absent until then, which is honest — a row that has never been
   played has no waveform to show and draws nothing rather than inventing one. */
.sound-wave {
  grid-column: 2;
  width: 100%;
  height: 16px;
  margin-top: 3px;
  opacity: 0.55;
  transition: opacity var(--fast) var(--ease);
}
.sound:hover .sound-wave, .sound.is-playing .sound-wave { opacity: 1; }

.sound-takes { display: flex; gap: 2px; }
.take {
  width: 22px;
  min-height: 22px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--ink-3);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.take:hover { color: var(--ink); border-color: var(--rule-strong); }
.take.is-playing { border-color: var(--meter-edge); background: var(--meter-dim); color: var(--meter-ink); }

/* --------------------------------------------------------------------------
   The playhead
   --------------------------------------------------------------------------
   NOT a ghost card: no 1px border under a wide soft shadow. It is an overlay
   floating above the page, so it gets the shadow and nothing else. */
/* `[hidden]` MUST BE RESTORED BY HAND HERE, AND IT IS NOT OPTIONAL.
 *
 * The UA sheet's `[hidden] { display: none }` has specificity 0,0,1. `.dock`
 * has 0,1,0 and sets `display: grid`, so the class wins and the attribute does
 * nothing — the markup ships `hidden`, the element is laid out anyway, and the
 * only reason it is not on screen is the opacity and the off-screen transform
 * below.
 *
 * Which means it looks fine and is not. Before a single sound has played, a
 * screen reader finds a "Now playing" landmark containing a pause button, a
 * scrubber and the words "Nothing playing", and a keyboard user tabs through all
 * three on the way down the page. `visibility: hidden` would not fix it either:
 * the element would still occupy its box.
 *
 * This is the same trap that hid a medal row for the ladder — worth knowing that
 * ANY class setting `display` silently disarms `hidden` on the same element. */
.dock[hidden] { display: none; }

.dock {
  position: fixed;
  z-index: 40;
  left: 50%;
  bottom: 16px;
  display: grid;
  grid-template-columns: auto minmax(120px, 1fr) auto auto;
  align-items: center;
  gap: 14px;
  width: min(740px, calc(100% - 24px));
  padding: 10px 12px;
  background: rgba(8, 11, 13, 0.96);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  box-shadow: 0 16px 44px rgba(0, 0, 0, 0.72), 0 0 0 1px rgba(125, 176, 255, 0.16);
  transform: translate(-50%, calc(100% + 34px));
  opacity: 0;
  transition: transform var(--med) var(--ease), opacity var(--med) var(--ease);
}
.dock.is-up { transform: translate(-50%, 0); opacity: 1; }

.dock-toggle {
  position: relative;
  width: 34px;
  height: 34px;
  border: 1px solid var(--meter-edge);
  background: var(--meter-dim);
  cursor: pointer;
}
.dock-toggle::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  border-top: 5.5px solid transparent;
  border-bottom: 5.5px solid transparent;
  border-left: 8px solid var(--meter);
  transform: translate(-38%, -50%);
}
.dock[data-state='playing'] .dock-toggle::before {
  width: 9px;
  height: 11px;
  border: 0;
  border-left: 2.5px solid var(--meter);
  border-right: 2.5px solid var(--meter);
  transform: translate(-50%, -50%);
}

.dock-meta { position: relative; min-width: 0; }
/* The live spectrum, behind the title. A real AnalyserNode reading the sound
   that is playing right now — the reason the dock feels connected to the audio
   instead of merely describing it. */
.dock-scope {
  position: absolute;
  inset: -4px -2px auto;
  width: 100%;
  height: 22px;
  opacity: 0.4;
  pointer-events: none;
  /* js/archive/dock.js strokes the trace with `getComputedStyle(canvas).color`,
     so the trace's colour is set HERE rather than as a literal in the module —
     one place decides it and the theme can move it.
     Without this declaration `color` inherits from `.dock`, which is `--ink`:
     the scope drew in body-text white, and in light mode would have drawn in
     near-black. It is the meter's signal, so it is the meter's colour. */
  color: var(--meter);
}
.dock-meta strong {
  position: relative;
  display: block;
  overflow: hidden;
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dock-bar { position: relative; display: flex; align-items: center; gap: 8px; margin-top: 5px; }
.dock-bar span {
  flex: 0 0 auto;
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}
.dock-bar input { min-width: 0; flex: 1; height: 3px; accent-color: var(--meter); cursor: pointer; }
.dock-kind {
  color: var(--ink-3);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
}
.dock-close {
  width: 28px;
  height: 28px;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--ink-3);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.dock-close:hover { color: var(--ink); border-color: var(--rule-strong); }

/* A section whose module refused to start says so rather than sitting empty. */
[data-archive-error='true'] .bench-layout, [data-archive-error='true'] .ladder { opacity: 0.5; }

/* --------------------------------------------------------------------------
   Responsive
   --------------------------------------------------------------------------
   The bench does not degrade into a stack — it MOVES to the top and stays
   sticky, which is the same idea at a different aspect ratio. That is what
   makes this page genuinely usable on a phone.

   1080px is where the two-column bench stops working: below it the index column
   is too narrow for a weapon name and the bench too narrow to frame a rifle. */
@media (max-width: 1080px) {
  .bench-layout { grid-template-columns: 1fr; gap: 0; }

  /* THE WRAPPER DISSOLVES, AND ONLY THE PICTURE STAYS PINNED.
     On desktop the stage and its readout are one sticky slab. On a phone that
     slab would be most of the screen and the index would have nothing left, so
     `display: contents` removes the wrapper from layout entirely: the stage
     becomes a direct grid child and is the only thing that sticks, while the
     readout and the index scroll under it normally.

     This is why the phone layout is the same idea rather than a fallback — the
     object still holds a fixed region of the screen while you browse. Doing it
     by re-ordering the DOM at a breakpoint would have needed two markup
     structures; one property does it and keeps the source order that a screen
     reader and a keyboard already follow. */
  .bench { display: contents; }
  .stage {
    position: sticky;
    /* Above the index and the page background, but BELOW the nav (30) and the
       playhead (40) — a bench that covered the dock would hide the stop button
       behind the thing making the noise. */
    z-index: 5;
    top: var(--nav-h);
    height: min(46vh, 380px);
    min-height: 220px;
    /* Full bleed. The model gets the whole width, and the hairline borders that
       framed it on desktop become the horizontal rules above and below. */
    margin-inline: calc(50% - 50vw);
    border-block: 1px solid var(--rule);
    background: linear-gradient(168deg, #131a1e 0%, #080b0d 62%);
  }
  .readout {
    padding: 14px 0 18px;
    border-top: 0;
    border-bottom: 1px solid var(--rule);
    background: transparent;
  }
  /* The readout is on the PAGE surface here, not on the dark bench, so its one
     accent has to switch to the readable token with it. Missing this leaves the
     weapon's class label as bright gold on near-white — 2.1:1, and the only
     thing on the phone layout that would be unreadable in light mode. */
  .readout .stage-kicker { color: var(--lit-ink); }
  .readout-id h3 { font-size: clamp(22px, 6vw, 30px); }
  .index { margin-top: 18px; }
  .stat-grid { grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); }
}

@media (max-width: 860px) {
  .sound-controls { grid-template-columns: 1fr; gap: 10px; }
  .sound-search { max-width: none; }
  .sound-list { grid-template-columns: 1fr; }
  .rung { grid-template-columns: 46px 52px minmax(0, 1fr); gap: 12px; }
  .rung::before { left: 72px; }
  /* The play button drops to its own line rather than squeezing the name. */
  .rung .medal-vo, .rung .medal-silent { grid-column: 2 / -1; justify-self: start; margin-top: 2px; }
}

@media (max-width: 560px) {
  .stage { height: min(42vh, 320px); min-height: 200px; }
  .tally > div { flex-basis: 44%; }
  .rung { grid-template-columns: 40px 44px minmax(0, 1fr); gap: 10px; padding-right: 0; }
  .rung::before { left: 62px; }
  .rung-at b { font-size: 15px; }
  /* The dock keeps its scrubber and loses its labels — being able to seek
     matters more than knowing the category. */
  .dock { grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; bottom: 8px; }
  .dock-kind { display: none; }
  .dock-bar span:last-of-type { display: none; }
}

/* Short landscape phones. A 46vh stage over a 320px-tall viewport leaves the
   index a sliver, so the picture gives up its minimum instead of the list. */
@media (max-height: 560px) and (max-width: 1080px) {
  .stage { height: 56vh; min-height: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .dock { transition: opacity var(--fast) linear; }
}
