/* Boom Beard — all of the chrome.
 *
 * Split out of index.html once it outgrew a <style> block. Still one file, still no build step,
 * still no framework: the whole UI is a top bar, a chat panel, two overlays and a menu card.
 *
 * Two typefaces, with a rule: the pixel face is for things you READ ONCE and the text face is for
 * things you read WHILE PLAYING. A pixel font at 12px in a kill feed is a puzzle, not a message.
 */

/* The display face. 16 KB against the 136 KB this replaced, and it covers every character the
 * interface puts through it — upper, lower, digits and the punctuation in `!?.,:'-/()&`, checked
 * against the cmap rather than assumed. A display font missing one glyph does not error: the
 * browser substitutes silently from the next family in the stack, so a single character in a
 * headline comes out in a different typeface and reads as a rendering bug.
 *
 * Licence lives beside it in assets/fonts/clegan-LICENSE.txt: free for personal use, which is
 * what this is. */
@font-face {
  font-family: "Clegan";
  src: url("/assets/fonts/clegan.ttf") format("truetype");
  font-display: block; /* no flash of fallback on the title; it is the first thing seen */
}

:root {
  color-scheme: dark;
  /* The accent: gold, and this time it means it.
   *
   * THREE tones, not one, because a single flat yellow is the thing that made the first gold
   * look cheap. Metal is not a colour, it is a RANGE — a dark underside, a body, and a specular
   * highlight — and the eye reads the relationship between those three as "this is reflective"
   * long before it reads the hue. One value can only ever be paint.
   *
   *   --gold-hi    the shine. Where the light lands.
   *   --gold       the metal itself. Everything flat uses this one.
   *   --gold-low   the shadowed underside, and the colour a bevel is cut from.
   *
   * The ramp below (`-lit`, `-deep`, `-shadow`) is the same three stretched for filled buttons,
   * and `--cast` further down is them poured through text. 9.7:1 on the panel, 11.2:1 on the
   * page, so the brightest interface colour is also the most readable one.
   *
   * It has been gold, baby blue, the mascot's bandana red and fire orange. Orange lost because
   * it WAS the fire — the chrome and the explosions were the same hue, and on a busy board that
   * is one signal too few. Gold is next to the fire without being it.
   *
   * CAREFUL: within 1.7:1 of `--bad`. Anything that has to read as an alarm beside the accent
   * cannot do it with hue — see `#clock.low`, which uses brightness instead. `--warn` is blue
   * for the same reason: an amber dot beside a gold HUD is not a state, it is more gold. */
  --gold-hi: #f6e7a9;
  --gold: #edc24e;
  --gold-low: #a06a12;

  --gold-lit: #f6e7a9; /* the top of a filled button */
  --gold-deep: #c08d22; /* the bottom of one */
  --gold-shadow: #6d4709; /* and the hard shadow under it */
  --on-gold: #2a1a04; /* text that sits on a filled accent — 8.9:1 */

  /* Cast gold, as a paintable background. Clipped to text by `.cast` below.
   *
   * Two layers. The lower one is the metal: a vertical ramp that goes dark at the top edge,
   * blows out to the highlight a third of the way down, drops back through the body, and comes
   * up to a SECOND smaller highlight near the bottom before going dark again. That second band
   * is the whole trick — it reads as the underside catching bounced light, and it is the
   * difference between gold and a yellow gradient. A single highlight looks like plastic.
   *
   * The upper one is a narrow specular sweep that travels across, which is what makes it look
   * cast rather than printed: real metal does not change, the light on it does. */
  --cast:
    /* The band is NARROW on purpose: 8% of a layer that is 2.6x the width of the text, so it
       lands as a highlight about a fifth of the word wide. It was 32% of a 3.2x layer, which
       works out wider than the whole word — so instead of a travelling glint it very slightly
       brightened everything, all the time, and the sweep was invisible. A specular highlight
       you cannot see the edges of is not a highlight, it is exposure. */
    linear-gradient(
      104deg,
      #ffffff00 42%,
      #ffffff00 46%,
      #fffffff2 50%,
      #ffffff00 54%,
      #ffffff00 58%
    ),
    linear-gradient(
      180deg,
      #7d5410 0%,
      #b8841f 14%,
      var(--gold-hi) 33%,
      #f0cf6a 44%,
      var(--gold) 56%,
      #b8841f 70%,
      #f2d479 84%,
      #6d4709 100%
    );
  --panel: #1a2029;
  --ink: #e8eef7;
  --dim: #92a0b4;
  --bad: #e8655a;
  /* In flight, not settled. BLUE, not the amber it was: the accent is gold now, and an amber
     dot beside a gold HUD is not a third state, it is more gold. Blue is maximally far from
     both the accent and `--bad`, and "working on it" is as conventional in blue as in amber. */
  --warn: #58b6f5;
  --good: #5ad1a0;

  /* The display face, for large type only. */
  --display: "Clegan", ui-monospace, monospace;
  /* Condensed humanist sans for anything you read mid-round. Falls back the whole way down. */
  --text:
    "Bahnschrift", "DIN Alternate", "Avenir Next Condensed",
    "Segoe UI Semibold", "Helvetica Neue", system-ui, sans-serif;
  /* Tabular figures for numbers that tick, so the timer does not shuffle sideways every second. */
  --nums: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* The black rim every piece of display type carries, so gold text stays readable over the
   * card, the gradient border and the sea alike.
   *
   * Four zero-blur offsets draw the outline, the wide blur lifts it off whatever is behind.
   * Only usable where the fill is opaque — #title clips a gradient through transparent glyphs,
   * so a shadow would show through the inside of every letter and it uses a filter instead. */
  --rim:
    1.5px 0 0 #000, -1.5px 0 0 #000, 0 1.5px 0 #000, 0 -1.5px 0 #000,
    0 0 12px #000b;

  /* The thin dark outline every piece of cast type carries.
   *
   * Four zero-blur drop-shadows at the compass points, which is an outline that follows the
   * letterforms. It has to be a FILTER rather than a text-shadow: cast text has a transparent
   * fill with a background clipped through it, and a text-shadow would show straight through
   * the inside of every glyph. Thin — 1px, not the 1.5 the old flat rim used — because its job
   * here is to give the metal an edge, not to make the word legible. Too thick and the letters
   * stop looking cast and start looking stickered on. */
  /* The drop shadow under every centre-screen announcement — YOU DIED and the kill streaks
   * alike. One token, because they appear in the same place seconds apart and two different
   * weights of shadow on the same spot reads as one of them being broken.
   *
   * Heavy on purpose: these land over a board full of fire and sixteen saturated players, and a
   * banner that does not sit clearly in FRONT of all that is a banner nobody reads in time. */
  --banner-shadow: 0 4px 0 #000, 0 8px 18px #000d, 0 14px 36px #000a;

  --cast-edge: drop-shadow(1px 0 0 #1c1304) drop-shadow(-1px 0 0 #1c1304)
    drop-shadow(0 1px 0 #1c1304) drop-shadow(0 -1px 0 #1c1304);

  /* A robot head: antenna, squared skull, two eyes. Inline so it can be a CSS mask and take
   * whatever colour the row it sits in is using. */
  --robot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M7.2 0h1.6v2.4H7.2zM2.6 4h10.8a1 1 0 011 1v7a1 1 0 01-1 1H2.6a1 1 0 01-1-1V5a1 1 0 011-1zm2.3 2.6a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm6.2 0a1.5 1.5 0 100 3 1.5 1.5 0 000-3z'/%3E%3Ccircle cx='8' cy='1.1' r='1.3'/%3E%3Crect x='0' y='6.6' width='1.3' height='3.8' rx='.6'/%3E%3Crect x='14.7' y='6.6' width='1.3' height='3.8' rx='.6'/%3E%3C/svg%3E");

  /* The crown, inline so it can be a CSS mask and take whatever colour it is used in. */
  /* A texture for the panels, built out of gradients rather than an image.
   *
   * Three layers, none of them individually visible: a fine diagonal weave at about one percent
   * white, the same weave crossed the other way at half that, and a very wide soft highlight
   * falling from the top left. Separately they are invisible; together they stop a flat #11161e
   * rectangle reading as a hole cut in the screen, which is what large unbroken fills do on a
   * dark page. Nothing here costs a request or a decode. */
  --weave:
    repeating-linear-gradient(45deg, #ffffff05 0 1px, #00000000 1px 4px),
    repeating-linear-gradient(-45deg, #ffffff03 0 1px, #00000000 1px 5px),
    radial-gradient(120% 90% at 12% 0%, #ffffff0a, #ffffff00 58%);

  --crown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 12h12l1-7-3.5 2.5L8 3 4.5 7.5 1 5z'/%3E%3Crect x='2' y='13' width='12' height='2'/%3E%3C/svg%3E");

  /* Podium medals: a struck disc on a ribbon, gold / silver / bronze.
   *
   * Full-colour SVG backgrounds rather than masks — a medal is three materials, not one colour.
   * Each has the same parts: a two-tone ribbon, a disc with a diagonal metal ramp, a darker rim, a
   * faint inner ring, the place number struck into it, and one soft highlight top-left (the
   * interface's light comes from there, like everything on the board). The RIBBONS differ in hue
   * too — red, blue, green — so the three are told apart by more than how shiny they are, which
   * matters for anyone who cannot easily tell gold from bronze. */
  --medal-1: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23fff3b0'/%3E%3Cstop offset='.5' stop-color='%23edc24e'/%3E%3Cstop offset='1' stop-color='%239c6a0c'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M4.5 0h4.2l3 9.5H7.5z' fill='%23c0392b'/%3E%3Cpath d='M15.5 0h-4.2l-3 9.5h4.2z' fill='%238f2a20'/%3E%3Ccircle cx='10' cy='15' r='8' fill='url(%23g)' stroke='%236d4709' stroke-width='1.2'/%3E%3Ccircle cx='10' cy='15' r='5.7' fill='none' stroke='%23fff6c8' stroke-opacity='.6' stroke-width='.8'/%3E%3Cellipse cx='7' cy='11.8' rx='2.6' ry='1.3' fill='%23fff' fill-opacity='.55' transform='rotate(-35 7 11.8)'/%3E%3Ctext x='10' y='18.4' font-family='Arial,Helvetica,sans-serif' font-weight='700' font-size='9' text-anchor='middle' fill='%234a2f05'%3E1%3C/text%3E%3C/svg%3E");
  --medal-2: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23ffffff'/%3E%3Cstop offset='.5' stop-color='%23c9d1db'/%3E%3Cstop offset='1' stop-color='%23737d8a'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M4.5 0h4.2l3 9.5H7.5z' fill='%232f6db5'/%3E%3Cpath d='M15.5 0h-4.2l-3 9.5h4.2z' fill='%23234f85'/%3E%3Ccircle cx='10' cy='15' r='8' fill='url(%23g)' stroke='%2346505e' stroke-width='1.2'/%3E%3Ccircle cx='10' cy='15' r='5.7' fill='none' stroke='%23ffffff' stroke-opacity='.6' stroke-width='.8'/%3E%3Cellipse cx='7' cy='11.8' rx='2.6' ry='1.3' fill='%23fff' fill-opacity='.6' transform='rotate(-35 7 11.8)'/%3E%3Ctext x='10' y='18.4' font-family='Arial,Helvetica,sans-serif' font-weight='700' font-size='9' text-anchor='middle' fill='%23262c35'%3E2%3C/text%3E%3C/svg%3E");
  --medal-3: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23f9cfa6'/%3E%3Cstop offset='.5' stop-color='%23c8814a'/%3E%3Cstop offset='1' stop-color='%23744020'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M4.5 0h4.2l3 9.5H7.5z' fill='%232e8b57'/%3E%3Cpath d='M15.5 0h-4.2l-3 9.5h4.2z' fill='%231f6140'/%3E%3Ccircle cx='10' cy='15' r='8' fill='url(%23g)' stroke='%234d2a12' stroke-width='1.2'/%3E%3Ccircle cx='10' cy='15' r='5.7' fill='none' stroke='%23ffe2c4' stroke-opacity='.55' stroke-width='.8'/%3E%3Cellipse cx='7' cy='11.8' rx='2.6' ry='1.3' fill='%23fff' fill-opacity='.45' transform='rotate(-35 7 11.8)'/%3E%3Ctext x='10' y='18.4' font-family='Arial,Helvetica,sans-serif' font-weight='700' font-size='9' text-anchor='middle' fill='%23361c0a'%3E3%3C/text%3E%3C/svg%3E");
  /* The podium tints behind those rows. Faint, and fading out to the right, so a row reads as
   * "placed" without the colour competing with the player's own name colour. */
  --tint-1: linear-gradient(90deg, #edc24e2e, #edc24e00 75%);
  --tint-2: linear-gradient(90deg, #c9d1db24, #c9d1db00 75%);
  --tint-3: linear-gradient(90deg, #c8814a29, #c8814a00 75%);
}

/* ─── medals and podium rows, shared by every board ──────────────────────────── */

.medal {
  display: inline-block;
  width: 19px;
  height: 22px;
  vertical-align: -6px;
  background: center / contain no-repeat;
  filter: drop-shadow(0 1px 1px #000b);
}
.medal.m1 {
  background-image: var(--medal-1);
}
.medal.m2 {
  background-image: var(--medal-2);
}
.medal.m3 {
  background-image: var(--medal-3);
}
/* Gold catches the light now and then: one slow glint every few seconds, not a strobe. */
.medal.m1 {
  animation: medalGlint 4.5s ease-in-out infinite;
}
@keyframes medalGlint {
  0%,
  82%,
  100% {
    filter: drop-shadow(0 1px 1px #000b);
  }
  88% {
    filter: drop-shadow(0 1px 1px #000b) drop-shadow(0 0 5px #fff1a8cc)
      brightness(1.18);
  }
}
@media (prefers-reduced-motion: reduce) {
  .medal.m1 {
    animation: none;
  }
}

/* Lifetime medal counts: three small medals, each with its count. A zero is dimmed, not
 * removed, so the column keeps one shape all the way down and "no silver yet" still reads. */
.medal.mini {
  width: 14px;
  height: 17px;
  vertical-align: -4px;
  animation: none;
}
.tally {
  display: inline-flex;
  gap: 9px;
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
}
.tally .t {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-width: 2.6ch;
}
.tally .t.zero {
  opacity: 0.3;
}
.tally.none {
  color: var(--dim);
}
#cardMedals {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 13px;
}
#cardMedals:empty {
  display: none;
}
#cardMedals .k {
  color: var(--dim);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-family: var(--text);
  margin-right: 8px;
}
body.observing #cardMedals {
  display: none;
}

.p1 {
  background: var(--tint-1);
}
.p2 {
  background: var(--tint-2);
}
.p3 {
  background: var(--tint-3);
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: #0d1117;
  color: var(--ink);
  font: 15px/1.5 var(--text);
  letter-spacing: 0.02em;
}

/* ─── layout ──────────────────────────────────────────────────────────────────
 * Three fixed regions: a banner across the top, the board, and a side panel. The board gets
 * whatever is left, and the renderer scales the map to fit it exactly — so there are no black
 * bars and the whole island is on screen at once.
 */

:root {
  --bar: 52px;
  --side: 340px;
  --gap: 10px;
}

/* Explicit width and height, not `auto` with four insets.
 *
 * A canvas is a REPLACED element, and for an absolutely positioned replaced box `width: auto`
 * resolves to the INTRINSIC width — the canvas default of 300x150 — rather than to the gap
 * between `left` and `right`. The box is then over-constrained and the browser drops `right` and
 * `bottom` entirely, so the game was laid out 300x150 in the corner and painted as an empty
 * replaced element: a white rectangle with a broken-content icon. calc() removes the ambiguity. */
canvas#game {
  position: fixed;
  top: calc(var(--bar) + var(--gap) * 2);
  left: var(--gap);
  width: calc(100vw - var(--side) - var(--gap) * 3);
  height: calc(100vh - var(--bar) - var(--gap) * 3);
  display: block;
  background: #0f2b40; /* the sea, so a frame that has not painted yet is not a flash. Keep in
                          step with PALETTE.seaDeep in art.js. */
  image-rendering: pixelated;
  border-radius: 8px;
}

/* Below this the side panel costs more than it gives: drop it, and let the board have the lot. */
@media (max-width: 1080px) {
  :root {
    --side: 0px;
  }
  #side {
    display: none;
  }
  canvas#game {
    width: calc(100vw - var(--gap) * 2);
  }
}

/* ─── the moving gradient ─────────────────────────────────────────────────────
 * One animated conic gradient, reused by every panel that wants a live border and by the hovered
 * menu text. Implemented as a padding-box/border-box mask rather than a pseudo-element, so the
 * border follows the border-radius exactly and costs one compositor layer instead of two.
 *
 * Gold and maroon, the landing page's colours, not the rainbow it was. Six stops — bright gold,
 * old gold, amber, a lit maroon, the deep maroon and a warm rose — the same six the menu's VU
 * runs through (EDGE_HUES in menuviz.js), on the same 28-second turn, so the whole interface
 * reads as one light. The rainbow put lavender and two blue-greens round every panel, and next
 * to cast-gold type and a maroon backdrop that read as a second, unrelated theme.
 */

@property --spin {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

@keyframes spin {
  to {
    --spin: 360deg;
  }
}

/* ─── cast gold ───────────────────────────────────────────────────────────────
 * Poured metal, for display type. `--cast` above is the paint; this is the brush.
 *
 * Three things have to be true at once or it reads as a yellow gradient:
 *
 *   1. **Two highlights, not one.** The big one a third down and a smaller one near the bottom
 *      edge. The second is bounced light off whatever the object is sitting on, and its absence
 *      is why most "gold text" CSS looks like plastic.
 *   2. **The light moves, the metal does not.** The specular band travels across on a slow loop.
 *      Real gold is static; what changes is the light on it, and animating the highlight rather
 *      than the colour is the difference between cast and printed.
 *   3. **A hard thin edge.** Metal has a boundary. Without `--cast-edge` the pale top of the
 *      gradient dissolves into any light background behind it.
 *
 * `background-clip: text` needs the fill transparent, which rules out text-shadow entirely —
 * see --cast-edge.
 *
 * LARGE TYPE ONLY — about 30px and up. Below that the vertical ramp has fewer pixels than it
 * has stops, so the two highlights and the two dark edges all land inside three or four rows
 * and the word turns to mush. Checked at 17px and it is unreadable. Small display type stays
 * flat `var(--gold)` with `--rim`, which is what it was always for. */
.cast {
  /* Flat gold first — see `.castSm` below for why every cast rule is built this way. */
  color: var(--gold);
  text-shadow: var(--rim);
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .cast {
    background: var(--cast);
    /* The sweep layer is three times the width of the text so it can travel across it; the metal
     underneath is fixed. Order matches the two layers in --cast. */
    background-size:
      260% 100%,
      100% 100%;
    background-position:
      120% 0,
      0 0;
    background-repeat: no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    text-shadow: none;
    filter: var(--cast-edge) drop-shadow(0 2px 1px #0009)
      drop-shadow(0 0 16px #edc24e3d);
    animation: castShine 7s linear infinite;
  }
}

/* -120% to 120% rather than 0 to 100%: the band has to start and finish entirely OFF the text,
   or the loop has a visible moment where it sits still at one end. */
@keyframes castShine {
  from {
    background-position:
      -120% 0,
      0 0;
  }
  to {
    background-position:
      220% 0,
      0 0;
  }
}

/* The shine is a flourish and the outline is not. Reduced motion keeps the metal and the edge
   and parks the highlight where it looks best, which is just off centre. */
@media (prefers-reduced-motion: reduce) {
  .cast,
  .castSm {
    animation: none;
    background-position:
      42% 0,
      0 0;
  }
}

/* ─── cast gold, small ─────────────────────────────────────────────────────────
 * The same metal for type between about 13 and 20px: your name, the panel headings, the
 * scoreboard's leader.
 *
 * It is a SEPARATE class and not just `.cast` at a smaller size, because the full ramp has
 * eight stops and at 15px there are not eight rows of pixels to put them in — that is the mush
 * documented on `.cast`. This one has four: dark, highlight, body, dark. Enough to read as
 * metal, few enough that each one gets more than a pixel.
 *
 * No drop-shadow glow either. At this size a bloom is a blur, and these sit in the top bar where
 * something is always moving behind them. The thin edge stays, because without it pale gold on
 * a translucent panel over bright water is not type, it is a suggestion. */
.castSm {
  /* Layer one: gold text with a real drop shadow. Opaque, unclipped, ordinary.
   *
   * This is BOTH the fallback and the shadow. An engine that cannot do text-clipping stops
   * here and gets gold type with a shadow under it, which is a perfectly good result — the rule
   * never has a failure mode worse than not having written it.
   *
   * Three shadow layers: a hard offset that reads as thickness, a soft one that lifts it off
   * the panel, and a warm bloom so the gold looks like it is giving light rather than sitting
   * on top of something dark. */
  color: var(--gold);
  text-shadow:
    0 2px 0 #000,
    0 4px 10px #000c,
    0 0 20px #edc24e59;
  position: relative;
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  /* Layer two: the metal, as a second copy of the text laid exactly on top.
   *
   * THE ORDER IS THE WHOLE POINT, and getting it the other way round is what the first attempt
   * did. `background-clip: text` makes the gold part of the element's BACKGROUND, and a
   * background paints before negative-z children — so a shadow copy behind it covered the gold
   * instead of sitting under it, and the name went dark again.
   *
   * Inverted, both layers land where they belong: the parent's inline text (and its shadow)
   * paints first, then this positioned pseudo-element paints over it. No filter anywhere, so
   * nothing gets its own compositing layer and the bug that started all this cannot come back.
   *
   * `data-text` is written by castText() in main.js; no attribute simply means no second layer.
   */
  .castSm[data-text]::after {
    content: attr(data-text);
    position: absolute;
    inset: 0;
    background: linear-gradient(
      180deg,
      #a06a12 0%,
      var(--gold-hi) 34%,
      var(--gold) 62%,
      #8a5a10 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    /* The thin edge, as a stroke rather than drop-shadows — no filter, no layer. Under the
       fill, or it eats half a pixel off every stem, which at this size is most of the stem. */
    -webkit-text-stroke: 0.9px #140d02;
    paint-order: stroke fill;
    text-shadow: none;
    /* Match the parent's clipping exactly, or a long name's metal outruns its own shadow. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    pointer-events: none;
  }
}

.edge {
  position: relative;
  border: 2px solid transparent;
  background:
    /* Texture first, so it lies over the fill and under the content. All three are clipped to
       the padding box, or the weave would run through the animated border and muddy it. */
    var(--weave) padding-box,
    /* The landing page's maroon, rising from the top of every panel: the same light the menu
       backdrop is lit with, faint enough that the panel is still dark where the text is. */
    radial-gradient(130% 70% at 50% 0%, #3a141f59, #2a101800 70%) padding-box,
    linear-gradient(var(--panel-fill, #11161ee6), var(--panel-fill, #11161ee6))
      padding-box,
    /* Centred on the bottom-right corner rather than the middle: the sweep then runs across the
       whole panel in one direction instead of pinwheeling about its centre, which reads as light
       moving past rather than as a spinning wheel. */
    conic-gradient(
        from var(--spin) at 100% 100%,
        #edc24e,
        #cc9434,
        #c4683a,
        #a83442,
        #801e32,
        #b85c54,
        #edc24e
      )
      border-box;
  animation: spin 28s linear infinite;
}

/* Browsers without @property animate nothing and simply show a static gradient, which is a fine
 * place to land. Safari below 16.4 is the realistic case. */
@supports not (background: conic-gradient(from 0deg, red, blue)) {
  .edge {
    border-color: #edc24e66;
    background:
      var(--weave) padding-box,
      linear-gradient(
          var(--panel-fill, #11161ee6),
          var(--panel-fill, #11161ee6)
        )
        padding-box;
  }
}

@media (prefers-reduced-motion: reduce) {
  .edge {
    animation: none;
  }
}

/* ─── top bar ─────────────────────────────────────────────────────────────────
 * Everything you glance at lives here, including the loadout, which used to sit at the bottom of
 * the screen under the chat where nobody looked at it.
 */

#hud {
  display: none;
}
#hud.on {
  display: block;
}

/* Tiny diagnostics badge: useful while tuning the live game, quiet enough to disappear into the
   lower-right chrome during normal play. Server values refresh every two seconds; FPS is local. */
#perf {
  display: none;
  position: fixed;
  right: var(--gap);
  bottom: var(--gap);
  z-index: 11;
  align-items: center;
  gap: 5px;
  padding: 4px 7px;
  border: 1px solid #ffffff12;
  border-radius: 8px;
  background: #0d1117b8;
  color: #8f9bad;
  font: 9px/1 var(--nums);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  backdrop-filter: blur(8px) saturate(1.2);
  -webkit-backdrop-filter: blur(8px) saturate(1.2);
}
#hud.on ~ #perf {
  display: flex;
}
#perfFps {
  color: var(--gold);
}
.perfSep {
  color: #ffffff2e;
}

#topbar {
  --panel-fill: #0d1117d9;
  position: fixed;
  top: var(--gap);
  left: var(--gap);
  right: calc(var(--side) + var(--gap) * 2);
  height: var(--bar);
  z-index: 5;
  border-radius: 10px;
  backdrop-filter: blur(10px) saturate(1.3);
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
  box-shadow: 0 6px 28px #0009;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 7px 14px;
  font-size: 13px;
}

#topbar .sep {
  width: 1px;
  height: 18px;
  background: #ffffff1f;
}

/* `#who` and `#rank` used to live up here, when the name was in the top bar. It moved to the
   side card and these two rules stayed behind — same id, same element, both still applying.
   That was invisible until the name became cast gold: the orphan's `text-shadow` beat the
   `.castSm` one on id specificity, and a shadow under transparent glyphs paints straight
   through the inside of every letter. The name came out as a dark smudge. */

/* Nothing in the top bar may move when a number changes.
 *
 * Two separate causes, both fixed here. Proportional digits are the obvious one — "1" is
 * narrower than "8", so a ticking clock shuffles everything to its right. Tabular figures solve
 * that for the monospace values. The other is the values that change LENGTH: ping goes 9ms ->
 * 115ms, alive goes 9 -> 16, the map name changes entirely. No figure setting helps there, so
 * each value reserves its own width and the bar simply cannot reflow.
 *
 * `inline-block` matters too: the bump animation scales these, and a scaled inline box
 * contributes its unscaled size to layout only if it is a block. */
.stat {
  display: flex;
  align-items: baseline;
  gap: 7px;
}
/* Was 10px in --dim, which is about 3:1 against the bar and sits below the readable threshold
   for type this small. Up a point, and lifted toward the ink: these are labels you read out of
   the corner of your eye mid-fight, not footnotes. */
.stat .k {
  color: #aab6c8;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
/* The SAME SIZE as the grey label beside it, not bigger.
 *
 * These were 15px against an 11px label, which made every stat a heading with a caption — three
 * of them in a row, each shouting, and the only number in the bar that actually wants to shout
 * is the clock. Matched in size, the pairing reads as one item, and the ACCENT does the work
 * that size was doing: the label is grey, the value is gold, and the eye still lands on the
 * value without the bar having to be loud about it.
 *
 * 600 weight stays. Dropping that too would make the value recede behind its own label. */
.stat .v {
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  font-weight: 600;
  /* The accent, not plain ink. These three are the numbers you glance at without stopping, and
     colour is what makes a glance work — the labels beside them stay grey so the value is the
     thing the eye lands on. */
  color: var(--gold);
  display: inline-block;
  text-align: left;
}
#vPing {
  min-width: 5.2ch;
}
#vOnline {
  min-width: 2.2ch;
}
#vMap {
  min-width: 9ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The round clock. The single most-glanced-at number on screen, so it is the biggest one. */
#clock {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
#clock .v {
  /* The display face. It is the biggest number on screen and the one you take in at a glance
     rather than compare digit by digit, which is exactly what a display face is for.
     Clegan has no tabular figures, so `min-width` in ch does the job `font-variant-numeric`
     used to: the box is reserved at its widest and the bar cannot jump as the digits change.
     Centred inside that box, or a narrow "1:05" would drift left of where "10:05" sat. */
  font-family: var(--display);
  font-weight: 400;
  font-size: 28px;
  line-height: 1;
  color: var(--gold);
  letter-spacing: 0.04em;
  display: inline-block;
  min-width: 4.8ch;
  text-align: center;
}
#clock .v.cast {
  /* The same solid lift as the landing title: a hard underside, a soft cast shadow and a warm
     bloom. The cast-gold sweep remains visible because this is a filter around transparent,
     background-clipped glyphs rather than a text-shadow painted through them. */
  filter: var(--cast-edge) drop-shadow(0 3px 0 #000)
    drop-shadow(0 6px 12px #000c) drop-shadow(0 0 22px #edc24e66);
}
#clock .k {
  color: var(--gold);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  /* A simpler horizontal glint at this size; the full eight-stop cast ramp turns ten-pixel type
     muddy. It still gets the familiar black depth and warm bloom. */
  background: linear-gradient(
    100deg,
    #a06a12 0%,
    var(--gold) 34%,
    #fff4c4 50%,
    var(--gold) 66%,
    #a06a12 100%
  );
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 0 #000) drop-shadow(0 4px 8px #000b)
    drop-shadow(0 0 14px #edc24e52);
  animation: timerShine 6s linear infinite;
}
/* The last minute. Now that the accent is itself a red, `--bad` no longer reads as an alarm
   next to it — it is darker and less saturated than the resting colour, which is backwards for
   a warning. White-hot and blinking is the signal instead: brightness, not hue. */
#clock.low .v {
  background: none;
  -webkit-text-fill-color: #fff;
  color: #fff;
  filter: drop-shadow(0 3px 0 #000) drop-shadow(0 6px 12px #000c)
    drop-shadow(0 0 12px #ff3b2f) drop-shadow(0 0 26px #ff3b2f66);
  animation: tick 1s steps(2) infinite;
}
@keyframes timerShine {
  from {
    background-position: 130% 0;
  }
  to {
    background-position: -130% 0;
  }
}
@keyframes tick {
  50% {
    opacity: 0.55;
  }
}

/* Connectivity, as a dot. Red by default, because every state that is not "open" is one you
   want to notice, including ones added later that this stylesheet has never heard of. */
/* Watching: the parts of the HUD that describe a player you do not have. */
body.observing #gear,
body.observing #xp,
body.observing #cardStats,
body.observing #avatar,
body.observing #say {
  display: none;
}
body.observing #who {
  color: var(--dim);
}

#conn {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: 0 0 auto;
  /* Sat on the ping's baseline, which puts it under the text rather than beside it. */
  align-self: center;
  margin-left: 1px;
  background: var(--bad);
  box-shadow:
    0 0 0 2px #0006,
    0 0 8px currentColor;
  color: var(--bad);
}
#conn[data-state="open"] {
  background: var(--good);
  color: var(--good);
}
/* Amber, not the accent. The dot is nine pixels wide and exists to be read without being
   looked at, which only works while its three states are three different hues. The accent
   used to be blue and could carry "in flight"; now that it is a red it collides with the
   failure state at that size. Red / amber / green survives the glance. */
#conn[data-state="reconnecting"],
#conn[data-state="connecting"] {
  background: var(--warn);
  color: var(--warn);
  animation: tick 1s steps(2) infinite; /* in-flight, not settled */
}

/* loadout */
#gear {
  display: flex;
  gap: 14px;
  align-items: center;
  position: relative;
}
/* Same again for the loadout. 16px numbers next to a 13px cap was the same mismatch, and these
   sit in the same row as the stats — three type sizes across one bar is two too many. */
.pip {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  font-weight: 600;
  color: var(--gold);
}
/* 1.5x. These are the only pictures in the bar and they were losing to the text beside them. */
.pip svg {
  width: 20px;
  height: 20px;
  display: block;
}
.pip .n {
  display: inline-block;
  min-width: 1.2ch;
  text-align: right;
}
.pip .cap {
  color: #9fabbd;
  font-size: 13px;
} /* matches .pip itself — see above */
.pip.max .n {
  color: var(--gold);
}
/* Carrying at least one. The glow is the part that reads without being looked at; the number is
   there for when you do look. */
.pip.on {
  color: var(--gold);
}
.pip.on svg {
  filter: drop-shadow(0 0 6px #edc24e88);
}
.pip:not(.on) {
  opacity: 0.5;
}

/* The flash a value does when it changes. Colour plus a little scale: motion catches the eye in
 * peripheral vision in a way a colour change alone does not. */
@keyframes bump {
  0% {
    transform: scale(1);
    color: var(--gold);
    text-shadow: 0 0 10px #edc24e;
  }
  35% {
    transform: scale(1.45);
    color: #fff;
    text-shadow: 0 0 16px #edc24e;
  }
  100% {
    transform: scale(1);
    color: inherit;
    text-shadow: none;
  }
}
.bump {
  display: inline-block;
  animation: bump 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}

/* A quarter of the jump. Used where a number changes every second and the full bump would be a
 * strobe rather than a signal. */
@keyframes bumpCalm {
  0% {
    transform: scale(1);
    color: var(--gold);
  }
  35% {
    transform: scale(1.11);
    color: #fff;
  }
  100% {
    transform: scale(1);
    color: inherit;
  }
}
.bump-calm {
  display: inline-block;
  animation: bumpCalm 0.45s ease-out;
}

@keyframes bumpBad {
  0% {
    transform: scale(1);
    color: var(--bad);
  }
  35% {
    transform: scale(1.45);
    color: #fff;
    text-shadow: 0 0 16px var(--bad);
  }
  100% {
    transform: scale(1);
    color: inherit;
  }
}
.bump-bad {
  display: inline-block;
  animation: bumpBad 0.45s cubic-bezier(0.2, 1.4, 0.4, 1);
}

.right {
  margin-left: auto;
  display: flex;
  gap: 10px;
  align-items: center;
}

/* The audio dock: both volume controls and what is playing, bottom left, one pill.
 *
 * One set of elements for both screens. The music starts on the menu, so the control for it has
 * to be on the menu — and a second set of widgets there, kept in step with these, is two sources
 * of truth and a sync bug waiting to happen. This just moves. It sits above `#menu` (z-index 10)
 * so it survives the overlay.
 *
 * Bottom left because that is the one corner nothing else wants: the board is centred, the HUD
 * is along the top, the side panel is right, and the mascot is top right. It is also where every
 * music player on earth puts this, which is worth more than any argument about it. */
#dock {
  position: fixed;
  z-index: 11;
  left: var(--gap);
  bottom: var(--gap);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px 4px;
  border-radius: 11px;
  background: #0d1117b8;
  backdrop-filter: blur(10px) saturate(1.3);
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
  border: 1px solid #ffffff12;
  /* Collapsed it is two icons and a few words. It has to stay out of the way of the board, so
     nothing in here is allowed to grow it past about a sixth of a 1080-wide window. */
  max-width: min(42vw, 340px);
}

/* ─── now playing ─────────────────────────────────────────────────────────────
 * A three-bar equaliser and the track name. The bars are the only thing that says "this is
 * audio" without a word of label, and they stop when the music does — which is also the answer
 * to "is it muted or is it just quiet right now".
 */
#nowPlaying {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0; /* or the ellipsis below never triggers */
  padding: 0 8px 0 5px;
  font-family: var(--nums);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  transition:
    color 0.2s,
    opacity 0.2s;
}
#nowPlaying .t {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Playing: the name takes the accent and the bars move. */
#dock.playing #nowPlaying {
  color: var(--gold);
}

.eq {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 11px;
  flex: 0 0 auto;
}
.eq b {
  display: block;
  width: 2px;
  /* Full height, squashed by a transform from main.js. Writing `height` sixty times a second
     forced a layout of the dock every frame; a scale is composited and costs nothing. */
  height: 11px;
  border-radius: 1px;
  background: currentColor;
  transform-origin: bottom;
  transform: scaleY(0.27);
}
/* The bars are driven from the ANALYSER, not from a keyframe — see syncMeter() in main.js.
 *
 * They were three canned animations on periods that did not divide into each other, which is
 * the standard trick for faking a level, and it is convincing for about four seconds. The
 * difference between a loop pretending to be a level and an actual level is the entire reason
 * this exists, and you can tell instantly without being able to say why: real bars land on the
 * beat. The transition is what fills the gap between frames at low refresh rates. */
.eq b {
  transition: transform 0.06s linear;
}

/* Stopped: the bars hold at their shortest and the whole readout steps back. */
#dock:not(.playing) #nowPlaying {
  opacity: 0.55;
}

/* The dock breathes with the kick. `--bass` is 0..1, written once a frame by syncMeter().
 *
 * This is the only part of the in-game visualiser that is not inside the readout, and it is
 * deliberately almost nothing: a glow that reaches eighteen pixels at its loudest, in the
 * corner you already look at for the track name. A round is not the place for a light show. */
#dock {
  box-shadow:
    0 4px 18px #0007,
    0 0 calc(8px + var(--bass, 0) * 20px)
      rgba(237, 194, 78, calc(var(--bass, 0) * 0.3));
}

@media (prefers-reduced-motion: reduce) {
  .eq b {
    transition: none;
  }
  #dock {
    box-shadow: 0 4px 18px #0007;
  }
}

/* Narrow windows lose the side panel and the board takes the lot, so the words go and the two
   controls stay. The icons are the part you need; the title is the part that is nice to have. */
@media (max-width: 760px) {
  #nowPlaying .t {
    display: none;
  }
  #nowPlaying {
    padding-right: 4px;
  }
}

button {
  font: inherit;
  font-size: 12px;
  color: var(--gold);
  background: transparent;
  border: 1px solid #edc24e77;
  border-radius: 5px;
  padding: 5px 12px;
  cursor: pointer;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition:
    background 0.12s,
    color 0.12s;
}
button:hover {
  background: var(--gold);
  color: #11161e;
}
button:disabled {
  opacity: 0.4;
  cursor: default;
}
button:disabled:hover {
  background: transparent;
  color: var(--gold);
}

/* Only the fader uses a range input, and it draws every part of itself below. This is the
   fallback for any engine that ignores the ::-webkit / ::-moz pseudo-elements. */
input[type="range"] {
  width: 64px;
  accent-color: var(--gold);
}

/* Volume: one control, not three widgets.
 *
 * Music used to be a text button plus a slider, and sfx a label plus a slider — four boxes and
 * about 200px of a bar that also has to hold a clock, a loadout and a connection state. Here the
 * icon IS the mute toggle, the level is a bar under the glyph so it is readable while collapsed,
 * and the slider unrolls only when you are actually reaching for it.
 *
 * focus-within as well as hover, so it is reachable by keyboard: tabbing to the slider opens the
 * control, and it cannot close while the thing you are dragging is inside it. */
.vol {
  position: relative; /* the fader above it is absolutely placed */
  display: flex;
  align-items: center;
  border-radius: 9px;
  border: 1px solid transparent;
  transition:
    background 0.14s,
    border-color 0.14s;
}
.vol:hover,
.vol:focus-within {
  background: #ffffff10;
  border-color: #ffffff22;
}

.volBtn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  position: relative;
  background: none;
  border: none;
  border-radius: 8px;
  color: var(--dim);
  transition: color 0.12s;
}
.volBtn svg {
  width: 18px;
  height: 18px;
  display: block;
}

/* Skip back and skip forward. Narrower than play/pause and without a level bar, because they
   are not a level — the asymmetry is what makes the middle one read as the one you press most. */
.volBtn.skip {
  width: 24px;
  height: 30px;
  color: #6f7b8c;
}
.volBtn.skip svg {
  width: 14px;
  height: 14px;
}
.volBtn.skip:hover,
.volBtn.skip:focus-visible {
  color: var(--ink);
  background: #ffffff10;
}

/* The level bar belongs to the two FADER buttons and to nothing else. On a transport button it
   would be a progress bar that never moves, which is worse than no bar at all. */
.volBtn.skip::after,
#musicBtn::after {
  content: none;
}
#musicBtn {
  width: 30px;
}
#musicBtn.on {
  color: var(--gold);
}

/* A hairline between the transport and the two faders. Three buttons then two buttons reads as
   five buttons without it, and the pill is small enough that five of anything is a row. */
#dock .sep {
  width: 1px;
  height: 16px;
  flex: 0 0 auto;
  margin: 0 4px;
  background: #ffffff1f;
}
.vol:hover .volBtn,
.volBtn:focus-visible {
  color: var(--ink);
}
.vol.on .volBtn {
  color: var(--gold);
}

/* The level, as a rule under the glyph. --lvl is set from JS, 0 to 1. Collapsed, this is the
   only thing telling you where the volume is, so it stays visible at all times. */
.volBtn::after {
  content: "";
  position: absolute;
  left: 7px;
  right: 7px;
  bottom: 5px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(
    90deg,
    currentColor 0,
    currentColor calc(var(--lvl, 0.5) * 100%),
    #ffffff1f calc(var(--lvl, 0.5) * 100%),
    #ffffff1f 100%
  );
}
.vol.muted .volBtn::after {
  background: #ffffff1f;
}

/* ─── the fader ───────────────────────────────────────────────────────────────
 * Slides UP out of its button, not sideways.
 *
 * Sideways it pushed everything to its right along, so opening the music fader moved the sfx
 * button and the track name under the pointer — and the dock is in a corner, where growing
 * sideways is the one direction that costs layout. Upward it grows into empty screen and
 * nothing else in the pill moves.
 *
 * Height, not display: an element that is display:none cannot animate.
 */
.slot {
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 30px;
  margin-left: -15px;
  height: 0;
  opacity: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px 8px 0 0;
  background: #0d1117f2;
  border: 1px solid #ffffff14;
  border-bottom: 0;
  pointer-events: none;
  transition:
    height 0.16s ease,
    opacity 0.16s ease;
}
/* `bottom: 100%` with no gap, on purpose: a few pixels of space between the button and the
   fader is a dead zone the pointer crosses on the way up, and crossing it closes the thing you
   were reaching for. Flush, the two read as one strip. */
.vol:hover .slot,
.vol:focus-within .slot {
  height: 78px;
  opacity: 1;
  pointer-events: auto;
}

/* Laid out horizontally and turned, which is the one approach that behaves the same in every
 * engine: `appearance: slider-vertical` is deprecated in Chrome and the
 * `writing-mode`/`direction` replacement is not in Safari yet.
 *
 * EVERY part is drawn by hand. `accent-color` alone leaves the native control's own geometry,
 * and a native thumb is a ~40px lozenge that, stood on its end in a 30px column, looks like a
 * bug rather than like a fader. Appearance is stripped and the track and thumb are rebuilt at
 * the size this actually wants. */
.slot input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 62px;
  height: 12px;
  margin: 0;
  padding: 0;
  background: transparent;
  transform: rotate(-90deg);
  cursor: pointer;
}
.slot input[type="range"]:focus {
  outline: none;
}
.slot input[type="range"]:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
}

/* The track carries the level as a hard-stop gradient, so the filled part below the thumb is
   the accent and the rest is a groove. `--lvl` is set on the .vol by syncAudio(). */
.slot input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(
    90deg,
    var(--gold) 0 calc(var(--lvl, 0.5) * 100%),
    #ffffff26 calc(var(--lvl, 0.5) * 100%) 100%
  );
}
.slot input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -3.5px;
  border-radius: 50%;
  background: var(--gold);
  border: 2px solid #0d1117;
  box-shadow: 0 0 0 1px #0006;
}
.slot input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: #ffffff26;
}
.slot input[type="range"]::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: var(--gold);
}
.slot input[type="range"]::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--gold);
  border: 2px solid #0d1117;
}
/* Muted: the fader is still usable, it just stops claiming to be carrying anything. */
.vol.muted .slot input[type="range"]::-webkit-slider-runnable-track {
  background: #ffffff26;
}
.vol.muted .slot input[type="range"]::-webkit-slider-thumb {
  background: #7b8698;
}
.vol.muted .slot input[type="range"]::-moz-range-progress {
  background: #7b8698;
}
.vol.muted .slot input[type="range"]::-moz-range-thumb {
  background: #7b8698;
}

@media (prefers-reduced-motion: reduce) {
  .slot {
    transition: none;
  }
}

/* Icon-only in the top bar — the GitHub mark needs no label, and the bar is crowded. Sized to a
 * comfortable click target rather than to the glyph. */
.iconBtn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  color: var(--dim);
  text-decoration: none;
  border: 1px solid transparent;
  transition:
    color 0.12s,
    border-color 0.12s,
    background 0.12s;
}
.iconBtn:hover {
  color: var(--ink);
  border-color: #ffffff2e;
  background: #ffffff12;
}
.iconBtn.bug:hover {
  color: var(--bad);
  border-color: #e8655a55;
  background: #e8655a18;
}
.iconBtn svg {
  width: 22px;
  height: 22px;
  display: block;
}

/* ─── side panel ──────────────────────────────────────────────────────────────
 * Who you are, who is winning, and the chat — in the space the board does not need. Everything
 * here is glanceable between bombs, which is why the live standings live here rather than behind
 * a key you have to hold down.
 */

#side {
  --panel-fill: #0d1117d4;
  position: fixed;
  z-index: 5;
  top: var(--gap);
  right: var(--gap);
  bottom: var(--gap);
  width: var(--side);
  border-radius: 10px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  backdrop-filter: blur(10px) saturate(1.2);
  -webkit-backdrop-filter: blur(10px) saturate(1.2);
  box-shadow: 0 6px 28px #0009;
}

#card {
  border-bottom: 1px solid #ffffff14;
  padding-bottom: 11px;
  /* 86px, up from 76. At 76 the face overhung the level line and the XP bar, and a bar you
     cannot see the end of is a bar you cannot read. Decoration does not sit on information. */
  padding-right: 86px;
}

/* The mascot's face, in the very top right corner, over everything.
 *
 * `object-fit: contain` and no crop: the art IS the head already, so cropping it further takes
 * the bandana off. No disc and no ring either — the alpha is the shape, and a plate behind it
 * would turn a character into a sticker. The z-index puts it above the bar and the panel it
 * overlaps; the menu is higher still, so it does not float over the landing page.
 */
/* ─── dead ────────────────────────────────────────────────────────────────────
 * Two signals, both at the top right, both saying the same thing in different ways.
 *
 * The name takes the colour of the banner that just crossed the board, which is what ties the
 * two together — you read YOU DIED in the middle and then your own name is still that colour
 * when you look away from it. And the mascot loses his colour entirely: a grey face is read as
 * "out" instantly and in peripheral vision, with no words and no icon.
 *
 * Both transition rather than cutting, because coming back to life should feel like colour
 * returning rather than like a state flag flipping.
 */
body.dead #who {
  /* An id beats a class, and `.castSm` paints its metal through a pseudo-element on top — so
     recolouring the base alone would be invisible. The overlay goes with it. */
  color: #ff5a5a;
  text-shadow:
    0 0 18px #c4140e,
    0 2px 0 #000;
}
body.dead #who::after {
  opacity: 0;
}
body.dead #who,
body.dead #avatar {
  transition:
    opacity 0.5s,
    filter 0.5s,
    color 0.5s;
}
body.dead #avatar {
  filter: grayscale(1) brightness(0.72) drop-shadow(0 6px 14px #000c);
}
body.dead #rank {
  color: #8a5a5a;
}

/* The mascot, and the only thing in the HUD that moves with the music.
 *
 * `--bob` and `--kick` are written once a frame by main.js off the listener. The CSS animation
 * is still the baseline — a slow six-second float that runs whether or not anything is playing
 * — and the custom properties ride ON TOP of it through a second transform on the wrapper, so
 * silence leaves him doing exactly what he always did.
 *
 * He is the right home for this in a way the coast was not: he is in the corner, he is already
 * moving, and nothing about the game is read through him. A visualiser can live anywhere your
 * eye does not have to go. */
#avatar {
  /* Up 6px and 10% smaller, for the same reason as the card's padding above. */
  position: fixed;
  top: 4px;
  right: 20px;
  width: 95px;
  height: 95px;
  object-fit: contain;
  /* Above the bar it overlaps, below the menu. pointer-events:none is what keeps it from eating
     the leave button and the volume sliders underneath it. */
  z-index: 9;
  pointer-events: none;
  /* The glow swells on the kick. Cheap, and it is what makes him read as lit by the music
     rather than merely jiggled by it. */
  filter: drop-shadow(0 6px 14px #000c)
    drop-shadow(
      0 0 calc(14px + var(--kick, 0) * 26px)
        rgba(237, 194, 78, calc(0.26 + var(--kick, 0) * 0.4))
    );
  animation: bob 6s ease-in-out infinite;
  /* The music's half: a lift on the bass and a squash on the beat, on top of the float. */
  translate: 0 calc(var(--bob, 0) * -14px);
  scale: calc(1 + var(--kick, 0) * 0.07) calc(1 - var(--kick, 0) * 0.05);
  transition:
    translate 0.05s linear,
    scale 0.05s linear;
}
@media (max-width: 1080px) {
  #avatar {
    display: none;
  }
}

/* The mascot's speech bubble.
 *
 * Above everything, including the overlays: the whole point is that it is the one thing in the
 * corner that moves, and a bubble that hides behind the death screen would only ever be seen
 * when nothing was happening. */
#say {
  position: fixed;
  top: 24px;
  right: 96px;
  max-width: 320px;
  z-index: 60;
  pointer-events: none;
  padding: 9px 14px;
  border-radius: 12px;
  background: #11161ef2;
  border: 1.5px solid #edc24e66;
  box-shadow: 0 10px 28px #000b;
  color: var(--ink);
  font-size: 14px;
  line-height: 1.35;
  text-wrap: balance;
  /* Hidden, not display:none: a transform animation needs a box to animate. Scaled from the
     right-hand edge so it unfolds out of his mouth rather than growing from its own middle. */
  opacity: 0;
  transform: scale(0.7) translateX(14px);
  transform-origin: 100% 50%;
  visibility: hidden;
}
/* The tail, as a rotated square tucked under the right edge, so it inherits the same fill and
   border without a second element or an SVG. */
#say::after {
  content: "";
  position: absolute;
  right: -7px;
  top: 50%;
  width: 12px;
  height: 12px;
  background: #11161ef2;
  border-right: 1.5px solid #edc24e66;
  border-top: 1.5px solid #edc24e66;
  transform: translateY(-50%) rotate(45deg);
}
#say.show {
  visibility: visible;
  animation: sayIn 0.9s cubic-bezier(0.2, 1.5, 0.4, 1) forwards;
}

/* Quick in with a little overshoot, then hold. The hold is what makes it readable; the fade out
   is driven from JS after 2.5s so the timing lives in one place. */
@keyframes sayIn {
  0% {
    opacity: 0;
    transform: scale(0.7) translateX(14px);
  }
  55% {
    opacity: 1;
    transform: scale(1.04) translateX(0);
  }
  100% {
    opacity: 1;
    transform: scale(1) translateX(0);
  }
}
#say:not(.show) {
  transition:
    opacity 0.25s ease,
    transform 0.25s ease;
}
@media (max-width: 1080px) {
  #say {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  #say.show {
    animation: none;
    opacity: 1;
    transform: none;
    visibility: visible;
  }
}
/* The text face, not the display one. This is a name read at a glance beside the mascot, and
   A display face turns anything with a lowercase 'rn' or a '1' in it into a puzzle at 17px. */
/* Your name, beside the mascot's face. The display face, cast in gold: it is the one piece of
   text on screen that is about YOU, and the only reason it was ever the accent colour.
   No `text-shadow` here — an id beats a class, so declaring it would silently cancel the drop
   shadow `.castSm` provides, which is exactly what it did until this comment replaced it. */
#who {
  font-family: var(--display);
  font-weight: 400;
  font-size: 23px;
  letter-spacing: 0.04em;
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#rank {
  display: inline;
  color: var(--dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  margin-top: 0;
}
/* Where a passive upgrade announces itself: directly under the loadout pips, in the bar above
 * the board.
 *
 * It belongs with the thing it changed. Down in the side card it sat under kills/deaths/round,
 * which are a record of the match rather than of what you are carrying — and it was in the one
 * part of the screen you are not looking at while playing. The pip going from 0 to 1 is correct
 * and almost invisible; the badge is the loud half of the same event, and then it is gone,
 * leaving the pip as the permanent record.
 *
 * Anchored to #gear and absolutely positioned, so it hangs below the bar without being able to
 * change the bar's height or push anything in it sideways. */
#ups {
  position: absolute;
  left: 0;
  top: 100%;
  height: 0;
  overflow: visible;
  pointer-events: none;
  z-index: 7;
}
.up {
  position: absolute;
  left: 0;
  top: 10px;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 8px;
  border-radius: 999px;
  background: #11161ef2;
  border: 1px solid currentColor;
  box-shadow:
    0 6px 20px #000a,
    0 0 18px -4px currentColor;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  animation: upFloat 2.6s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}
.up svg {
  width: 15px;
  height: 15px;
  display: block;
}
.up .lvl {
  color: var(--dim);
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Up and out. Most of the travel is early, so the eye is caught by the movement and then has a
   still badge to actually read. */
@keyframes upFloat {
  0% {
    opacity: 0;
    transform: translateY(14px) scale(0.8);
  }
  12% {
    opacity: 1;
    transform: translateY(0) scale(1.06);
  }
  22% {
    transform: translateY(-2px) scale(1);
  }
  70% {
    opacity: 1;
    transform: translateY(-10px) scale(1);
  }
  100% {
    opacity: 0;
    transform: translateY(-30px) scale(0.96);
  }
}
@media (prefers-reduced-motion: reduce) {
  .up {
    animation: upFloat 2.6s steps(1) forwards;
  }
}

#cardStats {
  display: flex;
  gap: 16px;
  margin-top: 9px;
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
}
#cardStats .k {
  color: var(--dim);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-family: var(--text);
  margin-right: 4px;
}

/* Gold. The only stat with a picture instead of a word, because it is the only one that is a
   THING rather than a count of events — and because a coin is faster to recognise than the
   four letters it would take to spell it. */
.coinStat {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--gold);
}
.coinStat .coin {
  width: 13px;
  height: 13px;
  display: block;
  color: var(--gold);
  filter: drop-shadow(0 1px 0 #0008) drop-shadow(0 0 5px #edc24e4d);
}

/* Experience.
 *
 * The bar answers "how far to the next one" without anybody doing arithmetic, which is the only
 * thing a level means moment to moment. The numbers under it are into/need rather than
 * total/target: 3 093 of 26 957 is a distance you can feel, and 60 000 of 83 864 is not.
 */
/* The card reserves 76px on its right for the mascot's face, which the name and the stat row
   need to stay clear of. The XP bar does not: it sits well below the face, and stopping it short
   left a gap that read as a broken layout rather than as breathing room. Pull it back out to the
   full panel width. */
#xp {
  margin-top: 11px;
  margin-right: -76px;
}
/* `lvl 4 · Rookie` on the left, `3 093 / 26 957` on the right. The tier used to live two rows
   up next to the name; it belongs here, because it is the name of the level beside it. */
.xpHead {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 5px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}
#xpLevel {
  color: var(--gold);
  font-weight: 700;
}
/* The dot is a pseudo-element rather than text, so the gap closes cleanly on the one frame
   before a tier has arrived from the server — a stranded "·" is worse than no separator. */
#rank:not(:empty)::before {
  content: "·";
  color: var(--dim);
  margin-right: 6px;
}
/* Pushes the numbers to the right edge whatever the tier's length. `justify-content` cannot do
   it any more now that there are three children rather than two. */
#xpNums {
  margin-left: auto;
}
#xpNums {
  color: var(--dim);
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
}
/* A little air above the bar, so `lvl 1 · ROOKIE` and the track under it both clear the
   mascot's chin instead of tucking beneath it. */
#xp {
  margin-top: 8px;
}
#xpTrack {
  height: 7px;
  border-radius: 4px;
  background: #00000066;
  overflow: hidden;
  box-shadow: inset 0 1px 2px #0008;
}
#xpTrack {
  position: relative;
}
#xpFill {
  height: 100%;
  width: 0;
  border-radius: 4px;
  background: linear-gradient(90deg, #2f8f5b, #5ad18a 55%, #9bf0b8);
  box-shadow: 0 0 10px #5ad18a66;
  transition: width 0.5s cubic-bezier(0.2, 0.8, 0.3, 1);
}
/* At the top of the ladder there is nothing left to fill toward. */
#xp.maxed #xpFill {
  background: linear-gradient(90deg, #b490da, #7badd3, #78b39b);
}

/* ── levelling up ──
 *
 * The bar runs to full, flares, and drops back to the sliver the new level starts on. The reset
 * has its transition suppressed for exactly one frame, because animating the fill backwards to
 * near-zero looks like losing progress, which is the opposite of what just happened.
 */
#xp.levelup #xpFill {
  animation: xpFlare 0.9s ease-out;
  box-shadow:
    0 0 18px #5ad18a,
    0 0 36px #5ad18aaa;
}
#xp.levelup #xpTrack::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 7px;
  box-shadow: 0 0 0 2px #9bf0b8;
  animation: xpRing 0.9s ease-out forwards;
  pointer-events: none;
}
#xp.levelup #xpLevel {
  animation: xpPop 0.9s cubic-bezier(0.2, 1.5, 0.4, 1);
  color: #9bf0b8;
}
#xp.resetting #xpFill {
  transition: none;
}

@keyframes xpFlare {
  0% {
    filter: brightness(1);
  }
  18% {
    filter: brightness(2.2) saturate(1.4);
  }
  100% {
    filter: brightness(1);
  }
}
@keyframes xpRing {
  0% {
    opacity: 0.9;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(1.5);
  }
}
@keyframes xpPop {
  0% {
    transform: scale(1);
  }
  30% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}
@media (prefers-reduced-motion: reduce) {
  #xp.levelup #xpFill,
  #xp.levelup #xpLevel,
  #xp.levelup #xpTrack::after {
    animation: none;
  }
}

/* Panel headings. 11px and cast, up from 9.5px flat grey — the cast ramp needs the extra
   pixels, and these are labels you read once per session rather than data. */
.panelHead {
  margin: 0 0 7px;
  font-size: 11px;
  font-weight: normal;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.18em;
}

/* The live board. Rows are a fixed height with the place in a fixed-width slot, so a medal and a
 * plain "7" line up and the names start on one edge. Column heads say what the two numbers are —
 * "3/5" next to a name was a riddle for anyone who had not asked. */
.liveHead,
#liveRows li {
  display: grid;
  grid-template-columns: 24px 1fr 46px 46px;
  align-items: center;
  column-gap: 6px;
  padding: 0 8px 0 4px;
}
.liveHead {
  color: var(--dim);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding-bottom: 4px;
  margin-bottom: 3px;
  border-bottom: 1px solid #edc24e2b;
}
.liveHead span:nth-child(n + 3) {
  text-align: right;
}
#liveRows {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13px;
}
#liveRows li {
  height: 25px;
  border-radius: 5px;
  margin-bottom: 1px;
  font-variant-numeric: tabular-nums;
}
#liveRows li:nth-child(even):not(.p1):not(.p2):not(.p3) {
  background: #ffffff06;
}
#liveRows .pos {
  color: var(--dim);
  font-family: var(--nums);
  font-size: 11px;
  text-align: center;
}
#liveRows .nm {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#liveRows .kd {
  color: var(--dim);
  font-family: var(--nums);
  font-size: 11px;
  text-align: right;
}
#liveRows .sc {
  font-family: var(--nums);
  color: var(--ink);
  text-align: right;
  font-weight: 700;
}
#liveRows li.p1 .sc {
  color: var(--gold-hi);
}

/* A little robot in front of anything that is not a person.
 *
 * An inline SVG mask rather than an emoji: emoji render at a different size and baseline on
 * every platform, and half of them arrive in full colour, which would make the one thing in the
 * row that is not a player's own colour the loudest thing in it. As a mask it takes
 * currentColor, so it dims with the row and sits exactly on the text baseline. */
#liveRows li.bot .nm::before {
  content: "";
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: 5px;
  vertical-align: -1px;
  background: currentColor;
  opacity: 0.75;
  -webkit-mask-image: var(--robot);
  mask-image: var(--robot);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
/* You. The same gold edge as your row on the round-over table, so "that is me" looks the same
 * everywhere it is asked. Inset so it never shifts the row. */
#liveRows li.me {
  font-weight: 700;
  box-shadow: inset 3px 0 0 var(--gold);
}
#liveRows li.me:not(.p1):not(.p2):not(.p3) {
  background: #edc24e12;
}

/* The host. Small enough to be a mark rather than an ornament: it says who can restart the
 * round, and nothing more. Drawn as a mask so one SVG symbol can be tinted gold here. */
#liveRows li.host .nm::after,
#scoreRows td.host::after {
  content: "";
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-left: 5px;
  vertical-align: -1px;
  background: var(--gold);
  -webkit-mask: var(--crown) center / contain no-repeat;
  mask: var(--crown) center / contain no-repeat;
  opacity: 0.9;
}
#liveRows li.out .nm {
  opacity: 0.45;
}

#chat {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
#chatList {
  list-style: none;
  margin: 0 0 7px;
  padding: 0 2px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  font-size: 12.5px;
  scrollbar-width: thin;
}
#chatList li {
  padding: 1px 0;
}
#chatList li b {
  font-weight: 600;
}

/* Anything that is about you, picked out of a log that never stops scrolling. */
#chatList li.feed {
  color: var(--dim);
}
#chatList li.mine {
  background: #edc24e12;
  box-shadow: inset 2px 0 0 var(--gold);
  padding-left: 6px;
}
#chatList li.mention {
  background: #edc24e0d;
}
#chatList li.feed.kill {
  color: var(--gold);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--gold);
  padding-left: 6px;
}
#chatList li.feed.death {
  color: var(--bad);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--bad);
  padding-left: 6px;
}
#chatInput {
  width: 100%;
  font: inherit;
  font-size: 12.5px;
  color: var(--ink);
  background: #060a0fcc;
  border: 1px solid #ffffff1a;
  border-radius: 6px;
  padding: 6px 9px;
  opacity: 0.55;
  transition:
    opacity 0.15s,
    border-color 0.15s;
}
#chatInput:focus {
  outline: none;
  opacity: 1;
  border-color: var(--gold);
}

#deadBanner {
  position: fixed;
  display: grid;
  place-items: center;
  z-index: 8;
  top: calc(var(--bar) + var(--gap) * 2);
  left: var(--gap);
  width: calc(100vw - var(--side) - var(--gap) * 3);
  height: calc(100vh - var(--bar) - var(--gap) * 3);
  background: #0d111788;
  pointer-events: none;
  border-radius: 8px;
  text-align: center;
  font-family: var(--display);
  letter-spacing: 0.08em;
  text-indent: 0.08em;
  text-shadow: var(--rim);
  opacity: 0;
  transform: none;
}

#deadBanner .who {
  margin-top: -20px;
}

/* ─── kill streaks ────────────────────────────────────────────────────────────
 * Centre screen, over everything, for about a second and a half. Deliberately loud: a streak is
 * the one moment the game should interrupt you.
 */

/* Anchored to the board, which is the window minus the top bar and the side panel. A banner
 * centred on the viewport sits noticeably off to one side of the thing you are actually looking
 * at, and the one moment it has to be read is the one moment you have no attention to spare. */

#streak {
  grid-area: 1 / 1;
  transform: translateY(100px);
  text-align: center;
  /* See @keyframes streakIn: letter-spacing is added after the LAST glyph too, so a centred
     line needs a matching text-indent or it sits half the spacing left of centre. */
  font-family: var(--display);
  letter-spacing: 0.08em;
  text-indent: 0.08em;
  text-shadow: var(--rim);
  opacity: 0;
}
/* A streak is the one moment the game is allowed to interrupt you, so it gets four layers:
 * a band that whips across behind the type, the type itself landing with a punch, a rule that
 * expands out from under it, and the name. The old version was the type alone, which at 52px
 * on a board full of explosions was just more orange. */
#streak {
  position: relative;
}
#streak.show {
  animation: streakIn 2.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

/* The band. Skewed, wider than the screen, wiping out from the centre — it is what separates
   the announcement from the board behind it, and the reason the type reads over fire. */
#streak::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  /* px, not em: `#streak` has no font-size of its own, so `1.25em` resolved against the
     inherited 16px and drew a 20px ribbon behind 118px type — a pinstripe, not a band. */
  width: 150vw;
  height: 62px;
  margin-left: -75vw;
  transform: translateY(-62%) skewX(-13deg) scaleX(0);
  background: linear-gradient(
    90deg,
    #edc24e00 0%,
    #edc24e26 18%,
    #edc24e4d 50%,
    #edc24e26 82%,
    #edc24e00 100%
  );
  z-index: -1;
  animation: streakBand 2.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* The rule under it, expanding outward. Two moving parts reading as one event. */
#streak::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -6px;
  width: 0;
  height: 2px;
  margin-left: 0;
  background: linear-gradient(90deg, #edc24e00, var(--gold), #edc24e00);
  animation: streakRule 2.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

#streak .big,
#deadBanner .big {
  display: block;
  font-size: 56px;
  color: var(--gold);
  text-shadow:
    0 0 28px #edc24e,
    var(--banner-shadow);
}
/* The first two rungs are cast gold. The top two are not, and that is the escalation: once the
   metal has been established, going PAST it has to mean something, and white-hot reads as the
   thing that was gold and is now too bright to be gold. */
#streak.s2 .big,
#streak.s3 .big {
  background: var(--cast);
  background-size:
    260% 100%,
    100% 100%;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  text-shadow: none;
  filter: var(--cast-edge) drop-shadow(0 3px 2px #000a)
    drop-shadow(0 0 26px #edc24e66);
  /* Twice the speed of the menu title. This is on screen for two and a half seconds, so the
     sweep has to cross in about one or it never arrives. */
  animation: castShine 1.1s linear infinite;
}
#streak .who,
#deadBanner .who {
  display: block;
  font-size: 14px;
  color: var(--ink);
  margin-top: -300px;
  opacity: 0.85;
  font-family: var(--text);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
/* The name gets the accent and some weight: it is the half of the message that says whether
   this is a thing to celebrate or a thing to worry about. */
/* Who did it, two and a half times the size it was, and impossible to miss: a molten gradient —
 * gold, flame, rose, white-hot — pouring through the letters, under a glow that pulses with it.
 * The name is the half of the announcement that says whether to celebrate or to run. */
#streak .who {
  font-size: 32px;
  font-weight: 900;
  letter-spacing: 0.1em;
  margin-top: 10px;
  color: var(--gold);
  opacity: 1;
  text-shadow: none;
  background: linear-gradient(
    90deg,
    #ffd24a,
    #ff8a2a,
    #ff3f5e,
    #ffe9a8,
    #ffd24a,
    #ff8a2a
  );
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 0 10px #ff7a2acc) drop-shadow(0 3px 0 #000)
    drop-shadow(0 6px 14px #000c);
  animation:
    whoFlow 1.1s linear infinite,
    whoGlow 0.55s ease-in-out infinite alternate;
}
/* The robot keeps its own colour inside the gradient text, or it would be invisible. */
#streak .who .botIcon {
  background: #ffe9a8;
  width: 0.85em;
  height: 0.85em;
}
@keyframes whoFlow {
  to {
    background-position: 150% 0;
  }
}
@keyframes whoGlow {
  from {
    filter: drop-shadow(0 0 6px #ff7a2a99) drop-shadow(0 3px 0 #000)
      drop-shadow(0 6px 14px #000c);
  }
  to {
    filter: drop-shadow(0 0 18px #ffb347ff) drop-shadow(0 3px 0 #000)
      drop-shadow(0 6px 14px #000c);
  }
}
@media (prefers-reduced-motion: reduce) {
  #streak .who {
    animation: none;
  }
}

/* The robot, inline, wherever a bot's name is written — the same head the live scoreboard
 * draws (`--robot`), in the colour of the text around it. */
.botIcon {
  display: inline-block;
  width: 0.95em;
  height: 0.95em;
  margin-right: 0.25em;
  vertical-align: -0.12em;
  background: currentColor;
  -webkit-mask: var(--robot) center / contain no-repeat;
  mask: var(--robot) center / contain no-repeat;
}

/* Each rung is bigger and hotter than the last, so the escalation is visible as well as audible.
   Every step is a real jump — 66, 82, 100, 118 — because two sizes eight pixels apart are the
   same size to anybody who is not comparing them side by side. */
/* clamp() rather than fixed pixels, because the banner is centred on the BOARD — the window
   minus the 340px side panel — not on the window. At a fixed 118px, "RAMPAGE!!" is about 830px
   of type inside a 910px board on a 1280 screen: one narrow window away from wrapping, and a
   kill streak that wraps onto two lines is a paragraph. The middle term does the work; the
   outer two stop it from vanishing or exploding. */
/* 15% off every rung. They were sized for a banner in the middle of an empty screen; up at a
   third they sit over the board, and anything covering the board has to earn its width. */
#streak.s2 .big {
  font-size: clamp(27px, 3.5vw, 45px);
}
#streak.s3 .big {
  font-size: clamp(31px, 4.3vw, 56px);
}
#streak.s4 .big {
  font-size: clamp(35px, 5.3vw, 68px);
  color: #fff;
  text-shadow:
    0 0 44px #edc24e,
    0 0 90px #e8655a,
    var(--banner-shadow);
}
#streak.ace .big {
  font-size: clamp(39px, 6.2vw, 80px);
  color: #fff;
  text-shadow:
    0 0 48px #ffc98a,
    0 0 100px #edc24e,
    0 0 170px #b36bff,
    var(--banner-shadow);
}
/* The top two rungs get a hotter band and a wider rule as well, or the escalation lives entirely
   in the font size and stops being felt somewhere around the third one. */
#streak.s3::before {
  height: 75px;
}
#streak.s4::before,
#streak.ace::before {
  height: 91px;
  filter: brightness(1.4);
}
#streak.ace::before {
  background: linear-gradient(
    90deg,
    #edc24e00 0%,
    #b36bff33 15%,
    #edc24e66 50%,
    #b36bff33 85%,
    #edc24e00 100%
  );
}

/* Blood and molten gold.
 *
 * `--bad` alone was a flat salmon — the colour of an error message, not of dying. This is a
 * deep arterial red with gold heat bleeding out of it, which is the same trick the streak
 * banner uses one tier up and the reason the two now read as the same family.
 *
 * The shadow stack is SHARED with #streak (see --banner-shadow) so the two can never drift
 * apart again: they appear in the same place, seconds apart, and two different weights of drop
 * shadow on the same spot reads as one of them being broken. */
#deadBanner .big {
  color: #ff2f2f;
  text-shadow:
    0 0 26px #ff6b2f,
    0 0 60px #c4140e,
    var(--banner-shadow);
}
/* 4.4s, up from 1.4. The old one was gone before you had finished reading who killed you, which
   made the only piece of information in it useless. The dialog takes over the instant this
   clears, so the screen is never empty and the two never overlap. */
#deadBanner.show {
  animation: deadIn 4.4s ease-out forwards;
}
@keyframes deadIn {
  0% {
    opacity: 0;
    transform: scale(1.5);
  }
  5% {
    opacity: 1;
    transform: scale(1);
  }
  90% {
    opacity: 1;
    transform: scale(0.98);
  }
  100% {
    opacity: 0;
    transform: scale(0.96);
  }
}

/* The `translateX(-50%)` every one of these keyframes used to carry was left over from when
 * this was absolutely positioned. In the centring grid it does nothing but shove the banner
 * half its own width to the LEFT of the middle — on an ace, about 150px off centre. */
/* `text-indent` tracks `letter-spacing` on every frame, and that pairing is not optional.
 *
 * Letter spacing is added AFTER each glyph including the last one, so a centred line with
 * `letter-spacing: .5em` is laid out half an em wider than it looks and the visible text sits
 * half of that to the LEFT of centre. On the ace tier at 118px that is about 30px of drift that
 * appears and disappears during the animation — the banner slides sideways as it lands, which
 * looks like a bug because it is one. An equal `text-indent` pushes the line back. */
@keyframes streakIn {
  0% {
    opacity: 0;
    transform: scale(0.55) rotate(-4deg);
    filter: blur(7px);
    letter-spacing: 0.42em;
    text-indent: 0.42em;
  }
  9% {
    opacity: 1;
    transform: scale(1.22) rotate(1.5deg);
    filter: blur(0);
    letter-spacing: 0.04em;
    text-indent: 0.04em;
  }
  16% {
    opacity: 1;
    transform: scale(0.96) rotate(-0.5deg);
  }
  23% {
    opacity: 1;
    transform: scale(1.03) rotate(0);
  }
  30% {
    opacity: 1;
    transform: scale(1) translateY(0) rotate(0);
    letter-spacing: 0.08em;
    text-indent: 0.08em;
  }
  /* It does not simply sit there for two seconds.
   *
   * A slow drift upward with half a degree of roll either side, alternating — not enough to
   * read as an animation, enough that the banner is ALIVE while you are looking at it. A
   * notice that lands hard and then freezes reads as a screenshot pasted over the game. */
  45% {
    opacity: 1;
    transform: scale(1.012) translateY(-5px) rotate(0.45deg);
  }
  60% {
    opacity: 1;
    transform: scale(1) translateY(-9px) rotate(-0.4deg);
  }
  74% {
    opacity: 1;
    transform: scale(1.01) translateY(-13px) rotate(0.3deg);
  }
  82% {
    opacity: 1;
    transform: scale(1) translateY(-16px) rotate(0);
  }
  100% {
    opacity: 0;
    transform: scale(1.06) translateY(-34px);
  }
}

/* The band arrives a frame BEFORE the type and leaves before it too, so the announcement reads
   as something hitting the screen rather than as several things fading in together. */
@keyframes streakBand {
  0% {
    transform: translateY(-62%) skewX(-13deg) scaleX(0);
    opacity: 0;
  }
  7% {
    transform: translateY(-62%) skewX(-13deg) scaleX(1.1);
    opacity: 1;
  }
  14% {
    transform: translateY(-62%) skewX(-13deg) scaleX(1);
    opacity: 1;
  }
  70% {
    opacity: 0.8;
  }
  100% {
    transform: translateY(-62%) skewX(-13deg) scaleX(1);
    opacity: 0;
  }
}

@keyframes streakRule {
  0% {
    width: 0;
    margin-left: 0;
    opacity: 0;
  }
  12% {
    width: 0;
    margin-left: 0;
    opacity: 1;
  }
  34% {
    width: 336px;
    margin-left: -168px;
    opacity: 1;
  }
  80% {
    width: 336px;
    margin-left: -168px;
    opacity: 0.7;
  }
  100% {
    width: 416px;
    margin-left: -208px;
    opacity: 0;
  }
}

/* Reduced motion: it still announces, it just stops moving. The information is the point. */
@media (prefers-reduced-motion: reduce) {
  #streak.show {
    animation: streakHold 2.6s steps(1) forwards;
  }
  #streak::before,
  #streak::after {
    animation: none;
    opacity: 0.6;
    transform: translateY(-62%) skewX(-13deg);
  }
  @keyframes streakHold {
    0%,
    92% {
      opacity: 1;
    }
    100% {
      opacity: 0;
    }
  }
}

/* ─── emotes ──────────────────────────────────────────────────────────────────
 * The picker, while Shift is held.
 *
 * Over the board and centred on it, so it is where your eyes are — but it appears INSTANTLY
 * and leaves instantly, because it is on screen for about a third of a second and a fade-out
 * would still be going when you need to see what you just walked into.
 *
 * Nothing here takes a pointer event. It is a legend for keys you are already holding, not a
 * menu, and a menu you could click would be a second way to do the same thing with different
 * timing.
 */
#emotePick {
  position: fixed;
  z-index: 8;
  pointer-events: none;
  top: calc(var(--bar) + var(--gap) * 2);
  left: var(--gap);
  width: calc(100vw - var(--side) - var(--gap) * 3);
  height: calc(100vh - var(--bar) - var(--gap) * 3);
  display: none;
  place-items: center;
}
#emotePick.on {
  display: grid;
}
@media (max-width: 1080px) {
  #emotePick {
    width: calc(100vw - var(--gap) * 2);
  }
}

/* `.ekey`, not `.slot`. `.slot` is the volume fader's pop-out, three thousand lines up, and it
   sets `opacity: 0; height: 0` on itself — exactly the two properties a second component called
   the same thing would least like to inherit. Two unrelated things with one class name is a bug
   waiting for whichever one is edited next. */
.ring {
  position: relative;
  width: 190px;
  height: 190px;
}
.ring .ekey {
  position: absolute;
  display: grid;
  place-items: center;
  width: 58px;
  height: 58px;
  margin: -29px 0 0 -29px;
  border-radius: 50%;
  background: #0d1117e8;
  border: 1.5px solid #ffffff1f;
  box-shadow: 0 6px 20px #000a;
  /* Each one springs out from the middle, staggered by direction, so the cross assembles
     rather than appearing. 90ms total — fast enough that it has finished before you have
     chosen, which is the point: the animation must never be something you wait through.

     SCALE ONLY, and no `backwards`. Animating opacity with a backwards fill means the slot
     sits at zero until the animation starts — so anything that stops animations running at all
     leaves an invisible picker, which is a worse outcome than never having animated it. At
     worst this now pops in at full size. */
  animation: emotePop 0.09s cubic-bezier(0.2, 1.6, 0.4, 1);
}
.ring .up {
  left: 50%;
  top: 14%;
}
.ring .left {
  left: 14%;
  top: 50%;
  animation-delay: 0.02s;
}
.ring .down {
  left: 50%;
  top: 86%;
  animation-delay: 0.04s;
}
.ring .right {
  left: 86%;
  top: 50%;
  animation-delay: 0.06s;
}
@keyframes emotePop {
  from {
    transform: scale(0.3);
  }
  to {
    transform: scale(1);
  }
}

.ring .ekey b {
  font-size: 26px;
  line-height: 1;
}
/* The key cap, under the glyph. Small and dim: you need it once. */
.ring .ekey i {
  position: absolute;
  bottom: -4px;
  font-family: var(--nums);
  font-style: normal;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--gold);
  background: #0d1117;
  border-radius: 3px;
  padding: 1px 4px;
  border: 1px solid #ffffff1a;
}

@media (prefers-reduced-motion: reduce) {
  .ring .ekey {
    animation: none;
  }
}

/* ─── toast ───────────────────────────────────────────────────────────────────
 * z-index above the menu: the first thing it ever has to say is "server full".
 */

#toast {
  position: fixed;
  top: 54px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  background: #1a2029ee;
  border: 1px solid #edc24e66;
  border-radius: 6px;
  padding: 7px 16px;
  color: var(--gold);
  opacity: 0;
  transition: opacity 0.25s;
  pointer-events: none;
  font-size: 12px;
  letter-spacing: 0.08em;
}

/* A delayed player dossier, anchored beside whichever name was held for 2.5 seconds. Fixed so it
   is never clipped by the compact live board or the TAB scoreboard's scroll frame. */
.playerHoverTarget {
  cursor: help;
  text-decoration: underline;
  text-decoration-color: #edc24e00;
  text-underline-offset: 3px;
  transition:
    text-decoration-color 0.2s,
    filter 0.2s;
}
.playerHoverTarget:hover,
.playerHoverTarget:focus-visible {
  text-decoration-color: #edc24e70;
  filter: drop-shadow(0 0 7px #edc24e4d);
  outline: none;
}
#playerHover {
  --panel-fill: #121923fa;
  position: fixed;
  z-index: 40;
  width: 310px;
  padding: 18px;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px) scale(0.94) rotate(-0.4deg);
  transform-origin: 22px 0;
  box-shadow:
    0 22px 52px #000d,
    0 0 36px #edc24e26;
  transition:
    opacity 0.18s ease,
    transform 0.28s cubic-bezier(0.2, 1.45, 0.3, 1),
    visibility 0s linear 0.28s;
}
#playerHover::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(110deg, #fff0 35%, #f6e7a929 48%, #fff0 61%);
  background-size: 260% 100%;
  pointer-events: none;
  animation: dossierShine 4.8s linear infinite;
}
#playerHover.show {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) scale(1) rotate(0);
  transition-delay: 0s;
}
.phHead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
#phName {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--display);
  font-size: 20px;
  letter-spacing: 0.08em;
}
#phBadge {
  flex: 0 0 auto;
  color: var(--good);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.13em;
}
#phRank {
  margin-top: 3px;
  color: var(--dim);
  font-size: 12px;
}
.phSection {
  margin: 13px 0 7px;
  padding-top: 9px;
  border-top: 1px solid #edc24e24;
  color: var(--gold);
  font-size: 9px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2em;
}
.phGrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 14px;
}
.phGrid span {
  min-width: 0;
}
.phGrid i {
  display: block;
  color: var(--dim);
  font-size: 9px;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.phGrid b {
  display: block;
  margin-top: 2px;
  color: var(--ink);
  font-family: var(--nums);
  font-size: 14px;
  font-weight: 700;
}
.phMedalCell .tally {
  justify-content: flex-start;
}
@keyframes dossierShine {
  from {
    background-position: 130% 0;
  }
  to {
    background-position: -130% 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  #playerHover,
  #playerHover::after {
    animation: none;
    transition: none;
  }
}

/* ─── death + round over + scores ─────────────────────────────────────────────
 * Click-through except for the card, so the chat underneath stays usable while dead and through
 * the twenty seconds of round-over standings.
 */

/* Over the BOARD, not the window. The side panel is 340px of chrome and the banner another 52,
   so a viewport-centred dialog sits visibly off to one side of the thing you are looking at —
   and it dimmed the chat and the standings, which are exactly what you read while dead. The
   insets match canvas#game exactly. */
#dead {
  position: fixed;
  display: none;
  place-items: center;
  z-index: 8;
  top: calc(var(--bar) + var(--gap) * 2);
  left: var(--gap);
  width: calc(100vw - var(--side) - var(--gap) * 3);
  height: calc(100vh - var(--bar) - var(--gap) * 3);
  background: #0d111788;
  pointer-events: none;
  border-radius: 8px;
}
#dead .card {
  pointer-events: auto;
}

.card {
  --panel-fill: #141b24f2;
  border-radius: 16px;
  padding: 34px 48px 26px;
  width: 600px;
  position: relative;
  box-shadow:
    0 0 80px #80203238,
    0 22px 60px #000b;
}
.card.small {
  /* Narrower, and padded to the buttons rather than to the old heading that used to sit above
     them. The card was 340px of mostly nothing round three controls. */
  width: 290px;
  text-align: center;
  padding: 20px 20px 18px;
}
/* Sitting lower in the card now that the instruction line under it is gone: the countdown button
   is the only other thing here, and the two were crowding the top edge together. */
/* Only the round-over card has a heading now. The death card leads with the respawn button,
   because the banner on the board has already said what happened and a second announcement
   just pushes the one control you want further down the screen. */
/* The text face, not the display one. The rule in this stylesheet is that the pixel face is for
   things you READ ONCE — the title, a streak — and the text face is for everything you read while
   playing. A round-over card is a scoreboard you actually read, and the display face on it was
   decoration sitting on top of information. */
#deadTitle {
  font-family: var(--text);
  font-weight: 700;
  color: var(--gold);
  font-size: 26px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 4px 0 16px;
  text-shadow: 0 2px 0 #0008;
  display: none;
}
.card.round #deadTitle {
  display: block;
}

/* The two quieter choices under it: outlined, not filled, so the respawn button stays the one
   thing the eye lands on. */
.ghost {
  width: 100%;
  margin-top: 10px;
  padding: 9px;
  font-family: var(--nums);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  background: transparent;
  border: 1px solid #ffffff1f;
  border-radius: 9px;
  transition:
    color 0.12s,
    border-color 0.12s,
    background 0.12s;
}
.ghost:hover {
  color: var(--ink);
  border-color: #ffffff38;
  background: #ffffff0f;
}
.card.round .ghost {
  display: none;
}
.card.round #autoRow {
  display: none;
}

/* Auto respawn.
 *
 * Under the button rather than beside it, and quiet: it is a setting, not a fourth choice, and
 * the respawn button has to stay the one thing the eye lands on. The whole row is the hit
 * target — a 13px checkbox on its own is a dexterity test. */
/* A sliding switch rather than a checkbox: the label on the left in the same face as the ghost
 * buttons under it, and a gold-and-maroon pill on the right whose knob slides across and lights.
 * The native box was a 14px browser-blue square in a card where everything else is cast in
 * gold — the one control on the card that looked like it came from another page. */
#autoRow {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 12px;
  padding: 8px 10px 8px 12px;
  border-radius: 10px;
  border: 1px solid #edc24e1f;
  background: linear-gradient(180deg, #2a101833, #2a101866);
  font-family: var(--nums);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #c9b48a;
  cursor: pointer;
  user-select: none;
  transition:
    color 0.15s,
    border-color 0.15s,
    background 0.15s;
}
#autoRow:hover {
  color: var(--gold-hi);
  border-color: #edc24e47;
}
/* The real input, invisible but still focusable and still the thing that is clicked. */
#autoRow input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}
#autoRow .switch {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 22px;
  border-radius: 11px;
  background: linear-gradient(180deg, #1a0a0f, #3a1520);
  box-shadow:
    inset 0 2px 4px #000b,
    inset 0 0 0 1px #edc24e33;
  transition:
    background 0.2s,
    box-shadow 0.2s;
}
#autoRow .knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: linear-gradient(180deg, #8a7a66, #5a4c3c);
  box-shadow:
    0 2px 3px #000a,
    inset 0 1px 0 #ffffff33;
  transition:
    transform 0.22s cubic-bezier(0.3, 1.4, 0.5, 1),
    background 0.2s,
    box-shadow 0.2s;
}
/* On: the track fills with maroon-lit gold and the knob slides across, cast and glowing. */
#autoRow input:checked + .switch {
  background: linear-gradient(180deg, #6d4709, #a06a12 60%, #c08d22);
  box-shadow:
    inset 0 2px 4px #0008,
    inset 0 0 0 1px #f6e7a966,
    0 0 12px #edc24e55;
}
#autoRow input:checked + .switch .knob {
  transform: translateX(18px);
  background: linear-gradient(
    180deg,
    var(--gold-lit),
    var(--gold) 55%,
    var(--gold-deep)
  );
  box-shadow:
    0 2px 4px #000a,
    0 0 8px #f6e7a9aa,
    inset 0 1px 0 #ffffffaa;
}
#autoRow input:focus-visible + .switch {
  outline: 2px solid var(--gold-hi);
  outline-offset: 2px;
}
/* On, it says so in the label too, so it reads at a glance without finding the switch —
   which matters because you are about to stop looking at this card entirely. */
#autoRow:has(input:checked) {
  color: var(--gold);
  border-color: #edc24e40;
}
@media (prefers-reduced-motion: reduce) {
  #autoRow .knob {
    transition: none;
  }
}

/* Leaving is the one choice on this card you cannot undo, so it is the one that is coloured.
   Still a ghost button — it is an alternative, not the thing you came here to press — but a red
   one, which is the convention for "this ends it". */
#deadLeave {
  color: #c86a62;
  border-color: #e8655a3d;
}
#deadLeave:hover {
  color: #ff8a7e;
  border-color: #e8655a8c;
  background: #e8655a14;
}
/* The countdown gets its own face. The display font is for titles you read once, and this is a
 * number that changes every second a few inches from your eyes. */
/* The one thing you came here to press, so it gets the same treatment as PLAY: filled, gold,
   with the countdown in it. The two ghosts under it are alternatives, not peers. */
#respawn {
  width: 100%;
  margin: 0;
  padding: 12px 14px;
  font-family: var(--display);
  font-size: 23px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-gold);
  background: linear-gradient(
    180deg,
    var(--gold-lit),
    var(--gold) 55%,
    var(--gold-deep)
  );
  border: none;
  border-radius: 10px;
  box-shadow:
    0 4px 0 var(--gold-shadow),
    0 10px 26px #edc24e33;
  text-shadow: 0 1px 0 #fff6;
  transition:
    filter 0.12s,
    box-shadow 0.12s,
    opacity 0.12s;
}
#respawn:hover:not(:disabled) {
  filter: brightness(1.08);
  box-shadow:
    0 4px 0 var(--gold-shadow),
    0 12px 34px #edc24e55;
}
/* Counting down: dimmed and flat, so it reads as "not yet" rather than as "broken". */
#respawn:disabled {
  opacity: 0.5;
  background: linear-gradient(180deg, #4a3826, #37291b);
  box-shadow: none;
  color: #e8d6c3;
}
/* Counting down, not broken: dimmed but still legible, and the number keeps moving. */

/* Round-over standings. A table, because a block of preformatted text only lines its columns up
   by luck, and your own row was indistinguishable from the other fifteen. */
#roundTable {
  display: none;
  margin-top: 14px;
  text-align: left;
}
.card.round {
  width: 460px;
}
.card.round #roundTable {
  display: block;
}
.card.round #respawn {
  display: none;
}
.card.round #deadTitle {
  color: var(--gold);
}

/* You won it. The only place in the interface that gets the cast treatment on demand — and the
   only time this card ever says something worth looking at twice. */
.card.round.won #deadTitle {
  font-family: var(--display);
  font-size: clamp(26px, 3.2vw, 38px);
  letter-spacing: 0.06em;
  background: var(--cast);
  background-size:
    260% 100%,
    100% 100%;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  text-shadow: none;
  filter: var(--cast-edge) drop-shadow(0 2px 1px #0009)
    drop-shadow(0 0 22px #edc24e5c);
  animation: castShine 2.4s linear infinite;
}
#roundTable table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
#roundTable th {
  color: var(--dim);
  font-weight: normal;
  text-align: left;
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  padding-bottom: 6px;
  border-bottom: 1px solid #edc24e33;
}
#roundTable td {
  padding: 4px 10px 4px 0;
  white-space: nowrap;
}
#roundTable td.dimcell {
  color: var(--dim);
}
#roundTable td.num {
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
}
/* Tall enough for sixteen rows on an ordinary window, so the scrollbar — which narrows the body
 * and pulls its columns out of line with the header — only appears when it has to, and is thin
 * and gold when it does. */
#roundTable tbody {
  display: block;
  max-height: 60vh;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #edc24e55 transparent;
}
#roundTable thead,
#roundTable tbody tr {
  display: table;
  width: 100%;
  table-layout: fixed;
}
/* Your row. Gold rule down the left and a lift off the background — on a sixteen-row table the
   only question anyone is asking is "where am I". */
#roundTable tr.me {
  background: #edc24e14;
  font-weight: 700;
  box-shadow: inset 3px 0 0 var(--gold);
}
/* Next map and the bar that counts down to it. Gold running into green: the gold of the rest of
 * the interface, arriving at "go". The fill is one CSS animation the length of the intermission,
 * started by main.js, so it moves smoothly with nothing in the frame loop driving it. */
#roundNext {
  margin: 18px 0 2px;
  text-align: center;
}
#roundNext .nextMap {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 10px;
}
#roundNext .k {
  color: var(--dim);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.16em;
}
#nextMapName {
  font-family: var(--display);
  font-size: 22px;
  letter-spacing: 0.05em;
  color: var(--gold);
  text-shadow:
    0 2px 0 #0009,
    0 0 18px #edc24e44;
}
#roundBar {
  position: relative;
  height: 12px;
  margin: 12px 4px 8px;
  border-radius: 7px;
  background: #0b1017;
  border: 1px solid #edc24e40;
  box-shadow: inset 0 2px 4px #000a;
  overflow: hidden;
}
/* Full width and revealed by a clip, not grown: the gradient stays pinned to the track, so the
 * bar is gold while you wait and turns green only as the round gets close. */
#roundBar .fill {
  position: absolute;
  inset: 0;
  border-radius: 6px;
  background:
    linear-gradient(180deg, #ffffff55, #ffffff00 55%),
    linear-gradient(90deg, #b8892e, #edc24e 38%, #9fd85a 74%, #3fbf6a);
  clip-path: inset(0 100% 0 0 round 6px);
}
/* A glint that runs along the fill, so a bar that only crawls still reads as alive. */
#roundBar .fill::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    transparent 30%,
    #ffffff70 50%,
    transparent 70%
  );
  background-size: 200% 100%;
  animation: roundGlint 1.6s linear infinite;
}
@keyframes roundFill {
  from {
    clip-path: inset(0 100% 0 0 round 6px);
  }
  to {
    clip-path: inset(0 0 0 0 round 6px);
  }
}
@keyframes roundGlint {
  from {
    background-position: 150% 0;
  }
  to {
    background-position: -50% 0;
  }
}
#roundCountdown {
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #9fd85a;
}

/* Between rounds the board is gone, not dimmed. Everything on it — the sprites mid-stride, the
 * sea, the banners — is hidden, and the frame loop stops drawing it (see `loop` in main.js), so
 * the standings are the only thing on screen and nothing behind them is still moving. */
body.intermission canvas#game,
body.intermission #deadBanner,
body.intermission #streak {
  visibility: hidden;
}
body.intermission #dead {
  background: radial-gradient(ellipse at 50% 40%, #16202b, #0b0f15 75%);
}

/* Centred on the BOARD, not the window — the same correction the kill-streak banner got.
 *
 * The side panel is 340px of chrome and the top bar is another 52. A panel centred on the
 * viewport therefore sits visibly off to one side and low of the thing you are looking at, and
 * TAB is pressed mid-round by somebody who wants to glance and get back. Anchored to the board
 * it lands where their eyes already are.
 *
 * The inset values are the same expressions `canvas#game` uses, so the two can never drift. */
#scores {
  --panel-fill: #141b24f7;
  position: fixed;
  z-index: 9;
  top: calc(var(--bar) + var(--gap) * 2);
  left: var(--gap);
  width: calc(100vw - var(--side) - var(--gap) * 3);
  height: calc(100vh - var(--bar) - var(--gap) * 3);
  /* The panel itself is centred INSIDE that box rather than filling it: a scoreboard the size
     of the board is a page, and this is a glance. `place-items` does the centring, and the grid
     is only here to do it. */
  display: none;
  place-items: center;
  pointer-events: none;
  background: none;
  border: 0;
  padding: 0;
}
#scores.on {
  display: grid;
  /* The board, softly out of focus behind the panel.
   *
   * Depth of field, and it is doing a real job rather than decorating: the scoreboard is a page
   * of small numbers laid over a field of sixteen moving sprites and a sea that never stops, and
   * the eye keeps being pulled off the text by motion behind it. Three pixels of blur is enough
   * that the movement stops competing and nowhere near enough to hide what is happening — you
   * can still see a blast go off while you are reading.
   *
   * `backdrop-filter`, so it costs one compositor pass on what is already painted and the game
   * canvas itself is untouched: no filter on the canvas, no change to the frame loop, and nothing
   * at all when the panel is closed. A touch of darkening with it, because blurring alone raises
   * the average brightness of a busy board and the panel's own fill then reads as lighter. */
  backdrop-filter: blur(3px) saturate(0.9) brightness(0.82);
  -webkit-backdrop-filter: blur(3px) saturate(0.9) brightness(0.82);
}
#scores > * {
  pointer-events: auto;
}

/* The card. Everything that used to be on #scores lives here now, so the outer element can be
   a positioning frame and nothing else. */
#scoresCard {
  --panel-fill: #141b24f7;
  /* #scores takes no pointer events, so the board behind it stays clickable. The card has to
     take them back, or the tabs on it cannot be clicked and it cannot be scrolled. */
  pointer-events: auto;
  /* 590px, matching the width the table was designed against once it grew a medals column and
     a coins column. Narrower and the headings wrap, which turns a glance into a read. */
  border-radius: 12px;
  padding: 20px 24px;
  min-width: 590px;
  max-width: 100%;
  max-height: 100%;
  overflow-y: auto;
  /* A floor under both panes, so switching tabs does not throw the panel up or down the screen.
     It is centred in its box, so any change in height moves BOTH edges and the whole thing
     appears to jump. The boards grow with the player count; this is roughly where a full island
     puts them, and the rules sheet is cut to the same cloth. */
  min-height: min(62vh, 520px);
}
/* The tabs stay put at the top; only what is under them changes size. */
#scoresCard > div {
  width: 100%;
}

@media (max-width: 1080px) {
  #scores {
    width: calc(100vw - var(--gap) * 2);
  }
}

/* The two panes behind Tab: the boards, and the rules sheet.
 *
 * Tabs rather than a second key. Tab is already where you look between rounds, and a rules sheet
 * nobody can find is a rules sheet nobody reads. */
#boardTabs {
  display: flex;
  gap: 6px;
  margin: -4px 0 12px;
}
#boardTabs .bt {
  font-family: var(--display);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--dim);
  background: #ffffff08;
  border: 1px solid #ffffff14;
  border-radius: 7px;
  padding: 5px 12px;
  cursor: pointer;
}
#boardTabs .bt:hover {
  color: #e9eef7;
}
#boardTabs .bt.on {
  color: var(--gold);
  background: #edc24e1c;
  border-color: #edc24e4d;
}
#rulesPane {
  display: none;
}
#scoresCard.rules #rulesPane {
  display: block;
}
#scoresCard.rules #boardsPane {
  display: none;
}

/* The sheet itself. Name on the left, sentence on the right, one row each.
 *
 * It was a stack: icon and name on one line, the sentence under it, which ran to eight rows of
 * two lines and made the panel twice the height of the boards it shares a key with. Side by
 * side, the names column is a list you can run your eye down, and the whole sheet is as tall as
 * the scoreboard rather than twice it. */
#rulesPane .rline {
  margin: 0 0 14px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--dim);
  max-width: 78ch;
}
#rulesPane dl.rules {
  display: grid;
  grid-template-columns: 136px 1fr;
  align-items: baseline;
  gap: 7px 16px;
  margin: 0 0 18px;
}
#rulesPane dt {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-family: var(--display);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
}
#rulesPane dt svg {
  width: 15px;
  height: 15px;
  color: var(--gold);
  flex: none;
}
#rulesPane dd {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: #c8d2de;
}

/* Scoring: a wrapping row of chips rather than a paragraph of numbers. A sentence is read; a
   row of figures is scanned, which is all anybody does with a scoring table. */
#rulesPane ul.score {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
#rulesPane ul.score li {
  background: #ffffff08;
  border: 1px solid #ffffff12;
  border-radius: 6px;
  padding: 4px 9px;
  font-size: 12px;
  color: var(--dim);
}
#rulesPane ul.score b {
  font-family: var(--nums);
  color: var(--gold);
  margin-right: 5px;
}
#scores table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
#scores th {
  color: var(--dim);
  font-weight: normal;
  text-align: left;
  padding-bottom: 7px;
  border-bottom: 1px solid #edc24e33;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}
#scores td {
  padding: 4px 12px 4px 0;
  white-space: nowrap;
}
#scores td:last-child,
#scores th:last-child,
#roundTable td:last-child,
#roundTable th:last-child {
  padding-right: 10px;
}
#scores td.dimcell {
  color: var(--dim);
}
#scores td.num {
  font-family: var(--nums);
  font-variant-numeric: tabular-nums;
}
#scores .sub {
  margin: 14px 0 0;
  text-align: center;
}
#allTime {
  display: none;
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid #edc24e33;
}
#scores h2.boardHead {
  margin: 0 0 8px;
  font-size: 11px;
  font-weight: normal;
  color: var(--gold);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  font-family: var(--display);
  text-shadow: var(--rim);
}

/* Every table board: taller rows, zebra stripes, the place column centred and wide enough for a
 * medal, podium tints on the top three, and your row edged in gold. The stripes are barely there
 * — on a dark panel a sixteen-row table without them is a wall, with strong ones it is a barcode. */
#scores td,
#roundTable td {
  padding-top: 6px;
  padding-bottom: 6px;
}
#scores tbody tr:nth-child(even):not(.p1):not(.p2):not(.p3),
#roundTable tbody tr:nth-child(even):not(.p1):not(.p2):not(.p3) {
  background: #ffffff06;
}
#scores td.place,
#roundTable td.place {
  width: 34px;
  padding-left: 4px;
  padding-right: 6px;
  text-align: center;
  color: var(--dim);
  font-family: var(--nums);
}
#scores th:first-child,
#roundTable th:first-child {
  width: 34px;
  text-align: center;
  padding-left: 4px;
}
#scores tr.me {
  box-shadow: inset 3px 0 0 var(--gold);
  font-weight: 700;
}
#scores tr.me:not(.p1):not(.p2):not(.p3) {
  background: #edc24e12;
}
#scores tr.p1 td.score,
#roundTable tr.p1 td.score {
  color: var(--gold-hi);
}
#scores td.score,
#roundTable td.score {
  font-weight: 700;
}
#scores td.num,
#roundTable td.num {
  text-align: right;
}
#scores th.num,
#roundTable th.num {
  text-align: right;
}
#allTime h2 {
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: normal;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  font-family: var(--display);
}

/* ─── menu ────────────────────────────────────────────────────────────────────── */

#menu {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  z-index: 10;
  /* Deep maroon at the top, not violet: the landing page reads in the mascot's gold and bandana
     red rather than purple. Dark enough to stay a backdrop. */
  background: radial-gradient(120% 90% at 50% 0%, #2a1018 0%, #0d1117 60%);
}

/* The visualiser, behind everything on the menu and out of the centring grid.
 *
 * `position: absolute` rather than a grid cell: in the grid it would be centred and sized to its
 * content like the card is, and what it wants is the whole window. Auto z-index with
 * `#menuWrap > .card` at 1 means DOM order decides, and this is first. */
#menuViz {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* The mascot floats, unframed, half in front of the card's top-right corner.
 *
 * No disc and no ring: the art has its own alpha and a circular backing plate behind it just
 * reads as a sticker. Overlapping the card is what puts him in the scene rather than beside it. */
#menuWrap {
  position: relative;
}
/* The card paints over the props. Both are positioned with auto z-index, which means DOM order
   decides — and the props come later in the markup, so without this they would sit on top of
   the form. */
#menuWrap > .card {
  z-index: 1;
  /* Fifteen percent wider and roughly fifteen percent shorter than the old 600 x 772 card.
     Width gives the control descriptions room; tighter vertical rhythm removes dead air without
     shrinking the readable type or the hit targets. */
  width: min(690px, calc(100vw - 32px));
  padding: 26px 48px 18px;
}

/* The kegs, standing on the top edge of the card.
 *
 * Over the panel, not behind it: tucked underneath they were a shape half-hidden by a rectangle,
 * which reads as a mistake rather than as depth. Sitting on the edge, with the bottom of the
 * stack overlapping the card, they look like something resting on it.
 *
 * Left of centre so they never crowd the title, and full strength — the one thing in front of
 * the panel has to look deliberate, and a faded object in front of a solid one looks like a
 * rendering fault. */
.prop {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  user-select: none;
}
.prop img {
  display: block;
  width: 100%;
  height: auto;
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 18px 30px #000c);
}
/* Down at the foot of the card, overlapping its bottom-left corner.
 *
 * Far enough left that the stack straddles the card's edge rather than sitting inside it, and
 * high enough that a good half of it is against the panel. A prop that only clips the corner
 * reads as resting against the card; one sitting squarely on top reads as a sticker stuck over
 * the controls, and one too far below looks like it fell off. */
#propBarrels {
  width: 285px;
  left: -270px;
  bottom: -40px; /* clear of the card, off to its left */
  transform: rotate(-6deg);
  animation: propA 9s ease-in-out infinite;
}

/* The same moving outline the mascot has, by the same method.
 *
 * A copy of the artwork painted behind it, scaled up a hair, filled with the conic gradient and
 * masked by the image's own alpha. The mask is what makes it an outline rather than a glow:
 * only the pixels the artwork actually covers get painted, so the colour appears as a rim
 * exactly where the edge of the stack is. A border-radius could never do this — a pile of
 * barrels is not a rectangle.
 *
 * 24 s, matching the mascot and the panel borders. Three things sweeping at three different
 * speeds reads as a glitch rather than as one light moving across the page. */
#propBarrels::before {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(
    from var(--spin) at 100% 100%,
    #f6e7a9,
    #edc24e,
    #c4683a,
    #a83442,
    #801e32,
    #cc9434,
    #f6e7a9
  );
  -webkit-mask-image: url("/assets/img/bomb-barrels.png");
  mask-image: url("/assets/img/bomb-barrels.png");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  transform: scale(1.045);
  filter: blur(1.5px) saturate(1.3);
  animation: spin 24s linear infinite;
  z-index: 0;
}

@keyframes propA {
  0%,
  100% {
    transform: translateY(0) rotate(-6deg);
  }
  50% {
    transform: translateY(-9px) rotate(-3deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .prop {
    animation: none;
  }
}
/* Below this the card has the width to itself; a prop here would be over the form, not beside
   it. Same breakpoint the mascot uses. */
@media (max-width: 1100px) {
  .prop {
    display: none;
  }
}
/* The dashboard mascot's size, in ONE place and on the box, not the artwork.
 *
 * `#mascot::before` is a second copy of the same PNG masked over the whole box and painted in
 * the moving gradient — that is what gives him a rim that follows his silhouette instead of a
 * circle around him. So the box and the artwork have to be the same size: shrink only the
 * <img> and the rim keeps painting at the old size, which is the purple ghost standing behind
 * and to the right of him. Change this one number; everything else follows. */
.mascot-dash {
  /* 255px: his normal size, the one he had from the supply-and-demand release until an inline
     override shrank him. Back on request. */
  --mascot-size: 255px;
}

#mascot {
  position: absolute;
  /* Pinned a third of the way down the card, so he sits beside the name field rather than over
   * the title, however tall the card gets. */
  top: 26%;
  right: -210px;
  width: var(--mascot-size);
  height: var(--mascot-size);
  z-index: 2;
  pointer-events: none;
  animation: bob 5.5s ease-in-out infinite;
}
#mascot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  position: relative;
  z-index: 1;
  /* A soft shadow under him instead of a frame around him. */
  filter: drop-shadow(0 18px 24px #000a) drop-shadow(0 0 40px #80203255);
}

/* The gradient outline follows his SILHOUETTE rather than sitting on a circle around him.
 *
 * A copy of the same image, scaled up a hair and painted behind him, with the moving conic
 * gradient as its fill and the image's own alpha as its mask. The mask is what makes it an
 * outline: only the pixels the artwork actually covers get painted, so the colour appears as a
 * rim exactly where his edge is. A border-radius could never do this — he is not a circle. */
#mascot::before {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(
    from var(--spin) at 100% 100%,
    #f6e7a9,
    #edc24e,
    #c4683a,
    #a83442,
    #801e32,
    #cc9434,
    #f6e7a9
  );
  -webkit-mask-image: url("/assets/img/mascot.png");
  mask-image: url("/assets/img/mascot.png");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  transform: scale(1.045);
  filter: blur(1.5px) saturate(1.3);
  animation: spin 24s linear infinite;
  z-index: 0;
}

/* His advice, on hovering PLAY. White, where the in-game bubble is dark: this one sits over a
 * near-black landing page and has to read as a piece of paper rather than another dim panel.
 *
 * Up and to his right, and inside #mascot so it rides the bob with him — a bubble that stayed
 * still while his head moved would look pinned to the page rather than attached to him. */
#menuSay {
  position: absolute;
  bottom: calc(100% - 46px);
  left: calc(100% - 34px);
  width: 232px;
  padding: 12px 15px;
  border-radius: 14px;
  background: #fbf7ec;
  color: #16202b;
  font-size: 14.5px;
  line-height: 1.35;
  font-weight: 600;
  text-wrap: balance;
  box-shadow:
    0 14px 34px #000a,
    0 0 0 1.5px #ffffff33;
  opacity: 0;
  transform: scale(0.72) translate(-12px, 10px);
  transform-origin: 0 100%;
  pointer-events: none;
  z-index: 3;
  transition:
    opacity 0.18s ease,
    transform 0.18s cubic-bezier(0.2, 1.5, 0.4, 1);
}
/* The tail, as a rotated square tucked under the bottom-left corner so it points back at him. */
#menuSay::after {
  content: "";
  position: absolute;
  left: 20px;
  bottom: -6px;
  width: 14px;
  height: 14px;
  background: #fbf7ec;
  transform: rotate(45deg);
}
#menuSay.show {
  opacity: 1;
  transform: scale(1) translate(0, 0);
}

/* Below this the bubble would run off the right-hand edge, so it goes over the card instead. */
@media (max-width: 1400px) {
  #menuSay {
    left: auto;
    right: calc(100% - 44px);
    transform-origin: 100% 100%;
  }
  #menuSay::after {
    left: auto;
    right: 20px;
  }
}

/* Half the travel it had. A sixteen-pixel bob on a face in the corner of the screen is movement
   in your peripheral vision during a firefight, which is exactly where you do not want any. */
@keyframes bob {
  0%,
  100% {
    transform: translateY(0) rotate(-0.5deg);
  }
  50% {
    transform: translateY(-8px) rotate(0.5deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  #mascot {
    animation: none;
  }
}
@media (max-width: 860px) {
  #mascot {
    display: none;
  }
}

h1 {
  font-family: var(--display);
  color: var(--gold);
  line-height: 1;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin: 0 0 10px;
  text-shadow:
    var(--rim),
    0 4px 0 #0009,
    0 0 34px #edc24e55;
}

/* The title on the landing page: one line, never wrapped, and always flowing.
 *
 * clamp() rather than a fixed size, because "Boom Beard" in a display face at a fixed 46px wraps
 * the moment the card is a little narrower than the design — and a wrapped title with a
 * background-clip gradient on it looks like a rendering fault rather than a line break. */
#title {
  /* Sized so the longest word fits the card, and the box is grown to the text rather than the
     card. background-clip paints only inside the element box: a glyph that overflows it has no
     background to clip, so an overflowing title loses its last letter entirely. */
  font-size: clamp(26px, 3.4vw, 44px);
  /* fit-content, then centred by its own margins. `text-align` alone would do nothing here: the
     box is only as wide as the word, so there is no slack inside it to centre against. */
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  white-space: nowrap;
  /* Cast gold, via .cast in the markup. It used to be a travelling conic rainbow — six hues
     rotating through the letterforms — which was striking and said nothing. The title is the
     first thing anybody sees and it should say "treasure", not "CSS". */
  text-shadow: none;
  /* The thin edge, then depth, then the bloom. `.cast` already supplies a version of this; the
   * title gets its own because it is the largest type on the site and wants more under it.
   *
   * It has to be a FILTER, not a text-shadow: the fill is transparent so a shadow would show
   * straight through the inside of every glyph. Each drop-shadow applies to the result of the
   * last, so the thin edge has to come first or the glow is what gets outlined. */
  filter: var(--cast-edge) drop-shadow(0 3px 0 #0009)
    drop-shadow(0 6px 10px #000a) drop-shadow(0 0 30px #edc24e52);
}

/* The 10px top margin is for the landing slogan, which wants air under the title rather than
   to sit tight against it. Harmless on the other `.sub`s — "tab to close" and "no scores" are
   both last lines in their cards with nothing above them to be pushed away from. */
.sub {
  color: var(--dim);
  margin: 10px 0 24px;
  font-size: 14.5px;
  letter-spacing: 0.04em;
}
#menuWrap > .card > .sub {
  margin: 6px 0 16px;
}
/* The two lines that introduce the game are centred; everything below them is a form and stays
   left-aligned, so the eye still has one edge to run down. */
#title,
#menu > #menuWrap .sub {
  text-align: center;
}

/* The version, under the right-hand end of the title.
 *
 * Inside a wrapper that is itself `fit-content`, so it tracks the title's actual rendered width
 * rather than the card's — the title is clamp()ed against the viewport, so the two are only the
 * same at one window size, and anchoring to the card would leave the marker drifting away from
 * the word on every other.
 *
 * Quiet on purpose. It is here so that "which build am I on" is answerable without opening
 * devtools, and that question is asked about once a month. */
#titleWrap {
  position: relative;
  width: fit-content;
  max-width: 100%;
  /* 10px up — it was 15, then back down 5. A negative TOP margin rather than a transform, so
     the slogan and the form below come up with it: a transform would move the title off its own
     space and leave the gap it used to fill sitting there empty. */
  margin: -10px auto 0;
}
#version {
  position: absolute;
  right: -75px;
  top: 100%;
  margin-top: 6px;
  font-family: var(--nums);
  font-size: 7px;
  letter-spacing: 0.14em;
  color: var(--dim);
  opacity: 0.5;
  pointer-events: none;
  white-space: nowrap;
}

label {
  display: block;
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 9px;
}
input[type="text"] {
  width: 100%;
  font: inherit;
  font-size: 17px;
  color: var(--ink);
  background: #080c12;
  border: 1px solid #ffffff1f;
  border-radius: 9px;
  padding: 14px 16px;
  letter-spacing: 0.04em;
  transition:
    border-color 0.15s,
    box-shadow 0.15s;
}
input[type="text"]::placeholder {
  color: #5c6a7d;
}
input[type="text"]:focus {
  outline: none;
  border-color: var(--gold);
  box-shadow: 0 0 0 3px #edc24e1f;
}

/* PLAY is the only thing on this page anyone needs to find, so it is the only filled control. */
#play {
  width: 100%;
  margin-top: 16px;
  padding: 15px;
  font-size: 32px;
  letter-spacing: 0.2em;
  font-family: var(--display);
  line-height: 1;
  color: var(--on-gold);
  /* Dark text on gold, so the rim is a light one: it thickens the strokes and keeps the word
     from dissolving into the brightest part of the gradient. */
  text-shadow:
    0 1px 0 #fff6,
    0 0 2px #0006,
    0 2px 5px #0004;
  background: linear-gradient(
    180deg,
    var(--gold-lit),
    var(--gold) 55%,
    var(--gold-deep)
  );
  border: none;
  border-radius: 10px;
  box-shadow:
    0 4px 0 var(--gold-shadow),
    0 10px 26px #edc24e33;
  transition:
    transform 0.08s,
    box-shadow 0.12s,
    filter 0.12s;
}
#menuWrap #play {
  margin-top: 12px;
  padding: 12px;
}
#menuWrap #watch {
  margin-top: 8px;
  padding: 7px;
}
#play:hover {
  filter: brightness(1.08);
  box-shadow:
    0 4px 0 var(--gold-shadow),
    0 12px 34px #edc24e55;
}
#play:active {
  transform: translateY(3px);
  box-shadow:
    0 1px 0 var(--gold-shadow),
    0 5px 16px #edc24e33;
}
#play:disabled {
  filter: grayscale(0.7) brightness(0.8);
  box-shadow: none;
  transform: none;
}

label {
  margin-bottom: 9px;
}

/* Shown when the server turned this browser away because its identity is already in a game. It
   has to say what to do next, not just that something went wrong. */
#signedOut {
  display: none;
  margin: 12px 0 0;
  padding: 10px 12px;
  background: #e8655a14;
  border: 1px solid #e8655a44;
  border-radius: 8px;
  color: var(--bad);
  font-size: 12.5px;
  text-align: center;
}

/* Controls.
 *
 * Keycaps rather than bold text: a row of real-looking keys is read as "press this" without
 * anybody parsing the sentence, and it lets the left column stay the same width no matter how
 * many keys a row names. The right column is a plain description at body size, because this is
 * the one block on the page somebody actually reads rather than scans. */
.keys {
  margin: 24px 0 0;
  padding: 20px 0 0;
  border-top: 1px solid #ffffff14;
}
#menuWrap .keys {
  margin-top: 16px;
  padding-top: 12px;
}
.keys h2 {
  margin: 0 0 14px;
  font-size: 11px;
  font-weight: 700;
  color: var(--gold);
  text-transform: uppercase;
  letter-spacing: 0.26em;
  font-family: var(--text);
}
#menuWrap .keys h2 {
  margin-bottom: 8px;
}
.keys dl {
  margin: 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 9px 20px;
  align-items: center;
}
#menuWrap .keys dl {
  gap: 4px 24px;
}
#menuWrap .keys kbd {
  height: 24px;
}
.keys dt {
  display: flex;
  align-items: center;
  gap: 4px;
}
.keys dd {
  margin: 0;
  color: var(--dim);
  font-size: 14px;
}
.keys .or {
  color: #5c6a7d;
  font-size: 11px;
  margin: 0 2px;
}
.keys code {
  font-family: var(--nums);
  font-size: 12.5px;
  color: var(--gold);
  background: #edc24e14;
  padding: 1px 5px;
  border-radius: 4px;
}

kbd {
  display: inline-grid;
  place-items: center;
  min-width: 26px;
  height: 26px;
  padding: 0 7px;
  font-family: var(--nums);
  font-size: 12px;
  font-weight: 700;
  color: var(--ink);
  background: linear-gradient(180deg, #2a323e, #1b212b);
  border: 1px solid #ffffff1f;
  border-bottom-width: 2px;
  border-bottom-color: #0009;
  border-radius: 6px;
  box-shadow: 0 1px 0 #ffffff14 inset;
}
kbd.wide {
  min-width: 58px;
}

.menuFoot {
  margin: 20px 0 0;
  padding-top: 15px;
  border-top: 1px solid #ffffff12;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 11px;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}
#menuWrap .menuFoot {
  margin-top: 14px;
  padding-top: 10px;
}
.menuFoot a {
  color: var(--dim);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 5px;
}
.menuFoot a:hover {
  color: var(--ink);
}
.menuFoot a.bug:hover {
  color: var(--bad);
}
.menuFoot svg {
  width: 16px;
  height: 16px;
}

/* ========================================
   THE COUNT-IN
   Five seconds at the top of every round.
   ======================================== */

/* Dead centre of the BOARD, not of the window: the side panel takes 340px off the right, and a
   count-in centred on the viewport sits visibly off to one side of the thing it is counting in.
   The same arithmetic #centre and #dead use, for the same reason.

   In the middle rather than under the clock, because it is no longer a caption: nothing moves
   until it reaches zero, so it is the only thing on screen worth looking at — and the middle is
   where the board is empty, since everyone is standing still on their spawn. */
#countIn {
  position: fixed;
  z-index: 10;
  pointer-events: none;
  top: calc(var(--bar) + var(--gap) * 2);
  left: var(--gap);
  width: calc(100vw - var(--side) - var(--gap) * 3);
  height: calc(100vh - var(--bar) - var(--gap) * 3);
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity 0.25s ease-out;
}
#countIn.on {
  opacity: 1;
}
@media (max-width: 1080px) {
  #countIn {
    width: calc(100vw - var(--gap) * 2);
  }
}

#countIn .n,
#countIn .ring {
  grid-area: 1 / 1;
}

#countIn .n {
  font-family: var(--display);
  font-size: 104px;
  line-height: 1;
  letter-spacing: 0.04em;
  /* The tick. Every whole second the class is re-applied (see the count-in block in
     client/main.js), which restarts this: the digit lands hard, overshoots a hair, and rocks
     back — the motion an escapement makes, not a fade. */
  filter: var(--cast-edge) drop-shadow(0 4px 0 #000)
    drop-shadow(0 8px 18px #000d) drop-shadow(0 0 28px #edc24e66);
  animation:
    countTick 1s cubic-bezier(0.2, 1.4, 0.3, 1) both,
    castShine 7s linear infinite;
}
/* The last two get hotter and bigger. By then the number is not information any more, it is a
   warning that the round is about to be live. */
#countIn.hot .n {
  font-size: 124px;
  filter: var(--cast-edge) drop-shadow(0 4px 0 #000)
    drop-shadow(0 8px 18px #000d) drop-shadow(0 0 22px #edc24ebb);
}
#countIn .n.go {
  font-size: 146px;
  animation:
    countGo 0.9s cubic-bezier(0.16, 1, 0.3, 1) both,
    castShine 7s linear infinite;
}

/* The second hand: a thin gold arc sweeping one full turn per count, under the digit. A
   conic-gradient mask rather than an SVG stroke, so it is one element and no markup. */
#countIn .ring {
  width: 168px;
  height: 168px;
  border-radius: 50%;
  background: conic-gradient(
    var(--gold) 0deg,
    #edc24e00 55deg,
    #edc24e00 360deg
  );
  -webkit-mask: radial-gradient(
    circle,
    #0000 72px,
    #000 73px,
    #000 80px,
    #0000 81px
  );
  mask: radial-gradient(circle, #0000 72px, #000 73px, #000 80px, #0000 81px);
  animation: countSweep 1s linear infinite;
  opacity: 0.85;
  filter: drop-shadow(0 3px 0 #000) drop-shadow(0 7px 14px #000b)
    drop-shadow(0 0 22px #edc24e70);
}
#countIn .ring::after {
  /* The face: twelve faint marks, so the sweep has something to sweep past. */
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: repeating-conic-gradient(#f6e7a966 0deg 2deg, #0000 2deg 30deg);
}

@keyframes countTick {
  0% {
    transform: scale(1.35) rotate(-5deg);
    opacity: 0.2;
  }
  35% {
    transform: scale(1) rotate(2.5deg);
    opacity: 1;
  }
  60% {
    transform: scale(1) rotate(-1.5deg);
  }
  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}
@keyframes countSweep {
  to {
    transform: rotate(360deg);
  }
}
@keyframes countGo {
  0% {
    transform: scale(0.6);
    opacity: 0;
  }
  22% {
    transform: scale(1.12);
    opacity: 1;
  }
  70% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  #countIn .n,
  #countIn .ring,
  #countIn .n.go {
    animation: none;
  }
}

/* ========================================
   KILL-STREAK RAINBOW
   One treatment, three places: the banner's
   headline, the name under it, the chat line,
   and your own name in the corner.
   ======================================== */

/* A gradient poured THROUGH the glyphs and then slid sideways, which is why the stops repeat at
   both ends: `background-position` animates from 0 to one full period, and a ramp whose first and
   last colour differ would jump at the loop. Six hues plus the repeat of the first.

   `--flow` is the period, set inline by client/main.js from the rung: a double kill pours slowly
   and an ace is a blur. Speed IS the escalation here — the colours are the same at every rung,
   so there is nothing else for the eye to measure it against. */
.rainbow {
  background-image: linear-gradient(
    90deg,
    #ff4d4d 0%,
    #ffb13c 16%,
    #ffe877 32%,
    #5ad18a 48%,
    #4fc4ff 64%,
    #b36bff 80%,
    #ff4d4d 100%
  );
  background-size: 200% 100%;
  background-repeat: repeat-x;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: rainbowFlow var(--flow, 2s) linear infinite;
}
@keyframes rainbowFlow {
  to {
    background-position: 200% 0;
  }
}

/* The banner, overriding the cast-gold and white-hot treatments it would otherwise get.
 *
 * Restated rather than inherited from `.rainbow`, because every rule it is fighting is written
 * at id-plus-class specificity: `#streak.s2 .big` sets a `background` SHORTHAND (which would
 * reset the image anyway) and `#streak .who` sets its own gradient and its own animation. A bare
 * `.rainbow` class loses all three. The shadows also have to become drop-shadows: a text-shadow
 * under transparent glyphs paints straight through them, which is the trap `.castSm` documents. */
#streak .big.rainbow,
#streak .who.rainbow {
  background-image: linear-gradient(
    90deg,
    #ff4d4d 0%,
    #ffb13c 16%,
    #ffe877 32%,
    #5ad18a 48%,
    #4fc4ff 64%,
    #b36bff 80%,
    #ff4d4d 100%
  );
  background-size: 200% 100%;
  background-repeat: repeat-x;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: rainbowFlow var(--flow, 2s) linear infinite;
  text-shadow: none;
  filter: drop-shadow(0 3px 0 #000) drop-shadow(0 6px 16px #000c)
    drop-shadow(0 0 22px #ffffff5c);
}

/* The chat line. The feed is 11px and dense, so the rainbow is the whole emphasis — no size
   change, no banner, nothing that would push the other lines around. */
#chatList li.streakLine {
  font-weight: 700;
  letter-spacing: 0.04em;
}
#chatList li.streakLine .rainbow {
  display: inline-block;
}

/* Your own name, up in the corner, for as long as the run is live. Same treatment, same speed,
   so the corner and the banner are plainly the same event.

   Two overrides, and both are about `.castSm`, which this element also carries. Its cast-metal
   layer is a pseudo-element painted ON TOP of the text — see the long note on `.castSm` — so it
   has to go, or the gold simply covers the rainbow. And its text-shadow has to go with it: a
   shadow under transparent glyphs paints straight through them, which would fill every letter
   with gold again from underneath. */
#who.rainbow {
  text-shadow: none;
  filter: drop-shadow(0 1px 0 #000) drop-shadow(0 0 12px #ffffff4d);
}
#who.rainbow::after {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .rainbow {
    animation: none;
    background-position: 25% 0;
  }
}

/* The between-rounds visualiser. Same canvas treatment as #menuViz — `#dead` is already the
   board's box, so inset:0 lands the bars on the board's own edges, exactly where the landing
   page puts them relative to its card. Behind the card: DOM order decides, and the canvas comes
   first in the markup. */
#roundViz {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  border-radius: 8px;
}

/* ========================================
   THE PASSWORD BOX
   Hidden until the server asks. See server/accounts.js.
   ======================================== */
#pwGroup {
  margin-top: 10px;
}
#pwGroup[hidden] {
  display: none;
}
#pw.invalid {
  border-color: var(--bad);
  box-shadow:
    0 0 0 3px #e8655a26,
    0 0 22px #e8655a24;
}
#pw.invalid:focus {
  border-color: var(--bad);
  box-shadow:
    0 0 0 3px #e8655a38,
    0 0 26px #e8655a30;
}
/* Why you are being asked, under the box: "pick a password for this name" and "that name needs
   its password" are different situations and the difference matters to the person reading it. */
#pwNote {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--dim);
  letter-spacing: 0.02em;
}
#pwNote[hidden] {
  display: none;
}
#pwNote.bad {
  color: var(--bad);
}
