/* KillBeat, the site.
   ────────────────────────────────────────────────────────────────────────────
   The shell is dark, the accent is the one color, and
   the unit on every surface is a card with a headline number on it — a `3K`, a
   map name, a length — rather than a row in a table.

   **The palette is the wordmark's and is not a taste choice.**
   `assets/killbeat-wordmark.zip` is the source of truth for the mark, and
   `assets/PROVENANCE.md` records what it is drawn in: red `#E1101B`, deep red
   `#6E0007`, bone `#F4EDEC` on black `#0a0708`. All four are here verbatim —
   `--accent`, the bottom of every accent gradient, `--ink` and `--bg` — and
   the rest of the shell is a warm neutral ramp built off that black, so the
   site and a clip's intro stinger share one palette. **A change to the mark is a change to these four values**, and there
   is one place to make it.

   Three rules that are not taste and must survive a redesign:

   - **Valve's sign-in button is an image and is not restyled** (`.steam-signin`
     is a bare box, `assets/PROVENANCE.md`).
   - **The artist note is on the surfaces that show a *track*** (`.artists`).
     Credit is a license condition, and it is discharged on the end card of the
     mp4 itself. **A clip card that names a track carries the credit with it.**
   - **Everything under "the curation instrument" at the bottom of this file is
     load-bearing** for the render guest's `curate.js`. That page's audio is a
     decoded buffer on one clock (`docs/curation.md`, "There is one clock").
     Restyle it only with the page open in front of you. */

:root {
  color-scheme: dark;
  --bg: #0a0708;
  --bg-2: #100c0c;
  --panel: #171213;
  --panel-2: #1e1819;
  --line: #2a2223;
  --line-2: #3a2f30;
  --ink: #f4edec;
  --dim: #9c908e;
  --dim-2: #7a6d6c;
  /* The mark's own red. Used for fills, marks, borders and gradients — the
     places a color is a *shape*. */
  --accent: #e1101b;
  /* The same red lifted until it is legible as text on this background.
     **Both exist because the mark's red is not a text color**: `#e1101b` on
     `#0a0708` is 3.2:1, which is fine under a heading and wrong for a link.
     This is the same hue at 5.6:1. Anything that sets `color:` reaches for
     this one; anything that fills reaches for `--accent`. */
  --accent-text: #f04c54;
  /* The mark's deep red, verbatim. The bottom of the accent gradient and the
     dim end of anything built out of the accent. */
  --accent-dim: #6e0007;
  --gold: #f2b33d;
  --ok: #4fd18b;
  /* Informational rather than good/bad: the alternate-drop swap, which is a
     detector's answer being tried rather than a curator's judgement. */
  --info: #4a8fe7;
  --warn: #e6b23c;
  /* The three tiers, on the palette's own three non-red hues: pistol is the
     info blue, rifle the gold, AWP the green — the AWP's own colour. Keyed by
     `data-tier` wherever a tier is drawn, so the wire word is the only thing
     that knows which rung it is. */
  --tier-pistol: #4a8fe7;
  --tier-rifle: #f2b33d;
  --tier-awp: #4fd18b;
  --radius: 10px;
  --shadow: 0 1px 0 rgba(255,255,255,.03) inset, 0 8px 24px rgba(0,0,0,.35);
}
* { box-sizing: border-box; }
/* Every page here draws itself by unhiding one of two or three sections, so a
   component rule that sets `display` would otherwise beat the `hidden`
   attribute and show a signed-out visitor the signed-in half of the bar. It is
   not a tidiness rule.

   It is load-bearing twice over, because **a menu panel's open state IS the
   `hidden` attribute** — a component rule that set `display` on it would leave
   every menu on the site open for good. */
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background:
    radial-gradient(1100px 520px at 50% -220px, #241d1e 0%, rgba(30,24,25,0) 70%),
    var(--bg);
  background-attachment: fixed;
  color: var(--ink);
  font: 15px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent-text); }
a:hover { color: #ef3b47; }
code {
  background: var(--panel); padding: .1em .35em; border-radius: 4px;
  font-size: .9em; color: var(--ink);
}
h2 {
  font-size: .82rem; text-transform: uppercase; letter-spacing: .14em;
  color: var(--dim); margin: 2.6rem 0 .9rem; font-weight: 700;
}
h2:first-child { margin-top: 0; }
.note, small { color: var(--dim); font-size: .86rem; display: block; }
.note a { color: var(--dim); }
.note a:hover { color: var(--accent-text); }
.lede { font-size: 1.06rem; max-width: 46rem; color: #d8ccca; }
.error { color: var(--accent-text); }
.muted { color: var(--dim); }

/* ── Header, the one bar ────────────────────────────────────────────────────
   **The whole navigation of the site, and there is no second strip under it.**
   `docs/contract.md` §8, "One bar, and nothing under it". Two `nav.topbar`
   groups: where you go on the left, who you are on the right, pushed apart by
   `.end`'s auto margin.

   Which entries exist is decided by `topbar.js` from /api/v1/me — the gate for
   Clips and Moments, a session for the username menu, and one *Manage* entry
   for any operator. **A courtesy, never a gate**: every route behind them is
   checked server-side. */
header.site {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .85rem 1.5rem;
  border-bottom: 1px solid var(--line);
  background: rgba(10,7,8,.82);
  backdrop-filter: blur(10px);
  position: sticky; top: 0; z-index: 20;
}
/* The lockup is the whole of it: no text to size, space or colour. What is left
   is the box it sits in and the row it sits on. */
.wordmark {
  display: inline-flex; align-items: center;
  text-decoration: none;
  margin: 0; flex: 0 0 auto;
}
/* **The wordmark is the mark and the name, and nothing else.** The bar marks
   the current page (`a.here`), so a page name beside the wordmark would say it
   twice. */
/* **The bar draws the whole lockup**, not the K beside the name set in the
   site's font — `tools/marks.py` writes `wordmark.svg` from
   `stinger.WORD_POINTS`, the same geometry the clip's stinger is drawn from, so
   the bar, the tab icon, the avatars and the end card are one shape with one
   source. The name is therefore drawn rather than typeset, and the markup
   carries an `aria-label` because there is no longer any text to read.

   It is a MASK rather than an `<img>` or a background image, for two reasons
   that are not stylistic: the span in every page's markup stays an empty span,
   and a mask takes the fill below, so the bar recolours with `--accent` the way
   everything else here does. The glow has to be `drop-shadow` and not
   `box-shadow` for the same reason a mask makes it possible at all:
   `box-shadow` traces the element's box, which is a wide rectangle, and would
   put a red halo around a shape that is not there.

   A browser with no mask support draws the box, a red bar, which is not a
   broken state. */
.wordmark .kb-lockup {
  /* **The lockup's own proportions**, so the letterforms are never squashed:
     this is `wordmark.svg`'s viewBox, which frames the whole inked mark, spikes
     and crossbar included, so the mask shows all of it. Height rather than
     width, because what has to line up in the bar is the row; the value carries
     the cap the old box did while the box now also holds the overshoot. */
  height: 1.587rem; aspect-ratio: 903.671 / 178.703;
  /* Flat `--accent`, not the accent gradient, and for the reason `tools/marks.py`
     gives for every other rendering of this mark: the gradient bottoms out at
     `#6e0007`, which is 1.3:1 on the bar's own black, so the foot of the upright
     and the tail of the lower blade sink into the background. It is a letterform
     and every part of it has to read. */
  background: var(--accent);
  -webkit-mask: url(/static/icons/wordmark.svg) center / contain no-repeat;
  mask: url(/static/icons/wordmark.svg) center / contain no-repeat;
  filter: drop-shadow(0 0 9px rgba(225,16,27,.45));
}
/* **The beta tag, and it is deliberately not part of the mark.** The site goes
   into a beta and comes out of one; a wordmark with BETA baked into it is an
   asset that has to be re-cut to end the beta, and `assets/killbeat-wordmark.zip`
   is the source of truth for what the mark IS. So this is a sibling of
   `a.wordmark` that `topbar.js` injects, it owns no geometry, and switching the
   beta off deletes an element rather than editing a file in `assets/`.

   Two things here are doing work rather than decorating:

   `margin-left` cancels most of `header.site`'s 1rem gap. The bar's gap is the
   spacing between the wordmark and a whole nav group, and at that distance a
   tag reads as the first nav entry rather than as a qualifier on the name.
   Pulling it to .4rem is what makes it belong to the wordmark.

   The skew is the mark's stance, not an effect. The wordmark is 21 degrees of
   slant; a tag beside it that stands upright reads as something the page picked
   up somewhere else. It is under half that, because four letters at .62rem
   leaning as hard as the mark stop being readable. Bone on the deep red is the
   pairing this palette does best (`assets/PROVENANCE.md`) and it is the DIM end
   of the accent on purpose, so the tag never competes with the name beside it. */
.beta-badge {
  flex: 0 0 auto; align-self: center;
  margin-left: calc(.4rem - 1rem);
  padding: .12rem .42rem;
  background: var(--accent-dim);
  border: 1px solid var(--accent);
  border-radius: 3px;
  color: var(--ink);
  font-size: .62rem; font-weight: 800; line-height: 1.35;
  letter-spacing: .14em; text-transform: uppercase;
  transform: skewX(-9deg);
  user-select: none;
}
nav.topbar { display: flex; align-items: center; gap: .35rem; flex: 1 1 auto; flex-wrap: wrap; }
/* The right-hand group. `margin-left: auto` rather than a spacer element, so
   the two groups are still one flex row and the bar wraps as one thing. */
nav.topbar.end { flex: 0 0 auto; margin-left: auto; }
nav.topbar a {
  padding: .35rem .7rem; border-radius: 6px; text-decoration: none;
  color: var(--dim); font-size: .84rem; font-weight: 600;
  letter-spacing: .02em; border: 1px solid transparent;
}
nav.topbar a:hover { color: var(--ink); background: var(--panel); }
/* Where you are, marked in the bar. An underline rather than a fill, so it does
   not read as hover. */
nav.topbar a.here { color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }
/* ── An icon in the bar ─────────────────────────────────────────────────────
   The Discord link (`docs/contract.md` §8, "One bar, and nothing under it"). It is a
   `nav.topbar a` like every other entry and inherits the padding, the radius
   and the hover; all this adds is centring the glyph and tightening the sides,
   because a 16px mark inside a word's padding sits in a box twice its width.

   The mark is inline SVG filled with `currentColor`, so it is the same
   `--dim` the words beside it are and lifts to `--ink` on the same hover. */
nav.topbar a.icon-link {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .35rem .5rem;
}
/* ── The rig light ──────────────────────────────────────────────────────────
   One dot in the bar on every page: whether a render worker has been heard
   from recently (`docs/contract.md` §8). Same colour vocabulary as `.state`'s
   dot — `--ok` for heard from, the mark's red for not — and deliberately not
   the same element, because this one carries no words at all and what it says
   lives in its `title`.

   `[hidden] { display: none !important; }` above is load-bearing here as it is
   everywhere else in this bar: the dot is hidden for a signed-out caller and
   whenever the rig cannot be read. */
.rig-light {
  width: .55rem; height: .55rem; border-radius: 50%;
  flex: 0 0 auto; margin-right: .5rem;
  background: var(--dim-2);
  /* Without a ring it reads as a speck of dust on the bar's blurred ground. */
  box-shadow: 0 0 0 1px var(--line);
}
.rig-light.live { background: var(--ok); }
.rig-light.dark { background: var(--accent); }
nav.topbar .menu-panel a.here { color: var(--ink); box-shadow: inset 2px 0 0 var(--accent); }
/* The menus — *Manage*, and the username. Each is one entry in the bar with a
   panel under it, and a second one is one more `.menu` rather than a framework.
   The button is drawn as a top bar link rather than as a button, because that
   is what Manage is: every entry behind it is a real URL. The username menu is
   the same shape, and its sign-out entry is a real button, because signing out
   is a POST. `docs/contract.md` §8.

   The panel's open state is the `hidden` attribute, so `[hidden]` above is
   load-bearing here too. */
nav.topbar .menu { position: relative; }
nav.topbar .menu-open {
  background: none; border: 1px solid transparent; box-shadow: none;
  padding: .35rem .7rem; border-radius: 6px;
  color: var(--dim); font: inherit; font-size: .84rem; font-weight: 600;
  letter-spacing: .02em; cursor: pointer;
}
nav.topbar .menu-open:hover { color: var(--ink); background: var(--panel); filter: none; }
nav.topbar .menu-open[aria-expanded="true"] {
  color: var(--ink); background: var(--panel); border-color: var(--line);
}
nav.topbar .menu-open::after { content: " ▾"; color: var(--dim-2); }
nav.topbar .menu-panel {
  position: absolute; top: calc(100% + .35rem); left: 0; z-index: 30;
  display: flex; flex-direction: column; min-width: 10rem;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 8px; padding: .3rem; box-shadow: var(--shadow);
}
/* The right-hand group's panels hang from the right edge, or a menu sitting
   against the window would open off it. */
nav.topbar.end .menu-panel { left: auto; right: 0; }
nav.topbar .menu-panel a,
nav.topbar .menu-panel button {
  display: block; width: 100%; text-align: left;
}
/* Sign out is an action and not a destination, so it is a button — but it is a
   button in a nav, so it wears the nav's clothes rather than the accent
   gradient every other button on the site wears. */
nav.topbar .menu-panel button.plain {
  background: none; border: 1px solid transparent; box-shadow: none;
  padding: .35rem .7rem; border-radius: 6px;
  color: var(--dim); font: inherit; font-size: .84rem; font-weight: 600;
  letter-spacing: .02em; cursor: pointer;
}
nav.topbar .menu-panel button.plain:hover {
  color: var(--ink); background: var(--bg); filter: none;
}

/* The username is the menu's own button, so it inherits `.menu-open` above and
   only needs to lay its own contents out. */
#who {
  display: inline-flex; align-items: center; gap: .45rem;
  max-width: 12rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
#who .role {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--gold); border: 1px solid rgba(242,179,61,.4);
  border-radius: 999px; padding: .1rem .45rem;
}

/* ── The tier badge ────────────────────────────────────────────────────────
   Beside the username in the bar and beside the name on a profile. A pill in
   the tier's colour with a dot in front: the dot is what reads at a glance
   from the corner of the eye, the word is what reads when looked at. */
.tier {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .1em;
  border-radius: 999px; padding: .1rem .55rem .1rem .45rem;
  color: var(--tier-color, var(--dim));
  border: 1px solid color-mix(in srgb, var(--tier-color, var(--dim)) 45%, transparent);
  background: color-mix(in srgb, var(--tier-color, var(--dim)) 10%, transparent);
  vertical-align: middle;
}
.tier::before {
  content: ""; width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--tier-color, var(--dim));
  box-shadow: 0 0 6px color-mix(in srgb, var(--tier-color, var(--dim)) 70%, transparent);
}
[data-tier="pistol"] { --tier-color: var(--tier-pistol); }
[data-tier="rifle"]  { --tier-color: var(--tier-rifle); }
[data-tier="awp"]    { --tier-color: var(--tier-awp); }
h1 .tier { font-size: .6rem; position: relative; top: -.25em; }

/* ── The tier card on /profile ─────────────────────────────────────────────
   The one place the tier is drawn large: the rung's name in its colour, a
   coloured edge, and the day as three tiles. `data-tier="free"` has no colour
   and falls back to the dim border, which is the point of it. */
.tier-card {
  margin-top: 1.25rem; padding: 1rem 1.25rem 1.1rem;
  border: 1px solid var(--line); border-left: 4px solid var(--tier-color, var(--line-2));
  border-radius: var(--radius); background: var(--panel);
}
.tier-card h3 {
  margin: 0; font-size: .7rem; text-transform: uppercase; letter-spacing: .12em; color: var(--dim);
}
.tier-name {
  margin: .15rem 0 .35rem; font-size: 2rem; font-weight: 800; letter-spacing: .02em;
  color: var(--tier-color, var(--ink));
  text-shadow: 0 0 18px color-mix(in srgb, var(--tier-color, transparent) 35%, transparent);
}
.tier-day { display: flex; flex-wrap: wrap; gap: .6rem; margin: .8rem 0 .4rem; }
.tier-tile {
  flex: 1 1 7rem; min-width: 7rem; padding: .6rem .75rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  display: grid; grid-template-columns: auto 1fr; column-gap: .45rem; align-items: baseline;
}
.tier-tile strong {
  font-size: 1.6rem; font-variant-numeric: tabular-nums; color: var(--tier-color, var(--ink));
}
.tier-tile span { font-size: .78rem; color: var(--dim); }
.tier-tile .what { grid-column: 1 / -1; text-transform: uppercase; letter-spacing: .08em; font-size: .66rem; }
.tier-tile.none { opacity: .45; }
.tier-tile.none strong { color: var(--dim); }

main { max-width: 66rem; margin: 0 auto; padding: 1.75rem 1.5rem 4rem; }

/* ── The stale-link banner ──────────────────────────────────────────────────
   Injected by `topbar.js` under the header, on every page, while `/api/v1/me`
   says `stale_link`. It is a strip and not a toast: it clears by re-linking or
   unlinking and in no other way, so it must survive being seen twice without
   reading as a stuck popup. The accent's dim end rather than the accent — it
   is a standing condition, not an alarm going off. */
.stale-banner {
  margin: 0; padding: .55rem 1.5rem;
  background: var(--accent-dim);
  border-bottom: 1px solid var(--accent);
  color: var(--ink); font-size: .86rem;
}
.stale-banner a { color: var(--ink); font-weight: 700; }
/* The getting-started banner wears the same strip and adds the one thing the
   stale-link banner must never have: a way to close it. Set as quiet text at
   the end of the line, because dismissing is the answer somebody gives once
   and the offer beside it is the point of the strip. */
.banner-dismiss {
  background: none; border: 0; padding: 0 0 0 .5rem; cursor: pointer;
  color: var(--ink); font: inherit; font-size: inherit;
  text-decoration: underline; opacity: .7;
}
.banner-dismiss:hover { opacity: 1; }

/* ── The getting-started steps ──────────────────────────────────────────────
   `/welcome`, and the only ordered list on the site. Numbered because the
   order is real: nothing on the second step can happen before the first. */
ol.steps { max-width: 46rem; padding-left: 1.2rem; }
ol.steps li { margin: 0 0 .9rem; line-height: 1.55; }
ol.steps li::marker { color: var(--accent); font-weight: 700; }

/* ── The profile's tab strip ────────────────────────────────────────────────
   `#profile-tabs`, the owner's three views of their own page. Buttons rather
   than links — nothing here is a route, and the bar above stays the whole
   navigation of the site — marked with the same underline `a.here` carries in
   the bar, so "where you are" is one vocabulary everywhere. */
.page-tabs {
  display: flex; gap: .35rem; margin: .25rem 0 1.4rem;
  border-bottom: 1px solid var(--line);
}
.page-tabs button {
  background: none; border: 0; cursor: pointer;
  padding: .5rem .8rem; color: var(--dim);
  font: inherit; font-size: .9rem; font-weight: 600; letter-spacing: .02em;
}
.page-tabs button:hover { color: var(--ink); }
.page-tabs button.here { color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }

/* ── The settings cards ─────────────────────────────────────────────────────
   `#self-controls` on the profile's Settings tab: one card per concern, in a
   grid that packs two-up where there is room and stacks on a phone. The gap
   is the spacing, so the children's own margins are zeroed — `.tier-card`
   carries a top margin for the places it used to sit under prose. */
.settings-cards {
  display: grid; gap: 1rem; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
}
.settings-cards > * { margin: 0; }
/* The card headings, in the tier card's own voice so the four read as one
   set — the tier card keeps its rule and these match it. */
.settings-cards .panel h3 {
  margin: 0 0 .5rem; font-size: .7rem; text-transform: uppercase;
  letter-spacing: .12em; color: var(--dim);
}
main.prose { max-width: 45rem; }

/* ── Buttons ───────────────────────────────────────────────────────────────── */
/* **Red into deep red with bone on it, which is the wordmark's own three
   colors doing what they do in the mark.** Dark-on-mid-red is the pairing this
   palette is worst at; bone on `--accent` is 6.3:1 and the gradient only
   darkens under it. */
.button, button {
  background: linear-gradient(180deg, var(--accent), #8d0410);
  color: var(--ink); border: 0; border-radius: 7px;
  padding: .52rem .95rem; font: inherit; font-weight: 700; cursor: pointer;
  text-decoration: none; display: inline-block; letter-spacing: .01em;
  transition: filter .15s ease, border-color .15s ease, transform .1s ease;
}
.button:hover, button:hover { filter: brightness(1.12); color: var(--ink); }
/* A press sinks the button while it is held, so it answers before the request
   it sends does. */
.button:active:not(.is-disabled), button:active:not(:disabled) { transform: scale(.97); }
button.plain, .button.plain {
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line-2); font-weight: 600;
}
button.plain:hover, .button.plain:hover { border-color: var(--dim); color: var(--ink); }
button.ghost {
  background: transparent; color: var(--dim); border: 1px solid var(--line);
  font-weight: 600;
}
button:disabled, .button.is-disabled {
  opacity: .5; cursor: default; filter: none;
  background: var(--panel-2); color: var(--dim); border: 1px solid var(--line);
}
button.small, .button.small { padding: .34rem .7rem; font-size: .84rem; }

/* ── Forms ─────────────────────────────────────────────────────────────────── */
form label { display: block; margin: .8rem 0; font-size: .9rem; color: var(--dim); }
input, select, textarea {
  display: block; width: 100%; max-width: 32rem; margin-top: .3rem;
  background: #0c0908; color: var(--ink);
  border: 1px solid var(--line-2); border-radius: 7px;
  padding: .55rem .7rem; font: inherit;
}
input:focus, select:focus, textarea:focus {
  outline: 0; border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(225,16,27,.15);
}
input::placeholder { color: var(--dim-2); }

/* ── Panels and generic cards ──────────────────────────────────────────────── */
.panel {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.1rem 1.2rem; margin: 1rem 0;
}
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1rem; margin: .75rem 0;
}
.card h3 { margin: 0 0 .25rem; font-size: 1rem; }
.card .meta { color: var(--dim); font-size: .85rem; }

/* ── The landing hero ───────────────────────────────────────────────────────
   `#showcase` ships as `.hero-frame.empty` in `index.html`, and `kb.js` drops
   the class when it draws a clip into the frame, so the empty frame has to read
   as deliberate rather than as a broken video. It draws a framed still with the
   pipeline's own three beats written on it. */
.hero {
  display: grid; grid-template-columns: 1.05fr .95fr; gap: 2.5rem;
  align-items: center; margin: 1rem 0 2.5rem;
}
.hero h1 {
  font-size: clamp(2.1rem, 5.5vw, 3.4rem); line-height: 1.02; margin: 0 0 1rem;
  letter-spacing: -.02em; font-weight: 800;
}
.hero h1 em { font-style: normal; color: var(--accent-text); }
.hero .lede { margin: 0 0 1rem; }
/* **The beta shout, over the heading.** A pill rather than a bigger heading:
   the heading is what the product is and this is a fact about today, so it
   reads as a label on the page rather than as the page's own title. Accent on
   the accent's dim end, which is the vocabulary the banners under the bar
   already use for a standing condition. */
.beta-open {
  display: inline-block; margin: 0 0 .7rem; padding: .3rem .7rem;
  border-radius: 999px; background: var(--accent-dim);
  border: 1px solid var(--accent); color: var(--ink);
  font-size: .75rem; font-weight: 800; letter-spacing: .12em;
  text-transform: uppercase;
}
.beta-line { margin: 0 0 1.4rem; max-width: 40rem; font-weight: 600; }
.hero-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }

.hero-frame {
  position: relative; border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line-2); background: var(--panel);
  box-shadow: var(--shadow); aspect-ratio: 16 / 9;
}
.hero-frame video { display: block; width: 100%; height: 100%; object-fit: cover; }
/* An embed fills the frame the same way. `object-fit` means nothing to an
   iframe — it is a document, not a replaced image — so it gets the box and
   YouTube letterboxes inside it, which is what the aspect ratio is for. */
.hero-frame iframe { display: block; width: 100%; height: 100%; border: 0; }
/* **The empty frame grows rather than clipping.** `.hero-frame` is `aspect-ratio`
   plus `overflow: hidden`, which is right for a video and wrong for words: a
   sentence rewritten longer in `server/copy.txt` would be cut off mid-line with
   nothing failing anywhere. `min-height: min-content` lets the box take the
   height its text needs and keeps 16:9 as the floor, and the bottom padding
   clears the 34px beat strip so a wrapped line never sits behind it. */
.hero-frame.empty {
  min-height: min-content;
  display: grid; place-items: center; text-align: center; padding: 1.5rem 1.5rem 3.2rem;
  background:
    linear-gradient(180deg, rgba(225,16,27,.10), rgba(0,0,0,0) 60%),
    repeating-linear-gradient(115deg, #131010 0 14px, #191414 14px 28px);
}
.hero-frame.empty .beats {
  display: flex; align-items: center; gap: .55rem; flex-wrap: wrap;
  justify-content: center; color: var(--dim); font-size: .82rem;
  text-transform: uppercase; letter-spacing: .1em; font-weight: 700;
}
.hero-frame.empty .beats b { color: var(--ink); font-weight: 700; }
.hero-frame.empty .beats i { color: var(--accent-text); font-style: normal; }
.hero-frame.empty .drop {
  margin-top: .9rem; font-size: .8rem; color: var(--dim-2);
  text-transform: none; letter-spacing: 0;
}
/* The bar of beats along the bottom, one of them the drop. It is a still
   picture of what the product does and costs nothing to draw. */
.hero-frame .grid-strip {
  position: absolute; left: 0; right: 0; bottom: 0; height: 34px;
  display: flex; align-items: flex-end; gap: 3px; padding: 0 10px 8px;
  background: linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.65));
  pointer-events: none;
}
.hero-frame .grid-strip span {
  flex: 1 1 0; height: 6px; border-radius: 2px; background: var(--line-2);
}
.hero-frame .grid-strip span.bar { height: 12px; background: var(--dim-2); }
.hero-frame .grid-strip span.drop {
  height: 22px; background: var(--accent); box-shadow: 0 0 12px rgba(225,16,27,.7);
}
.hero-caption {
  margin-top: .6rem; font-size: .82rem; color: var(--dim);
  display: flex; gap: .5rem; flex-wrap: wrap; align-items: baseline;
}
.hero-caption strong { color: var(--ink); }

/* Three short claims under the hero. Not features — the shape of the thing. */
.pitch { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0; }
.pitch div {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); padding: 1rem 1.1rem;
}
.pitch h3 {
  margin: 0 0 .35rem; font-size: .78rem; text-transform: uppercase;
  letter-spacing: .12em; color: var(--accent-text);
}
.pitch p { margin: 0; font-size: .9rem; color: var(--dim); }

/* Valve's button is an image and must not be restyled — a recolored Steam
   button is no longer the thing a user recognizes, which is the only reason to
   use one. The link is a plain box around it; `.button` is deliberately not
   applied. See `assets/PROVENANCE.md`.

   It is in the top bar of every page as well as in the landing hero
   (`docs/contract.md` §8), so `nav.topbar a`'s padding and hover box are
   around it. **Nothing here may set a width, a height or a filter on the
   image** — including in the narrow breakpoint at the foot of this file, which
   shrinks the bar's *text* and must go on leaving this alone. */
.steam-signin { display: inline-block; line-height: 0; }
.steam-signin img { display: block; }

/* The note to musicians, on every surface a clip or a track is shown on. It is
   a `.note` that has to survive being skimmed past by everybody it is not for,
   so it keeps the dim size and gains a rule down the side rather than a box: a
   panel here would read as another control. */
.artists {
  color: var(--dim); font-size: .86rem; max-width: 46rem;
  border-left: 2px solid var(--accent); padding-left: .9rem; margin: 1.4rem 0;
}
.artists strong { color: var(--ink); }

/* ── The moment card ────────────────────────────────────────────────────────
   The unit of the whole site. A moment is a tick range off a parse of a demo,
   and every clip names one; there is no form anywhere that names a demo. */
.grid {
  display: grid; gap: .9rem;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.moment {
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: .95rem 1rem 1rem; display: flex; flex-direction: column; gap: .7rem;
  position: relative; overflow: hidden;
}
.moment.ace { border-color: rgba(242,179,61,.55); }
.moment.ace::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, var(--gold), rgba(242,179,61,0));
}
.moment-top { display: flex; align-items: flex-start; gap: .8rem; }
/* The headline number. A 3K is the reason the card exists, so it is the biggest
   thing on it and the headshot count rides underneath rather than in prose. */
.killcount {
  flex: 0 0 auto; width: 3.6rem; height: 3.6rem; border-radius: 9px;
  display: grid; place-items: center; align-content: center;
  background: #0c0908; border: 1px solid var(--line-2); line-height: 1;
}
.killcount b { font-size: 1.5rem; font-weight: 800; letter-spacing: -.02em; }
.killcount span { font-size: .62rem; color: var(--dim); letter-spacing: .06em; margin-top: .18rem; }
.moment.ace .killcount { border-color: var(--gold); color: var(--gold); }
.moment.ace .killcount span { color: var(--gold); }
/* **A knife kill has no number, so the weapon is the headline** (migration
   00067). The box stays the square every other card gets — one that sized
   itself to its word would break the row a grid draws it in — so the word is
   set to fit the box rather than the box grown to fit the word. */
.moment.knife .killcount b { font-size: .9rem; letter-spacing: .01em; }
.moment-head { min-width: 0; flex: 1 1 auto; }
.moment-head h3 {
  margin: 0; font-size: 1.02rem; font-weight: 700; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.moment-head .sub {
  color: var(--dim); font-size: .82rem; margin-top: .15rem;
  display: flex; gap: .45rem; flex-wrap: wrap; align-items: baseline;
}

/* ── The subject strip ──────────────────────────────────────────────────────
   A band across the top of a card that is about a PERSON rather than about the
   reader's own play — the promo page's cards. It is
   full-bleed to the card's padding and separated by a rule, so it reads as the
   heading the round is filed under rather than one more row inside it.

   **It is above the round and the pills are below, and that is the whole
   point.** The pill row is what the round was; a fact of another kind wearing
   the same chip is read as one more weapon. Anything a page knows about the
   SUBJECT belongs here.

   **No new colour.** Gold is an ace, green is "yours" and the accent is the
   accent — a fourth hue for a fourth meaning is how a palette stops meaning
   anything. The strip is told apart by where it is, and spends colour only on
   the reach state, which is a state. */
.moment > .subject {
  display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
  margin: -.95rem -1rem 0; padding: .45rem 1rem .4rem;
  background: #0c0908; border-bottom: 1px solid var(--line);
  box-shadow: inset 3px 0 0 var(--line-2);
  font-size: .8rem;
}
.moment > .subject .who { flex: 1 1 8rem; min-width: 0; display: flex; gap: .4rem; align-items: baseline; }
.moment > .subject .who a {
  color: var(--ink); font-weight: 700; text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.moment > .subject .who a:hover { color: var(--accent-text); text-decoration: underline; }
.moment > .subject .renamed { color: var(--dim); font-size: .74rem; white-space: nowrap; }
.moment > .subject .flag { color: var(--dim-2); font-size: .7rem; letter-spacing: .04em; }
/* The reach state, in the corner a reader's eye ends on. Uppercased and
   letterspaced so it is told from the name at a glance without a border round
   it: this strip already has edges. */
.moment > .subject .reach {
  color: var(--dim); font-size: .68rem; text-transform: uppercase;
  letter-spacing: .07em; white-space: nowrap; cursor: help;
}
/* **Only a proved yes is coloured.** `unknown` is most of the pool and gets the
   button exactly as `yes` does, so warning about it would warn about the
   ordinary case; `no` is dimmed rather than red, because it is a worse bet and
   not an error. */
.moment > .subject.reach-yes { box-shadow: inset 3px 0 0 var(--ok); }
.moment > .subject.reach-yes .reach { color: var(--ok); }
.moment > .subject.reach-no { opacity: .62; }

.pills { display: flex; flex-wrap: wrap; gap: .35rem; }
.pill {
  font-size: .74rem; border-radius: 999px; padding: .18rem .55rem;
  border: 1px solid var(--line-2); color: var(--dim); background: #0c0908;
  white-space: nowrap;
}
.pill.hs { border-color: rgba(225,16,27,.45); color: #f4525c; }
.pill.gold { border-color: rgba(242,179,61,.5); color: var(--gold); }
.pill.weapon { color: var(--ink); font-variant-numeric: tabular-nums; }

.victims { font-size: .82rem; color: var(--dim); }
.victims b { color: #c6b8b6; font-weight: 600; }

.moment-foot {
  display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
  margin-top: auto; padding-top: .3rem;
}
.moment-foot .status { font-size: .84rem; color: var(--dim); flex: 1 1 8rem; }
.moment-foot .status.bad { color: var(--accent-text); }
.moment-foot .status.good { color: var(--ok); }

/* ── The clip card, on the front page ───────────────────────────────────────
   The map, the round, the length and the status. **Nothing about music**:
   the credit is a license condition on the mp4 and it
   is on the mp4's end card, where a viewer of a shared link actually sees it. A
   card here that named a track would grow the credit with it. */
.clip { padding: 0; overflow: hidden; }
.clip video, .clip iframe, .clip .clip-poster, .clip .facade {
  display: block; width: 100%; aspect-ratio: 16/9; background: #070505;
}

/* ── The play facade ────────────────────────────────────────────────────────
   Our own chrome instead of the player's, because YouTube's is laid out for a
   full-width hero and is most of the picture on a card three to a row, and the
   embed parameters do not trim it.

   **On a card it is a link to `/c/{slug}` and on that page it is the play
   button**: the same thumbnail, and only the button carries the glyph and its
   wash, since a card does not play. `clipLink` in `clipcard.js` draws the
   first, `paintStage` in `clip.js` the second. */
.clip .facade {
  position: relative; padding: 0; border: 0; cursor: pointer;
  overflow: hidden; color: #fff; text-decoration: none;
}
.clip .facade img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover;
  /* hqdefault is 480x360 — 4:3 — so a 16:9 card would letterbox it. Scaled up
     just past the crop, which is what every thumbnail grid does. */
  transform: scale(1.34);
}
.clip button.facade::after {
  /* A wash under the glyph so it reads on a bright frame as well as a dark one.
     On the element rather than the image, so it is still there when the
     thumbnail 404s and removes itself. */
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(ellipse at center, rgba(0,0,0,.42), rgba(0,0,0,.06) 70%);
}
.clip .facade-play {
  position: absolute; top: 50%; left: 50%; translate: -50% -50%;
  width: 2.9rem; height: 2.9rem; border-radius: 999px;
  background: rgba(9,6,7,.72); border: 1px solid rgba(255,255,255,.22);
  backdrop-filter: blur(2px);
  transition: background .15s ease, scale .15s ease;
}
.clip .facade-play::before {
  /* A triangle from a border, so there is no glyph font and no SVG to inline. */
  content: ""; position: absolute; top: 50%; left: 54%; translate: -50% -50%;
  border-style: solid; border-width: .46rem 0 .46rem .74rem;
  border-color: transparent transparent transparent #fff;
}
.clip .facade:hover .facade-play,
.clip .facade:focus-visible .facade-play {
  background: var(--accent); border-color: transparent; scale: 1.06;
}
.clip .facade-time {
  position: absolute; right: .5rem; bottom: .5rem;
  padding: .1rem .32rem; border-radius: 3px;
  background: rgba(7,5,5,.82); font-size: .68rem;
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
}
/* The embed carries its own chrome and its own border; ours would be a second
   frame around it. `iframe` has a border by default in every engine. */
.clip iframe { border: 0; }
/* **A clip card off screen is not laid out or painted.** The element stays in
   the page, so find-in-page, focus and a playing embed are untouched; the
   browser skips its contents until it nears the viewport, and remembers the
   height it last drew at (`auto`) so the scrollbar does not jump. It also
   contains paint, which `.clip`'s `overflow: hidden` already demands.

   `.matchgroup > .matchbody` below does the same for a match's cards. */
.card.clip {
  content-visibility: auto;
  contain-intrinsic-size: auto none auto 320px;
  transition: border-color .15s ease;
}
.card.clip:hover { border-color: var(--line-2); }
.clip .clip-poster {
  display: grid; place-items: center; color: var(--dim-2); font-size: .82rem;
  background: repeating-linear-gradient(115deg, #110d0d 0 14px, #161111 14px 28px);
}
/* The poster stacks its own content once there is a bar or a note under the
   sentence. `place-items: center` alone centres a single child; a column with
   its own gap is what keeps three of them from touching. */
.clip .clip-poster { grid-auto-flow: row; gap: .5rem; padding: 0 1.4rem; text-align: center; }

/* ── The hero's play facade ─────────────────────────────────────────────────
   The landing clip does not autoplay, because a round cut to a drop with the
   sound off is the product with its point removed, and unmuted autoplay is
   blocked everywhere: the press is what buys the sound (`kb.js`). So the hero
   frame wears the card's facade at hero size, with the offer written on it.

   The rules are duplicated rather than widened to `.facade` on its own: the
   card's glyph is sized for a box three to a row, and this one is most of the
   screen. Same objects, two sizes. */
.hero-frame .facade {
  position: relative; display: block; width: 100%; height: 100%;
  padding: 0; border: 0; cursor: pointer; overflow: hidden;
  background: #070505; color: #fff;
}
.hero-frame .facade img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover;
  /* hqdefault is 4:3, so a 16:9 frame would letterbox it. */
  transform: scale(1.34);
}
.hero-frame .facade::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(ellipse at center, rgba(0,0,0,.45), rgba(0,0,0,.08) 70%);
}
.hero-frame .facade-play {
  position: absolute; top: 50%; left: 50%; translate: -50% -50%;
  width: 4.4rem; height: 4.4rem; border-radius: 999px;
  background: rgba(9,6,7,.72); border: 1px solid rgba(255,255,255,.24);
  backdrop-filter: blur(2px);
  transition: background .15s ease, scale .15s ease;
}
.hero-frame .facade-play::before {
  content: ""; position: absolute; top: 50%; left: 54%; translate: -50% -50%;
  border-style: solid; border-width: .7rem 0 .7rem 1.12rem;
  border-color: transparent transparent transparent #fff;
}
/* **The offer, written under the glyph.** It is the accessible name of the
   button as well, so the press and the reason to make it are one target. */
.hero-frame .facade-sound {
  position: absolute; left: 50%; top: calc(50% + 3.1rem); translate: -50% 0;
  white-space: nowrap; padding: .26rem .66rem; border-radius: 999px;
  background: rgba(7,5,5,.72); border: 1px solid rgba(255,255,255,.16);
  font-size: .74rem; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase;
}
.hero-frame .facade:hover .facade-play,
.hero-frame .facade:focus-visible .facade-play {
  background: var(--accent); border-color: transparent; scale: 1.06;
}
.hero-frame .facade:hover .facade-sound,
.hero-frame .facade:focus-visible .facade-sound {
  background: var(--accent); border-color: transparent;
}

/* ── The clip page's player (`/c/{slug}`) ──────────────────────────────
   **The only player on the site.** It carries `.clip` as well, so the facade,
   the poster and the play glyph above are the same objects a card draws; what
   is different is the shape, which follows the version being shown rather than
   being 16:9 for ever.

   The width is computed from the two numbers `clip.js` sets, so a 9:16 render
   is bounded by the height of the window instead of running off the bottom of
   it — an `aspect-ratio` box with a `max-height` on it stops matching its own
   ratio the moment that clamp bites. */
.clip-stage {
  --clip-w: 16; --clip-h: 9;
  width: min(100%, calc(72vh * var(--clip-w) / var(--clip-h)));
  margin: 0 auto 1rem; border-radius: 8px;
}
.clip-stage video, .clip-stage iframe, .clip-stage .clip-poster, .clip-stage .facade {
  aspect-ratio: var(--clip-w) / var(--clip-h); height: auto;
}

/* ── Progress ───────────────────────────────────────────────────────────────
   Drawn only for `filming` and `editing`, which are the two states carrying a
   real number (`docs/contract.md` §3, "Progress is two columns, and it is not
   a state"). `waiting on YouTube` gets `.clip-note`
   instead, because nothing here can know how far along YouTube's own
   processing is — see `jobProgress`. */
.progress {
  width: min(14rem, 70%); height: 3px; border-radius: 999px;
  background: rgba(255,255,255,.12); overflow: hidden;
}
.progress span {
  display: block; height: 100%; background: var(--accent);
}
/* A ping redraws the card, so the bar is a new element and a CSS transition on
   it never fires; `patchList` (`site.js`) slides it from the old card's width. */
/* A full-width row of its own inside the wrapping `.moment-foot`, so a note
   under a status never shares a line with the button beside it. */
.clip-note { margin: 0; color: var(--dim-2); font-size: .74rem; line-height: 1.35; }
.moment-foot .clip-note, .moment-foot .delivery { flex: 1 1 100%; }

.clip .body { padding: .85rem 1rem 1rem; display: flex; flex-direction: column; gap: .55rem; }

/* ── The track browser's list ───────────────────────────────────────────────
   **One row per track, not a grid of cards** (`docs/contract.md` §8, "`/music`"): a
   catalog is scanned and filtered down, and a card grid fits three on a
   screen while making each one look like a decision.

   A row is still a `.card` with a `.moment-foot` in it, and that is
   load-bearing rather than leftover: `sampler.js` finds the `.card` its button
   is inside and inserts the video frame before that foot. The frame and its
   caption take a whole line of the row when they appear — hence `flex-wrap`
   and the two `flex-basis: 100%` rules — so a sampled row grows downwards and
   nothing beside it moves.

   Denser is the goal; losing the attribution to get dense is not. The title and
   the artist are in `.track-id` and the license is a pill in the row. */
.tracklist {
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; margin: .9rem 0;
}
.tracklist .empty { border: 0; }
.card.track {
  display: flex; align-items: center; gap: .6rem 1rem; flex-wrap: wrap;
  margin: 0; border: 0; border-top: 1px solid var(--line); border-radius: 0;
  padding: .6rem .9rem; background: var(--panel);
}
.card.track:first-child { border-top: 0; }
/* The row is the control (`docs/contract.md` §8, "`/music`"): there is no play button, so
   the row has to look pressable and has to show focus for a keyboard. */
.card.track[role="button"] { cursor: pointer; }
.card.track:hover { background: var(--panel-2); }
.card.track:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.card.track.playing { background: var(--panel-2); }
.card.track.playing .row-state { color: var(--accent-text); }
.card.track.loading .row-state::after { content: "…"; }
.card.track.broken .row-state::after { content: "cannot play"; }
.card.track.silent { opacity: .6; }
/* Paused: the picture is the pause control, so it says so rather than looking
   like a video that stopped working. */
.sampler-frame { cursor: pointer; }
.sampler-frame.paused { opacity: .55; }
.track-id { flex: 1 1 14rem; min-width: 0; }
.track-id h3 {
  margin: 0; font-size: .95rem; font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.track-id .meta { margin: 0; }
.card.track .pills { flex: 0 1 auto; }
.card.track .moment-foot { flex: 0 0 auto; margin: 0; padding: 0; gap: .35rem; }
/* **The selected row outlives the ten seconds of audio**, because `#go` on the
   moment context depends on it — a row that only looked selected while it was
   sounding would leave a person pressing "make this clip" with no idea which
   track it meant. `docs/contract.md` §8, "`/music`". */
.card.track.chosen {
  background: var(--panel-2);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* ── The music sampler, on the track browser ────────────────────────────────
   One reference moment auditioned against every track (`docs/contract.md` §8,
   "The example film").
   There is **one** `<video>` element on the page and `sampler.js` moves it into
   whichever card is being sampled, so the frame is drawn where the answer
   belongs rather than in a fixed panel somebody has to scroll back to.

   The element is muted picture: no controls, nothing to press, and no way for
   it to become a second clock in the audio path. The caption under it is the
   copy that stops this reading as a preview of somebody's clip — it is not
   decoration and it goes wherever the frame goes. */
.sampler-frame {
  position: relative; margin: .6rem 0 .5rem;
  border: 1px solid var(--line-2); border-radius: 8px; overflow: hidden;
  background: #070505; aspect-ratio: 16 / 9;
}
.sampler-frame video { display: block; width: 100%; height: 100%; object-fit: cover; }
.sampler-note { font-size: .8rem; margin: 0 0 .6rem; }

/* ── Status, everywhere a user is waiting on something ──────────────────────
   The strings themselves are in `site.js` and are the contract's, verbatim.
   `docs/contract.md` §3 is the table; a status that promises a future is the
   highest-risk string in the repo, so none of them does. */
.state {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .82rem; color: var(--dim);
}
.state::before {
  content: ""; width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--dim-2); flex: 0 0 auto;
}
/* **Still, never animated.** A dot that loops keeps the compositor drawing
   every frame for as long as a job is working, which on a stuck queue is for
   ever; the sentence beside it already says it is working. */
.state.working::before { background: var(--gold); }
.state.done::before { background: var(--ok); }
.state.bad::before { background: var(--accent); }
/* A clip that succeeded and whose delivery window has closed
   (`docs/contract.md` §4, "Direct delivery"). It is
   neither working nor failed, so it takes the default grey dot and says so in
   words — the same treatment `.muted` gets, named separately because it is a
   phase and not a styling choice. */
.state.expired::before, .state.muted::before { background: var(--dim-2); }

/* The delivery choice on a moment card and on `/music`'s commit panel
   (`docs/contract.md` §4, "Direct delivery"). Deliberately quiet: the default — publish it and
   keep it — is what nobody has to think about, so this reads as a footnote
   beside the buttons rather than as a second decision of equal weight. */
/* `flex-wrap` and `min-width: 0` are what let this label be rewritten longer:
   an `inline-flex` item will not shrink below its own content, so without them
   a longer sentence pushes the panel wide on a phone instead of wrapping. */
.delivery, .size-field {
  display: inline-flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  font-size: .8rem; color: var(--dim); cursor: pointer;
  margin: 0 .2rem .5rem 0;
}
.delivery > span, .size-field > span { min-width: 0; }
.delivery input { accent-color: var(--accent); margin: 0; }
.delivery:hover, .size-field:hover { color: var(--ink); }

/* The output-type selector, wherever one is offered. Its label is
   `.size-field` and NOT `.delivery`, though the two are drawn the same: the
   delivery checkbox is one thing a card offers exactly once and the harnesses
   count it by that class, so sharing it makes a passing page fail on a
   sibling control. Shared rule above, separate name. */
select.output { width: auto; margin: 0; font-size: .8rem; padding: .2rem .4rem; }

/* ── The match row ─────────────────────────────────────────────────────────── */
.match {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .8rem 1rem; margin: .6rem 0;
}
.match .id { flex: 1 1 16rem; min-width: 0; }
.match .id b { display: block; font-size: .95rem; font-weight: 700; }
.match .id code {
  background: transparent; padding: 0; color: var(--dim); font-size: .78rem;
  overflow-wrap: anywhere;
}
.match .acts { display: flex; gap: .45rem; flex-wrap: wrap; }
.match .meta { color: var(--dim); font-size: .8rem; margin-top: .15rem; }
.match .meta code { background: transparent; padding: 0; font-size: .78rem; overflow-wrap: anywhere; }
/* Your team's rounds first. Drawn only when the demo named a winner, so its
   absence is the page having nothing to say rather than a nil-all draw. */
.match .score { color: var(--ink); font-variant-numeric: tabular-nums; margin-left: .4rem; }

/* ── A match and the moments inside it ──────────────────────────────────────
   A real <details>, so the open state is the browser's and a keyboard reaches
   it for nothing. The marker is dropped because the whole header is the
   control; `cursor: pointer` is what says so. */
.matchgroup { margin: .6rem 0; }
/* **A match the reader was in**, on the promo page: an admin is looking at the
   whole install's pool and their own evening is in the middle of it with
   nothing to tell it apart. The left edge rather than a full outline, so it
   reads down a column of headers at a glance and does not compete with the
   card grid under an open one. `--ok` is the same green as the ready dot: on
   this site that colour means "this one is yours to act on". */
.matchgroup.mine > summary.match { border-left: 3px solid var(--ok); }
summary.match { cursor: pointer; margin: 0; list-style: none; transition: border-color .15s ease; }
summary.match::-webkit-details-marker { display: none; }
summary.match:hover { border-color: var(--line-2); }
.matchgroup[open] > summary.match { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
/* **Opening and shutting a match is a transition on `::details-content`**, the
   box a `<details>` keeps its body in; a `<details>` animates nothing by
   itself. `content-visibility` is how that box is hidden when shut, so it
   transitions discretely and the body stays drawn until the height reaches 0.
   The height slides only where `interpolate-size` lets `auto` be a transition
   end; elsewhere it snaps and the fade is the motion. A browser without
   `::details-content` ignores all of it. A group drawn open by a poll starts
   open and does not animate. */
.matchgroup { interpolate-size: allow-keywords; }
.matchgroup::details-content {
  block-size: 0; opacity: 0; overflow: clip;
  transition: block-size .26s ease, opacity .2s ease, content-visibility .26s allow-discrete;
}
.matchgroup[open]::details-content { block-size: auto; opacity: 1; }
/* **The border and the closing corners are the body's, not the grid's.** The
   body holds the cards and, under them, the DeadBeat panel — so the panel is
   the bottom of the match's own container rather than a second box that reads
   as another match in the list. */
.matchgroup > .matchbody {
  border: 1px solid var(--line); border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  margin: 0;
}
.matchgroup > .matchbody > .grid { padding: .9rem; margin: 0; }
/* **An open match's cards off screen are not laid out or painted**, as with
   `.card.clip`. On the body and not the group: the header is cheap and always
   drawn, so a closed group costs nothing to scroll past, and only an open
   group's cards are skipped. The height is a first guess for a body never
   drawn; `auto` replaces it with the drawn one. */
.matchgroup > .matchbody {
  content-visibility: auto;
  contain-intrinsic-size: auto none auto 50rem;
}

/* ── The DeadBeat panel ────────────────────────────────────────────────────
   **The last row of the open match, under every card.** It is about the whole
   match rather than any one round, and a control that spends twenty films does
   not belong in a summary the reader scans a season of. A rule above it and no
   border of its own: it is part of the body it sits in. */
.deadbeat-panel {
  border-top: 1px solid var(--line);
  padding: .7rem .9rem;
  display: flex; flex-wrap: wrap; gap: .5rem .6rem; align-items: center;
}
.deadbeat-panel > b { font-size: .88rem; }
.deadbeat-panel .meta.deadbeat { flex: 1 1 12rem; margin: 0; }
.deadbeat-panel .acts { display: flex; gap: .45rem; flex-wrap: wrap; align-items: center; margin-left: auto; }

/* **The spacing, as three chips rather than a dropdown.** There are exactly
   three, and the choice is the whole edit — a `<select>` hides two of them
   behind a press and opens a menu over the button beside it. They read as one
   control, so they share a border and only the seams between them are drawn. */
.beats { display: inline-flex; }
.beats .chip {
  font: inherit; font-size: .82rem; cursor: pointer;
  background: transparent; color: var(--dim);
  border: 1px solid var(--line-2); border-right-width: 0;
  padding: .28rem .55rem;
}
.beats .chip:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.beats .chip:last-child { border-right-width: 1px; border-radius: 0 var(--radius) var(--radius) 0; }
.beats .chip:hover { color: var(--text); }
.beats .chip.here { background: var(--panel-2); color: var(--text); }

/* ── Empty states ──────────────────────────────────────────────────────────
   An empty list is a dead end, so every one of them says where the thing comes
   from instead of saying "none". */
.empty {
  border: 1px dashed var(--line-2); border-radius: var(--radius);
  padding: 1.6rem 1.2rem; text-align: center; color: var(--dim);
  background: rgba(255,255,255,.012);
}
/* An empty state inside a card grid is about the whole grid, not about one
   missing card, so it spans it. */
.grid > .empty { grid-column: 1 / -1; }
.empty h3 { margin: 0 0 .4rem; color: var(--ink); font-size: 1rem; }
.empty p { margin: .3rem auto; max-width: 34rem; font-size: .9rem; }

/* ── Tables, for the operator surfaces. ─────────────────────────────────────── */
table { width: 100%; border-collapse: collapse; font-size: .92rem; }
th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); }
th { color: var(--dim); font-weight: 700; font-size: .76rem; text-transform: uppercase; letter-spacing: .08em; }

/* Reading, rather than operating: the FAQ is prose and wants a measure. */
.prose h2 { margin-top: 2.4rem; }
.prose p { margin: .75rem 0; }

/* The FAQ, one closed answer per question.
   The whole summary row is the target rather than the words in it, because a
   question is a long thin thing and the gap after the text is the easiest part
   of it to hit. The marker is drawn here instead of the browser's triangle so
   it sits at the right edge at the height of the question. */
.qa { border-bottom: 1px solid var(--line); }
.qa > summary {
  list-style: none; cursor: pointer; position: relative;
  padding: .1rem 2rem .1rem 0;
}
.qa > summary::-webkit-details-marker { display: none; }
.qa > summary h2 { margin: 1.1rem 0; display: inline; }
.qa > summary::after {
  content: "+"; position: absolute; right: .35rem; top: 50%;
  transform: translateY(-50%);
  color: var(--dim); font-size: 1.2rem; line-height: 1;
}
.qa[open] > summary::after { content: "\2013"; }
.qa > summary:hover h2, .qa[open] > summary h2 { color: var(--accent-text); }
.qa > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.qa > :last-child { margin-bottom: 1.1rem; }
.prose ul { padding-left: 1.1rem; }
.prose li { margin: .35rem 0; }

/* ── The setup callout ─────────────────────────────────────────────────────
   `/auto`, at the top, and it is the reason the page is not allowed a session
   check: it has to read as "this needs setting up" to a visitor who has none,
   without ever asserting that the reader has none. So it is drawn for
   everybody and coloured like the stale-link strip — the accent's dim end,
   a standing condition rather than an alarm. */
.callout {
  margin: 1.5rem 0 2.5rem; padding: 1.1rem 1.3rem;
  background: var(--accent-dim);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
}
.prose .callout h2 { margin-top: 0; }
.callout > :last-child { margin-bottom: 0; }
.callout ol.steps { margin-bottom: .75rem; }

/* ── The copyable line ──────────────────────────────────────────────────────
   `/auto`'s bind, and the one control on that page (`docs/contract.md` §8,
   "`/faq`, `/auto`, and the rig dot").
   The `<code>` is `user-select: all` so a visitor who cannot use the button
   still gets the whole line from one click, and it wraps rather than scrolling:
   a phone is the case this page is actually read on, and a horizontally
   scrolling command is a command somebody copies half of. */
.copyline {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin: .75rem 0;
}
.copyline code {
  user-select: all; padding: .5rem .7rem; font-size: .95em;
  border: 1px solid var(--line); overflow-wrap: anywhere;
}
.copyline .note { margin: 0; }

/* ── Narrow ────────────────────────────────────────────────────────────────
   A CS2 player checking their clips is usually on a phone, so this is the case
   that gets checked rather than the one that gets an afterthought. */
@media (max-width: 860px) {
  .hero { grid-template-columns: 1fr; gap: 1.5rem; }
  .pitch { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  header.site { padding: .7rem .9rem; gap: .6rem; }
  main { padding: 1.1rem .9rem 3rem; }
  /* The bar is the only navigation there is, so on a phone it wraps rather
     than scrolls: nothing on it may end up off the edge unreachable. */
  nav.topbar a, nav.topbar .menu-open { padding: .35rem .5rem; font-size: .8rem; }
  .grid { grid-template-columns: 1fr; }
  /* A track row stacks on a phone rather than squeezing three columns into
     390px; the controls get the full width the same way a moment card's do. */
  .card.track { align-items: stretch; }
  .card.track .moment-foot { flex: 1 1 100%; }
  .card.track .moment-foot button { width: auto; flex: 1 1 auto; }
  #who { max-width: 7rem; }
  .moment-foot .button, .moment-foot button { width: 100%; text-align: center; }
}

/* ════════════════════════════════════════════════════════════════════════════
   The curation instrument.

   Everything below belongs to the curation page, which **this server does not
   serve**: it runs on the render guest, beside the audio, and reads this
   stylesheet out of the checkout rather than carrying a copy
   (`worker/killbeat_worker/curateweb.py`, `STATIC_DIRS`). So this section is
   the one place those classes are written down and deleting it takes the
   guest's page with it. It is referenced by class from `curate.js` under
   `worker/curateweb/`. **The audio there is a decoded buffer on one clock, never
   an `<audio>` element in the graph** (`docs/curation.md`, "There is one
   clock").
   ══════════════════════════════════════════════════════════════════════════ */

/* The scrub bar sits directly under the waveform and the button below both, so
   the row is the button and the clock rather than the whole transport. */
.player { display: flex; align-items: center; gap: .7rem; margin-top: .6rem; }
/* Square, and the same corner radius as every other button on the page: it is
   one of the controls the curator presses, not a piece of media furniture.
   Big, because it and "back to the proposal" are the two things pressed over
   and over through a sitting, and they are pressed while listening rather than
   while looking. */
.player .play {
  flex: 0 0 auto; width: 3.6rem; height: 3.6rem; border-radius: 4px;
  background: var(--accent); color: var(--ink); border: 0; cursor: pointer;
  font-size: 1.4rem; line-height: 1; display: grid; place-items: center;
  padding: 0; font-weight: 700;
}
.player .play.playing { background: var(--ink); color: var(--bg); }
/* The same height and the same weight as play: one row, two controls that
   belong to each other. */
.player .seek {
  flex: 0 0 auto; height: 3.6rem; padding: 0 1.1rem; border-radius: 4px;
  font-size: .95rem; display: inline-grid; place-items: center;
}
.player .clock {
  flex: 0 0 auto; color: var(--dim); font-size: .8rem;
  font-variant-numeric: tabular-nums;
}
/* The scrub bar is a map of the whole track, so it carries where the proposal
   sits as well as where the playhead is. Tall enough to be a target on a
   trackpad; the hit area is the row, not the 6px line. */
.beat .scrub {
  position: relative; display: block; width: 100%; height: 6px;
  border-radius: 3px; margin-top: .35rem;
  background: #100d0d; border: 1px solid var(--line); cursor: pointer;
  padding: 0; touch-action: none; box-sizing: border-box;
}
.beat .scrub::before {
  /* An invisible grab area above and below the line. */
  content: ""; position: absolute; inset: -9px 0; display: block;
}
.beat .buffered, .beat .played {
  position: absolute; top: 0; bottom: 0; left: 0; border-radius: 3px;
}
.beat .buffered { background: var(--line); }
.beat .played { background: var(--ink); }
.beat .drop-mark {
  position: absolute; top: -4px; bottom: -4px; width: 2px;
  background: var(--accent); border-radius: 1px;
}

/* The review table's filters, and the add-a-moderator row: two or three
   controls on one line rather than a stacked form, because they are a question
   being narrowed rather than a record being filled in. */
.filters, .add-mod {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin: .6rem 0;
}
/* The leaves of an open root. Indented and quieter than the row above it, so
   the two levels read as one control rather than two rows of equal chips --
   and `[hidden]` in this file is what closes it, which is why nothing here
   sets `display`. */
.subfilters {
  margin-top: -.2rem; padding-left: 1.1rem;
  border-left: 2px solid var(--line);
}
.subfilters button { font-size: .8rem; }

/* The catalog's search box. It sits in a `.filters` row above the chips, so it
   is one line with them rather than a form above a filter: the box and the
   chips narrow the same list and read as one control. The margin is cleared
   because the global input rule tops one off for a stacked form. */
.filters.search { margin-bottom: .3rem; }
.filters.search input { flex: 1 1 16rem; width: auto; max-width: 22rem; margin: 0; }

/* The shared song, `/music?track=`. Marked out from the list below it so it
   reads as the row the link named rather than as the top of the catalog. */
#pinned .track { border-color: var(--accent); }
#pinned-note { margin: .1rem 0 1rem; }

.filters label { margin: 0; color: var(--dim); font-size: .85rem; }
.filters select { width: auto; margin: 0; }
.add-mod input { width: auto; flex: 1 1 12rem; margin: 0; }
#mod-table td { vertical-align: middle; }

/* The transport. Volume is global — one control for the whole sitting, sticky
   so it stays reachable however far down the queue the curator has read. */
#transport {
  position: sticky; top: 3.4rem; z-index: 2;
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  background: var(--panel); border: 1px solid var(--line); border-radius: 6px;
  padding: .6rem .9rem; margin: .75rem 0;
}
#transport label { margin: 0; color: var(--dim); font-size: .85rem; }
#transport input[type="range"] { display: inline-block; width: 12rem; margin: 0; padding: 0; accent-color: var(--accent); }
#transport output { font-variant-numeric: tabular-nums; min-width: 3ch; color: var(--ink); font-size: .85rem; }
#transport .toggle { display: flex; align-items: center; gap: .4rem; cursor: pointer; }
#transport input[type="checkbox"] { display: inline-block; width: auto; margin: 0; }
#transport .note { flex-basis: 100%; margin: 0; }

/* The beat grid: the ruler under the player, the bar counter, and the one
   marker that must not be mistakable for a tick. */
.beat { margin-top: .7rem; }
.beat .ruler {
  display: block; width: 100%; height: 112px;
  background: #100d0d; border: 1px solid var(--line); border-radius: 4px;
  /* Dragging it scrubs, so the browser must not claim the gesture for a scroll
     and the drag must not select the page around it. */
  cursor: ew-resize; touch-action: none; user-select: none;
}
.beat .dots { display: flex; gap: .35rem; margin: .5rem 0; }
.beat .dot {
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  display: grid; place-items: center; font-size: .7rem;
  background: #100d0d; border: 1px solid var(--line); color: var(--dim);
}
.beat .dot.one { border-color: var(--accent); color: var(--accent-text); font-weight: 700; }
.beat .dot.on { background: var(--ink); color: #141010; border-color: var(--ink); }
.beat .dot.one.on { background: var(--accent); border-color: var(--accent); color: var(--ink); }
.drop-badge {
  display: inline-flex; align-items: baseline; gap: .5rem;
  border: 1px solid var(--accent); border-radius: 4px; padding: .25rem .6rem;
  color: var(--accent-text); font-size: .8rem;
}
.drop-badge span { color: var(--dim); }
.drop-badge.hit { background: var(--accent); color: var(--ink); }
.drop-badge.hit span { color: rgba(244,237,236,.72); }

/* The genres, on a curation card: a line of their own under the nudges rather
   than words appended to the metadata line. They are shown, not decided
   (`docs/contract.md` §5). */
.genre-line {
  display: flex; align-items: baseline; gap: .5rem; margin: 1.1rem 0 0;
}
.genre-line span {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .12em;
  color: var(--dim); font-weight: 700;
}
.genre-line b {
  font-size: 1.35rem; font-weight: 800; letter-spacing: -.01em; color: var(--ink);
}
/* A track with no genre, drawn as `unclassified`. A normal facet, so it is
   muted rather than alarming, and it is not spelled like a genre. */
.genre-line b.none { color: var(--dim-2); font-style: italic; font-weight: 600; }

.verdicts { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .8rem; }
.verdicts button { flex: 0 0 auto; }
.verdicts .why { color: var(--dim); font-size: .8rem; flex-basis: 100%; margin: 0; }
/* Skip is neither a state nor a nudge — nothing is sent and nothing is
   recorded — so it sits apart from both rows rather than beside them. */
.verdicts.skip { margin-top: 1.1rem; border-top: 1px solid var(--line); padding-top: .7rem; }
.v-yes { background: var(--ok); }
.v-early, .v-late { background: var(--warn); }
/* **Blue, because it is neither a nudge nor a verdict.** A nudge moves the drop
   by a bar and is the curator's own judgement; this jumps to a number the
   *detector* produced, and it is reversible. Its own colour says so without a
   sentence. `docs/curation.md`, "States and verdicts". */
.v-alternate { background: var(--info); }
.v-offgrid, .v-bad { background: var(--panel); color: var(--ink); border: 1px solid var(--line); }

/* Per-entry results from a bulk add: which of the ten went in, and why the one
   that did not, did not. */
.results { list-style: none; padding: 0; margin: .5rem 0; font-size: .85rem; }
.results li { padding: .15rem 0; }
.results .ok { color: var(--dim); }
.results .bad { color: var(--accent-text); }
.add-mod textarea {
  flex: 1 1 20rem; min-width: 14rem; margin: 0; font: inherit;
  background: #100d0d; color: var(--ink); border: 1px solid var(--line);
  border-radius: 4px; padding: .45rem .6rem; resize: vertical;
}
.add-mod { align-items: flex-start; }

/* The metronome, in the transport row with play and "back to the proposal": it
   is pressed while
   listening, over and over, and it belongs in the same reach as the other two.
   `.on` is the pressed state — a control with two states has to say which one
   it is in without being clicked to find out. */
.player .metronome.on {
  background: var(--ink); color: var(--bg); border-color: var(--ink);
}

/* ── The showcase, the profile and the renders panel ────────────────────────
   `docs/contract.md` §8, "The wall, and a player's page". The shapes of their
   own are a name under a clip and a face beside a heading; everything else is
   the card grid and the table. */

/* The name under a clip on the wall. It is a link to a profile on this site
   and never straight out to Steam: a wall of outbound links is a wall that
   empties itself, and the Steam link belongs on the profile where there is
   room to say what it is. */
.owner {
  display: inline-flex; align-items: center; gap: .45rem;
  color: var(--dim); font-size: .86rem; text-decoration: none;
  min-width: 0;
}
.owner:hover { color: var(--ink); }
.owner img { width: 24px; height: 24px; border-radius: 50%; display: block; }
.owner span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The fallback for an account that has not signed in since avatars existed,
   and for one whose picture Steam has stopped serving. **A broken-image glyph
   in the middle of a card is the thing this exists to prevent**: an initial
   reads as a choice. */
.owner-initial {
  display: grid; place-items: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--panel-2); border: 1px solid var(--line-2);
  color: var(--dim); font-weight: 700; font-size: .8rem;
}

.profile { display: flex; align-items: center; gap: 1.1rem; margin: 1.4rem 0 .4rem; }
.profile h1 { margin: 0; font-size: 1.6rem; }
.profile .meta { margin: .2rem 0 .3rem; }
.profile-avatar img { width: 96px; height: 96px; border-radius: 50%; display: block; }
.profile-avatar .owner-initial { width: 96px; height: 96px; font-size: 2.4rem; }

/* A checkbox and its label on one line, with the explanation under both. The
   input is not restyled: a checkbox that does not look like one is a control
   people do not press. */
.check { display: flex; align-items: center; gap: .55rem; margin: 0 0 .4rem; }
.check input { width: auto; margin: 0; }

/* The re-queue confirmation. A real `<dialog>` rather than `confirm()`, because
   what it has to say is longer than one line — it spends minutes of the only
   GPU there is, and it cannot take the old video down. */
dialog {
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: 10px;
  padding: 1.2rem 1.3rem; max-width: 34rem; width: calc(100% - 2rem);
  /* Said rather than left to the UA stylesheet: this box holds four
     paragraphs out of `server/copy.txt`, and on a phone a rewrite one
     paragraph longer is a dialog whose Queue it button is off the bottom of
     the screen with no way to reach it. */
  max-height: calc(100% - 4rem); overflow: auto;
}
dialog::backdrop { background: rgba(10, 7, 8, .7); }
dialog h3 { margin: 0 0 .6rem; }
dialog .note { margin: .5rem 0; }
dialog label { display: block; margin: .9rem 0 .3rem; color: var(--dim); font-size: .85rem; }

/* The renders table. The right-hand column is the two controls, and they sit
   at the end of the row rather than in a menu: there are two of them and a
   menu to hold two things is a click for nothing. */
#rows td.actions { text-align: right; white-space: nowrap; }
#rows td.actions button { margin-left: .35rem; }
#rows tr.hidden-row td { opacity: .62; }
/* The jobs panel's two number blocks. **`#rig` shares `#stats`' rule** because
   it is the same thing one row down — a count over its label — and without a
   rule `<b>3</b>` and its label run together. */
#stats, #rig { display: flex; flex-wrap: wrap; gap: .4rem 1.6rem; }
#stats div, #rig div { font-variant-numeric: tabular-nums; }
#stats b, #rig b { display: block; font-size: 1.3rem; }
#stats span, #rig span { color: var(--dim); font-size: .78rem; text-transform: uppercase; letter-spacing: .07em; }

/* ── The cookie notice, which is a joke about not needing one ──────────────
   `frontend/static/banner.js`, `server/internal/api/banner.go`.

   **It has to read as the banner everybody dismisses**, or the joke has no
   setup: a strip pinned to the bottom of the window, over the page, with the
   accept button first and the reading one quiet beside it. Everything past
   that is ordinary panel styling, because the text is the feature.

   It scrolls once opened rather than growing the page, so four hundred words
   cannot push the site out from under somebody reading them. */
.cookie-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  margin: 0; padding: .9rem 1.5rem;
  max-height: 70vh; overflow-y: auto;
  background: var(--panel); border-top: 1px solid var(--line-2);
  color: var(--ink); font-size: .9rem;
}
.cookie-banner > p { margin: 0 0 .7rem; max-width: 52rem; }
.cookie-banner .row { gap: .6rem; }
.cookie-body { margin-top: 1rem; border-top: 1px solid var(--line); padding-top: .9rem; }
.cookie-body p { margin: .7rem 0; max-width: 52rem; }
.cookie-body .note { margin-top: 1rem; }

/* ── Motion ───────────────────────────────────────────────────────────────────
   **Every transition and animation on the site ends**, because one that loops
   keeps the compositor drawing every frame for as long as it is on screen.
   Reduced motion turns all of it off here, in one place; `patchList` checks the
   same query before starting any of its own. `::details-content` is a rule of
   its own because a browser that does not know it drops the whole list. */
@media (prefers-reduced-motion: reduce) {
  *, ::before, ::after {
    transition: none !important;
    animation: none !important;
  }
  ::details-content { transition: none !important; }
}
