/* Bomb Voyage — 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.
 */

@font-face {
  font-family: 'Shotling';
  src: url('/assets/shotling.ttf') format('truetype');
  font-display: block;        /* no flash of fallback on the title; it is the first thing seen */
}

:root {
  color-scheme: dark;
  --gold: #ffd21f;
  --panel: #1a2029;
  --ink: #e8eef7;
  --dim: #92a0b4;
  --bad: #e8655a;
  --good: #5ad1a0;

  /* The display face, for large type only. */
  --display: 'Shotling', 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 crown, inline so it can be a CSS mask and take whatever colour it is used in. */
  --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");
}

* { 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: .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: #0a1a2e;        /* the sea, so a frame that has not painted yet is not a flash */
  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.
 *
 * The stops are the palette's hues at half saturation. At full saturation a rainbow border is
 * the brightest thing on screen, and the brightest thing on screen should be a player or a fire.
 */

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

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

.edge {
  position: relative;
  border: 2px solid transparent;
  background:
    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%,
      #c7b057, #ce8c6e, #c4837e, #b490da, #7badd3, #78b39b, #c7b057) 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: #ffd21f66; background: var(--panel-fill, #11161ee6); }
}

@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; }

#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 {
  font-family: var(--display);
  font-size: 15px;
  color: var(--gold);
  text-shadow: var(--rim), 0 2px 0 #0008;
  letter-spacing: .06em;
}

#rank { color: var(--dim); font-size: 12px; text-transform: uppercase; letter-spacing: .14em; }

/* 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: 6px; }
.stat .k { color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .14em; }
.stat .v {
  font-family: var(--nums); font-variant-numeric: tabular-nums; font-size: 13px;
  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 {
  /* Monospace, not the display face: Shotling has no tabular figures, so 07:05 and 08:88 are
     different widths and the whole bar jumps once a second. The clock is a number you read, not
     a title, and it is the one place where legibility beats character. */
  font-family: var(--nums); font-weight: 700;
  font-size: 24px; line-height: 1;
  color: var(--ink); letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
  display: inline-block; min-width: 5.2ch; text-align: left;
}
#clock .k { color: var(--dim); font-size: 9px; text-transform: uppercase; letter-spacing: .16em; }
#clock.low .v { color: var(--bad); animation: tick 1s steps(2) infinite; }
@keyframes tick { 50% { opacity: .55; } }

#conn {
  display: inline-block; min-width: 4.5em; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#conn[data-state=open] { color: var(--good); }
#conn[data-state=reconnecting], #conn[data-state=connecting] { color: var(--gold); }

/* loadout */
#gear { display: flex; gap: 14px; align-items: center; }
.pip {
  display: flex; align-items: center; gap: 6px;
  font-family: var(--nums); font-variant-numeric: tabular-nums; font-size: 14px;
}
/* 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: var(--dim); font-size: 11px; }
.pip.max .n { color: var(--gold); }
#shields { display: flex; gap: 4px; min-width: 11px; }
#shields i {
  width: 10px; height: 10px; border-radius: 50%; background: var(--good);
  box-shadow: 0 0 8px #5ad1a0aa;
}

/* 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 #ffd21f; }
  35%  { transform: scale(1.45); color: #fff;        text-shadow: 0 0 16px #ffd21f; }
  100% { transform: scale(1);    color: inherit;     text-shadow: none; }
}
.bump { display: inline-block; animation: bump .45s cubic-bezier(.2,1.4,.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 .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 .45s cubic-bezier(.2,1.4,.4,1); }

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

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

input[type=range] { width: 64px; accent-color: var(--gold); }
.vlabel { color: var(--dim); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; }

/* 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 .12s, border-color .12s, background .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; padding-right: 76px; }

/* 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.
 */
#avatar {
  position: fixed; top: 2px; right: 6px;
  width: 92px; height: 92px;
  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;
  filter: drop-shadow(0 6px 14px #000c) drop-shadow(0 0 18px #ffd21f44);
  animation: bob 6s ease-in-out infinite;
}
@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 #ffd21f66;
  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(.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 #ffd21f66; border-top: 1.5px solid #ffd21f66;
  transform: translateY(-50%) rotate(45deg);
}
#say.show { visibility: visible; animation: sayIn .9s cubic-bezier(.2, 1.5, .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(.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 .25s ease, transform .25s ease; }
@media (max-width: 1080px) { #say { display: none; } }
@media (prefers-reduced-motion: reduce) {
  #say.show { animation: none; opacity: 1; transform: none; visibility: visible; }
}
#who {
  font-family: var(--display); font-size: 17px; color: var(--gold);
  text-shadow: var(--rim), 0 2px 0 #0008; letter-spacing: .06em; display: block;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#rank {
  display: block; color: var(--dim); font-size: 10px;
  text-transform: uppercase; letter-spacing: .2em; margin-top: 3px;
}
#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: .14em; font-family: var(--text); margin-right: 4px;
}

/* 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.
 */
#xp { margin-top: 11px; }
.xpHead {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 5px; font-size: 10px;
  text-transform: uppercase; letter-spacing: .14em;
}
#xpLevel { color: var(--gold); font-weight: 700; }
#xpNums { color: var(--dim); font-family: var(--nums); font-variant-numeric: tabular-nums; }
#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 .5s cubic-bezier(.2,.8,.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 .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 .9s ease-out forwards;
  pointer-events: none;
}
#xp.levelup #xpLevel {
  animation: xpPop .9s cubic-bezier(.2,1.5,.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: .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; }
}

.panelHead {
  margin: 0 0 7px; font-size: 9.5px; font-weight: normal; color: var(--dim);
  text-transform: uppercase; letter-spacing: .22em;
}

#liveRows { list-style: none; margin: 0; padding: 0; font-size: 12.5px; }
#liveRows li {
  display: flex; align-items: baseline; gap: 7px; padding: 2px 0;
  font-variant-numeric: tabular-nums;
}
#liveRows .pos { color: var(--dim); width: 1.2em; font-family: var(--nums); font-size: 11px; }
#liveRows .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#liveRows .kd { color: var(--dim); font-family: var(--nums); font-size: 11px; }
#liveRows .sc { font-family: var(--nums); color: var(--ink); }
#liveRows li.me { font-weight: 700; }

/* 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 {
  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: .9;
}
#liveRows li.out .nm { opacity: .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: #ffd21f12; box-shadow: inset 2px 0 0 var(--gold); padding-left: 6px; }
#chatList li.mention { background: #ffd21f0d; }
#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: .55; transition: opacity .15s, border-color .15s;
}
#chatInput:focus { outline: none; opacity: 1; border-color: var(--gold); }

/* ─── 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. */
#centre {
  position: fixed; z-index: 9; 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;
  align-content: center;
}
@media (max-width: 1080px) { #centre { width: calc(100vw - var(--gap) * 2); } }

#streak, #deadBanner {
  grid-area: 1 / 1;
  text-align: center;
  font-family: var(--display); letter-spacing: .08em;
  text-shadow: var(--rim);
  opacity: 0;
}
#streak.show { animation: streakIn 1.9s ease-out forwards; }
#streak .big, #deadBanner .big {
  display: block; font-size: 52px; color: var(--gold);
  text-shadow: 0 0 28px #ffd21f, 0 5px 0 #0009;
}
#streak .who, #deadBanner .who {
  display: block; font-size: 16px; color: var(--ink); margin-top: 8px; opacity: .85;
  font-family: var(--text); letter-spacing: .14em; text-transform: uppercase;
}
/* Each rung is bigger and hotter than the last, so the escalation is visible as well as audible. */
#streak.s2 .big { font-size: 52px; }
#streak.s3 .big { font-size: 64px; color: #ffe98a; text-shadow: 0 0 34px #ffd21f, 0 6px 0 #0009; }
#streak.s4 .big { font-size: 76px; color: #fff; text-shadow: 0 0 40px #ff9a3d, 0 0 80px #e8655a; }
#streak.ace .big {
  font-size: 88px; color: #fff;
  text-shadow: 0 0 44px #b36bff, 0 0 90px #b36bff, 0 0 140px #7badd3;
}

#deadBanner .big { color: var(--bad); text-shadow: 0 0 30px #e8655a, 0 5px 0 #0009; }
#deadBanner.show { animation: deadIn 1.4s ease-out forwards; }
@keyframes deadIn {
  0%   { opacity: 0; transform: scale(1.5); }
  14%  { opacity: 1; transform: scale(1); }
  70%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(.96); }
}

@keyframes streakIn {
  0%   { opacity: 0; transform: translateX(-50%) scale(.6)  rotate(-3deg); }
  12%  { opacity: 1; transform: translateX(-50%) scale(1.14) rotate(1deg); }
  22%  { opacity: 1; transform: translateX(-50%) scale(1)    rotate(0); }
  75%  { opacity: 1; transform: translateX(-50%) scale(1); }
  100% { opacity: 0; transform: translateX(-50%) scale(.95) translateY(-18px); }
}

/* ─── 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 #ffd21f66; border-radius: 6px;
  padding: 7px 16px; color: var(--gold); opacity: 0; transition: opacity .25s;
  pointer-events: none; font-size: 12px; letter-spacing: .08em;
}

/* ─── 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 #6b21a840, 0 22px 60px #000b;
}
.card.small { width: 340px; text-align: center; }
#deadTitle {
  font-family: var(--display); color: var(--bad); font-size: 30px; margin-bottom: 8px;
  text-shadow: var(--rim);
}
#deadText { white-space: pre-line; }
/* 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. */
#respawn {
  width: 100%; margin-top: 16px; padding: 13px; font-family: var(--nums);
  font-size: 15px; font-weight: 700; letter-spacing: .1em; text-transform: none;
}
#deadText { font-family: var(--nums); font-size: 12px; letter-spacing: .02em; }
/* Counting down, not broken: dimmed but still legible, and the number keeps moving. */
#respawn:disabled { opacity: .55; }
#respawn:disabled:hover { background: transparent; color: var(--gold); }

/* 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 #deadText, .card.round #respawn { display: none; }
.card.round #deadTitle { color: var(--gold); }
#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: .16em;
  padding-bottom: 6px; border-bottom: 1px solid #ffd21f33;
}
#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; }
#roundTable tbody { display: block; max-height: 44vh; overflow-y: auto; }
#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: #ffd21f14; font-weight: 700;
  box-shadow: inset 3px 0 0 var(--gold);
}
#roundTable tr.me td:first-child { padding-left: 8px; }
#roundNext { margin: 12px 0 0; text-align: center; color: var(--dim); font-size: 11px; }

#scores {
  --panel-fill: #141b24f7;
  position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 9;
  display: none; border-radius: 12px; padding: 20px 24px; min-width: 500px;
  max-height: 82vh; overflow-y: auto;
}
#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 #ffd21f33; font-size: 10px;
  text-transform: uppercase; letter-spacing: .14em;
}
#scores td { padding: 4px 12px 4px 0; white-space: nowrap; }
#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 #ffd21f33; }
#allTime h2 {
  margin: 0 0 8px; font-size: 11px; font-weight: normal; color: var(--gold);
  letter-spacing: .2em; text-transform: uppercase; font-family: var(--display);
  text-shadow: var(--rim);
}

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

#menu {
  position: fixed; inset: 0; display: grid; place-items: center; z-index: 10;
  background: radial-gradient(120% 90% at 50% 0%, #1b1430 0%, #0d1117 60%);
}

/* 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; }
#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: -150px;
  width: 255px; height: 255px;
  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 #6b21a855);
}

/* 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%,
    #ffd21f, #ff9a5c, #ff7a9a, #b78bff, #5cc6ff, #5ce0b0, #ffd21f);
  -webkit-mask-image: url('/assets/mascot.png');
  mask-image: url('/assets/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(.72) translate(-12px, 10px);
  transform-origin: 0 100%;
  pointer-events: none;
  z-index: 3;
  transition: opacity .18s ease, transform .18s cubic-bezier(.2, 1.5, .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; }
}

@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-16px) rotate(1deg); }
}
@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: .05em;
  text-transform: uppercase;
  margin: 0 0 10px;
  text-shadow: var(--rim), 0 4px 0 #0009, 0 0 34px #ffd21f55;
}

/* The title on the landing page: one line, never wrapped, and always flowing.
 *
 * clamp() rather than a fixed size, because "Bomb Voyage" 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);
  width: fit-content; max-width: 100%;
  white-space: nowrap;
  background: conic-gradient(from var(--spin) at 100% 100%,
    #ffd21f, #ffb45c, #ff9a7a, #c79bf0, #7fc6ff, #6fe0b8, #ffd21f);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  text-shadow: none;
  animation: spin 24s linear infinite;
  /* A black rim, then a black glow, then the gold bloom.
   *
   * text-shadow cannot do this: the fill is transparent so the glyph interior would show the
   * shadow straight through it. A filter works on the composited pixels instead, so four tight
   * zero-blur drop-shadows build an outline that follows the letterforms, and the wider blurred
   * one lifts the whole word off the card. Order matters — each shadow is applied to the result
   * of the last, so the rim has to come before the glow. */
  filter:
    drop-shadow(1.5px 0 0 #000) drop-shadow(-1.5px 0 0 #000)
    drop-shadow(0 1.5px 0 #000) drop-shadow(0 -1.5px 0 #000)
    drop-shadow(0 0 10px #000c)
    drop-shadow(0 4px 0 #000a)
    drop-shadow(0 0 26px #ffd21f44);
}

.sub { color: var(--dim); margin: 0 0 24px; font-size: 14.5px; letter-spacing: .04em; }

label {
  display: block; color: var(--dim); font-size: 11px; letter-spacing: .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: .04em;
  transition: border-color .15s, box-shadow .15s;
}
input[type=text]::placeholder { color: #5c6a7d; }
input[type=text]:focus {
  outline: none; border-color: var(--gold);
  box-shadow: 0 0 0 3px #ffd21f1f;
}

/* 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: .2em;
  font-family: var(--display); line-height: 1;
  color: #130f05;
  /* 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, #ffe066, var(--gold) 55%, #e0a800);
  border: none; border-radius: 10px;
  box-shadow: 0 4px 0 #8a6f00, 0 10px 26px #ffd21f33;
  transition: transform .08s, box-shadow .12s, filter .12s;
}
#play:hover { filter: brightness(1.08); box-shadow: 0 4px 0 #8a6f00, 0 12px 34px #ffd21f55; }
#play:active { transform: translateY(3px); box-shadow: 0 1px 0 #8a6f00, 0 5px 16px #ffd21f33; }
#play:disabled { filter: grayscale(.7) brightness(.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;
}

/* Hovering runs the same conic gradient through the glyphs themselves: the text is painted by the
 * gradient and clipped to the letterforms, so it flows rather than just changing colour.
 *
 * The PLAY button needs `background-clip: text` on a background that is also its fill, so on hover
 * it gives up its solid fill and becomes outlined gradient type instead. */
.hoverFlow:hover, .hoverFlow:focus-visible {
  background: conic-gradient(from var(--spin),
    #c7b057, #ce8c6e, #c4837e, #b490da, #7badd3, #78b39b, #c7b057);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  text-shadow: none;
  animation: spin 16s linear infinite;
}


/* 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; }
.keys h2 {
  margin: 0 0 14px; font-size: 11px; font-weight: 700; color: var(--gold);
  text-transform: uppercase; letter-spacing: .26em; font-family: var(--text);
}
.keys dl {
  margin: 0; display: grid; grid-template-columns: max-content 1fr;
  gap: 9px 20px; align-items: center;
}
.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: #ffd21f14; 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: .14em;
}
.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; }
