/* ---- the LAVA palette (Marcin's pick, 2026-10-11; mockup f-lava-replay in
   the redesign files): a maroon-black page lit by an ember glow at the top,
   with a teal accent.
   Panels are SMOKE (Marcin's pick, 2026-10-12, over solid plum, warm glass and
   no panels at all): translucent darkenings of whatever is behind them, so the
   glow shows through and a panel reads as a shadow on the page rather than a
   grey box. That is why everything that FLOATS over other content (tooltips,
   menus, the map's own controls) takes --float instead, which is opaque. Every
   chrome colour below is one of these, so a retheme is an edit here. Team
   colours, unit colours and the per-flag/state chips keep their own meanings
   and are not part of it. The SUMMARY tab is deliberately left on its own
   palette (its library carries one, --brs-*), which is why #summary restores
   the old page colour behind it. */
:root {
  color-scheme: dark;
  --bg: #170d10;          /* the page */
  --glow: #6a2b18;        /* the ember glow at the top of the page */
  --rail: #130a0d;        /* the left menu */
  --panel: rgba(6, 2, 3, .42);          /* bars, the sidebar, framed boxes */
  --raised: rgba(255, 190, 150, .07);   /* controls and hovered rows on a panel */
  --raised-hi: rgba(255, 190, 150, .12); /* a hovered control */
  --well: rgba(0, 0, 0, .5);            /* inputs, code, chart surfaces: below the panel */
  --float: rgba(20, 9, 11, .96);        /* tooltips, menus, popovers: over content */
  --line: rgba(255, 170, 130, .17);     /* borders */
  --line-soft: rgba(255, 170, 130, .06); /* row separators, grids */
  --line-hi: rgba(255, 170, 130, .22);  /* hovered/strong borders */
  --text: #f6ebe9;
  --text-2: #ecd9d5;      /* secondary bright text */
  --muted: #c8afab;       /* values beside labels, quiet text */
  --dim: #a88f8b;         /* labels, menu entries */
  --faint: #957b78;       /* column headers, timestamps, empty cells */
  /* Teal: the selected thing, links, focus. It replaced gold (Marcin, 2026-10-12:
     "not a fan of this yellow"), and sits opposite the page's ember on the
     colour wheel, so what can be clicked never reads as part of the lava. */
  --accent: #4fdcc4;
  --accent-hi: #86eadb;
  --accent-ink: #062019;  /* text on the accent */
  --accent-soft: rgba(79, 220, 196, .13); /* a selected chip's fill */
  --accent-line: rgba(79, 220, 196, .5);  /* a selected chip's border */
  --lilac: #c3a9dc;       /* work in flight: progress, running jobs, charts */
  --lilac-soft: #2a1f33;
  --lilac-line: #5a4670;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, sans-serif;
  /* The glow is the mockup's: an ellipse at the top centre, fading into the
     page colour; the second layer covers whatever the gradient does not. */
  background: radial-gradient(70% 70% at 50% 0%, var(--glow), var(--bg)) no-repeat, var(--bg);
  color: var(--text);
  overflow: hidden;
  display: flex; flex-direction: column; height: 100vh;
}
header {
  padding: 8px 16px;
  background: transparent;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 14px;
}
h1 { font-size: 15px; margin: 0; white-space: nowrap; }
h1 a { color: inherit; text-decoration: none; }
h1 a:hover { color: var(--accent); }
h1 .sub { color: var(--faint); font-weight: 400; font-size: 12px; margin-left: 8px; }
select, button {
  background: var(--raised); color: var(--text); border: 1px solid var(--line-hi);
  border-radius: 4px; padding: 5px 10px; cursor: pointer; font-size: 13px;
}
button:hover { background: var(--raised-hi); }
button:disabled { opacity: .4; cursor: default; }
select { padding: 5px 8px; }

.controls {
  display: flex; gap: 8px; align-items: center;
  padding: 7px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
}
/* Playback bar: sits below the map, so the border flips to the top edge. */
#bottombar { border-bottom: none; border-top: 1px solid var(--line); }
.controls .grow { flex: 1; }
input[type=range] { flex: 1; min-width: 120px; accent-color: var(--accent); }

/* ---- the playback strip (index.html #bottombar) ----
   A card under the map: a round play button, a step either way, the speed,
   then the TIMELINE — a canvas drawing the whole game (app.js drawStrip: the
   army lead, chat ticks, minutes) with the slider laid over it, its track
   invisible and its thumb drawn as the playhead, a line through the strip
   with a dot on the centre — and the clock. */
body.replay-page #bottombar {
  margin: 4px 10px 10px; padding: 8px 14px 8px 10px; gap: 6px;
  border: 1px solid var(--line); border-radius: 12px;
}
#play {
  width: 42px; height: 42px; padding: 0; margin-right: 4px; border: none; border-radius: 50%;
  display: grid; place-items: center; background: var(--accent); color: var(--accent-ink);
}
#play:hover { background: var(--accent-hi); }
#play svg { width: 20px; height: 20px; fill: currentColor; }
#play .i-pause, #play.playing .i-play { display: none; }
#play.playing .i-pause { display: block; }
.stepbtn {
  width: 30px; height: 30px; padding: 0; display: grid; place-items: center;
  background: none; border: none; color: var(--muted); border-radius: 6px;
}
.stepbtn:hover { background: var(--raised); color: var(--text); }
.stepbtn svg {
  width: 16px; height: 16px; fill: currentColor;
  stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round;
}
#speed {
  padding: 3px 6px; margin: 0 6px 0 2px; border-radius: 999px; font-size: 12px;
  font-variant-numeric: tabular-nums;
}
#timeline { flex: 1; min-width: 120px; position: relative; height: 56px; }
#stripcv { position: absolute; inset: 0; width: 100%; height: 100%; cursor: pointer; }
/* Downloaded ranges along the graph's foot (app.js updateBuffBar: bright =
   fully loaded chunk, dim = keyframe only), inset like the slider's ends. */
#buffbar {
  position: absolute; left: 7px; right: 7px; bottom: 11px; height: 2px;
  border-radius: 1px; background: transparent; pointer-events: none;
}
/* The slider covers the whole strip, so a press anywhere seeks there. Its
   thumb is 14px wide (app.js STRIP_THUMB) and as tall as the strip. */
#timeline #slider {
  -webkit-appearance: none; appearance: none;
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  background: transparent; cursor: pointer;
}
#timeline #slider:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
#timeline #slider::-webkit-slider-runnable-track { height: 56px; background: transparent; }
#timeline #slider::-moz-range-track { height: 56px; background: transparent; }
#timeline #slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 14px; height: 56px; border: none;
  background:
    radial-gradient(circle at 50% 50%, var(--accent) 0 5px, var(--panel) 5.5px 7px, transparent 7.5px),
    linear-gradient(var(--accent), var(--accent)) 50% 0 / 2px 100% no-repeat;
}
#timeline #slider::-moz-range-thumb {
  width: 14px; height: 56px; border: none; border-radius: 0;
  background:
    radial-gradient(circle at 50% 50%, var(--accent) 0 5px, var(--panel) 5.5px 7px, transparent 7.5px),
    linear-gradient(var(--accent), var(--accent)) 50% 0 / 2px 100% no-repeat;
}
.timelabel {
  min-width: 96px; margin-left: 8px; text-align: right; white-space: nowrap;
  font-variant-numeric: tabular-nums; color: var(--dim); font-size: 12px;
}
#timelabel { color: var(--text); font-size: 17px; font-weight: 700; }

/* ---- home view: the left menu and the section it selects, shown when no
   replay is selected. Toggled via body.home, which hides the playback chrome
   underneath. The menu is a fixed rail; only the section next to it scrolls,
   so the menu stays reachable in a long list. */
body.home .controls, body.home #wrap { display: none; }

/* ---- page layout: the left menu (index.html #rail) as a full-height
   column on every page, the header beside it. On the landing page the
   section the menu selects (#home) fills the rest; on a replay page the map
   and the playback bar sit under the header in one column and the sidebar is
   a full-height column on the right. A grid rather than the body's default
   column flow, because the menu and the sidebar span the header and the bar
   while the map sits between them. body.home is set by app.js showHome,
   body.replay-page by applyReplayTab; the summary hides the playback chrome
   and fills the page beside the menu, scrolling on its own. */
body.home, body.replay-page {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
body.home { grid-template-columns: auto minmax(0, 1fr); }
body.replay-page { grid-template-columns: auto minmax(0, 1fr) auto; }
body.home #rail, body.replay-page #rail { grid-column: 1; grid-row: 1 / -1; }
body.home header, body.replay-page header { grid-column: 2; grid-row: 1; }
body.home #home { grid-column: 2; grid-row: 2 / -1; }
body.replay-page #wrap { grid-column: 2; grid-row: 2; }
body.replay-page #bottombar { grid-column: 2; grid-row: 3; }
body.replay-page #side { grid-column: 3; grid-row: 1 / -1; }
body.replay-page #summary { grid-column: 2 / -1; grid-row: 2 / -1; }
/* The header's brand link gives way to a "Replays" back link leading the
   replay's own title, so the way back to the list sits where the page starts
   as well as in the menu. */
body.replay-page #homelink { display: none; }
#backlink { display: none; }
body.replay-page #backlink {
  display: inline-flex; align-items: center; gap: 3px; vertical-align: middle;
  margin-right: 12px; padding: 3px 9px 3px 5px; border-radius: 6px;
  border: 1px solid var(--line); color: var(--muted); font-size: 12px; font-weight: 500;
}
body.replay-page #backlink:hover { color: #fff; background: var(--raised); border-color: var(--line-hi); }
#backlink:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#backlink svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round;
}
body.replay-page h1 .sub { margin-left: 0; vertical-align: middle; }
/* The replay's title: the map, then the game's facts quieter beside it
   (app.js renderReplayTitle). No rule under the header: the map, the cards
   and the strip float on the page. */
body.replay-page header { border-bottom: none; padding: 10px 16px 6px; }
body.replay-page h1 .sub .maptitle { color: var(--text); font-size: 17px; font-weight: 700; margin-right: 10px; }
body.replay-page h1 .sub .facts { color: var(--dim); font-size: 12px; }

#rail {
  width: 72px; padding: 12px 0 10px;
  background: var(--rail); border-right: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
}
#raillogo {
  width: 36px; height: 36px; margin-bottom: 14px; border-radius: 10px;
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink); font-weight: 800; font-size: 16px;
  letter-spacing: -.02em; text-decoration: none;
}
#raillogo:hover { background: var(--accent-hi); }
.railitem, .railsubitem {
  position: relative; border-radius: 10px;
  display: flex; flex-direction: column; align-items: center;
  color: var(--dim); text-decoration: none;
}
.railitem { width: 56px; padding: 8px 0 6px; gap: 4px; font-size: 10.5px; }
.railitem svg, .railsubitem svg {
  width: 20px; height: 20px;
  fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round;
}
.railitem:hover, .railsubitem:hover { color: var(--text); background: var(--raised); }
.railitem.active, .railsubitem.active { color: var(--accent); background: #ffffff12; }
/* The selected entry is also marked at the menu's edge, so it reads at a
   glance without depending on the tint alone. Only ever ONE mark: on a
   replay page it is the sub menu's, and Replays reads as open instead. */
.railitem.active::before {
  content: ''; position: absolute; left: -8px; top: 12px; bottom: 12px;
  width: 3px; border-radius: 0 2px 2px 0; background: var(--accent);
}
#raillogo:focus-visible, .railitem:focus-visible, .railsubitem:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* Admin-only entries (an Access sign-in — app.js adminMode()), same gate as
   the list's refresh button. homeTab() enforces it too, so a shared /queue
   link can't reach the section past the hidden entry. */
.railitem.admin { display: none; }
body.admin-mode .railitem.admin { display: flex; }
/* Count of jobs still in flight, on the Queue icon's corner; empty (and
   invisible) when there are none. */
.railitem .badge {
  position: absolute; top: 3px; right: 6px;
  font-size: 9px; line-height: 1; padding: 2px 5px; border-radius: 8px;
  background: var(--accent); color: var(--accent-ink); font-weight: 700; font-variant-numeric: tabular-nums;
}
.railitem .badge:empty { display: none; }

/* Replays and, while a replay is open, its SUB MENU: the replay's views,
   boxed together with Replays, smaller than the main entries and hung off a
   guide line, so they read as belonging to it rather than as sections of
   their own. The main entries never change; only this box opens. */
.railgroup { width: 60px; display: flex; flex-direction: column; align-items: center; border-radius: 12px; }
#replaytabs { display: none; }
body.replay-page #railreplays {
  padding: 2px 0 6px; background: var(--panel); box-shadow: inset 0 0 0 1px var(--line-soft);
}
#railreplays > .railitem.open { color: var(--text); }
body.replay-page #replaytabs {
  position: relative; width: 100%; padding: 2px 0 0 12px;
  display: flex; flex-direction: column; gap: 2px;
}
#replaytabs::before {
  content: ''; position: absolute; left: 7px; top: 0; bottom: 6px;
  width: 1px; background: var(--line-hi);
}
.railsubitem { width: 45px; padding: 6px 0 5px; gap: 3px; font-size: 9.5px; border-radius: 8px; }
.railsubitem svg { width: 16px; height: 16px; }
.railsubitem.active::before {
  content: ''; position: absolute; left: -6px; top: 7px; bottom: 7px;
  width: 3px; border-radius: 2px; background: var(--accent);
}
/* "new!" pointing at Summary from just outside the menu (Marcin's ask: the
   summary is easy to miss). It is part of the link, so clicking it opens the
   summary too. It nudges left and back, toward the entry, until the reader
   has opened the summary once (app.js initNewTag, remembered in
   localStorage), then stays put. Over the map, so a solid accent-coloured
   tag with a glow rather than text, which the terrain would swallow. */
.newtag {
  position: absolute; left: calc(100% + 15px); top: 50%; z-index: 30;
  height: 20px; margin-top: -10px; padding: 0 8px 0 7px;
  display: flex; align-items: center;
  background: var(--accent); color: var(--accent-ink); border-radius: 5px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .5), 0 0 14px color-mix(in srgb, var(--accent) 45%, transparent);
}
.newtag::before {
  content: ''; position: absolute; left: -6px; top: 50%; margin-top: -6px;
  border: 6px solid transparent; border-left: 0; border-right-color: var(--accent);
}
.railsubitem:hover .newtag { background: var(--accent-hi); }
.railsubitem:hover .newtag::before { border-right-color: var(--accent-hi); }
/* Two quick nudges toward the entry, then a rest, so it reads as pointing
   rather than as something vibrating. */
.newtag.wiggle { animation: newtag-wiggle 2.6s ease-in-out infinite; }
@keyframes newtag-wiggle {
  0%, 36%, 100% { transform: translateX(0); }
  9%, 27% { transform: translateX(-6px); }
  18% { transform: translateX(1px); }
}
@media (prefers-reduced-motion: reduce) { .newtag.wiggle { animation: none; } }

/* The admin corner at the foot of the menu (index.html #adminnote, app.js
   renderAdminNote): the signed-in admin's initial with a sign-out entry, or
   a muted sign-in entry. margin-top:auto pins it to the foot. */
#adminnote { margin-top: auto; }
#adminnote .railitem { color: var(--faint); }
#adminnote .railitem:hover { color: var(--text); }
#adminnote.error .railitem { color: #e0a060; cursor: help; }
#adminnote .avatar {
  width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
  background: var(--line-hi); color: var(--text); font-size: 11px; font-weight: 700;
}
body.home #side, body.home #summary { display: none !important; }
body.summary-tab #wrap, body.summary-tab .controls, body.summary-tab #side { display: none; }
body.summary-tab #summary { display: block !important; }
#summary { flex: 1; min-height: 0; overflow-y: auto; background: #11151a; }
#summary .summarymsg { color: #9fb0bf; text-align: center; margin-top: 60px; }
/* The summary's clip player: a window over the summary holding the real map
   (#wrap is moved into #clipbody while it is open; two ids outrank the
   summary tab's rule hiding it). The sidebar stays behind: the window is the
   map and its timeline, nothing else. */
#clip {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  width: min(680px, calc(100vw - 40px)); height: min(520px, calc(100vh - 110px));
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg); border: 1px solid var(--line-hi); border-radius: 8px;
  box-shadow: 0 14px 44px rgba(0, 0, 0, .6);
}
#cliphead, #clipbar { display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px; background: var(--float); }
#cliphead { border-bottom: 1px solid var(--line); font-size: 13px; }
#clipbar { border-top: 1px solid var(--line); }
#cliptitle { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
#clipclose { padding: 3px 9px; }
/* Drag by the title bar; resize from any edge (the edge's cursor wins over
   the map's crosshair while the pointer is on it). */
#cliphead { cursor: move; user-select: none; }
#cliphead button { cursor: pointer; }
#clip.edge * { cursor: inherit !important; }
body.clip-dragging { user-select: none; }
body.clip-dragging #clip canvas { pointer-events: none; }
#clipplay { min-width: 34px; }
#clipbody { flex: 1; min-height: 0; display: flex; }
#clipbody #wrap { display: flex; flex: 1; min-width: 0; }
#clipbody #zoomlabel { display: none; }
/* The moment the clip is about, marked on its timeline, so the lead-in reads
   as a few seconds' wait for it. The slider's ends are a thumb's half-width
   in from the box (--thumb), which is where its 0 and 1 sit. */
#clipslot { flex: 1; min-width: 0; position: relative; display: flex; align-items: center; --thumb: 16px; }
#clipslot #clipslider { flex: 1; width: 100%; margin: 0; }
#clipmark {
  position: absolute; top: -3px; bottom: -3px; margin-left: -1.5px; border-radius: 2px;
  left: calc(var(--thumb) / 2 + var(--at, 0) * (100% - var(--thumb)));
  /* A span (--len > 0) is a band from the moment's start to its end. */
  width: calc(3px + var(--len, 0) * (100% - var(--thumb)));
  /* Lilac, not the accent: the played part of the track is the accent, and
     the moment has to stand apart from it. */
  background: rgba(195, 169, 220, 0.45); border-left: 3px solid var(--lilac); box-sizing: border-box;
  box-shadow: 0 0 0 1px var(--bg); pointer-events: none;
}
/* ?embed=clip: the clip player alone, filling a frame on another site. */
body.clip-embed > *:not(#clip) { display: none !important; }
body.clip-embed #clip {
  inset: 0; width: auto; height: auto; border: 0; border-radius: 0; box-shadow: none;
}
#cliptime { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 90px; text-align: right; }
#home { min-height: 0; display: flex; }
#homemain { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: 18px 24px; }
#hometable { border-collapse: collapse; width: 100%; max-width: 1650px; margin: 0 auto; font-size: 13px; }
#hometable th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--faint); font-weight: 600; padding: 4px 14px 6px 8px; border-bottom: 1px solid var(--line);
}
/* Each cell is filled by a block-level link (the row IS a link: middle/ctrl
   click opens the replay in a new tab), so the cell padding lives on the <a>
   to keep the whole row surface clickable. */
#hometable td { padding: 0; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
#hometable td a,
#hometable td > span {
  display: block; padding: 6px 14px 6px 8px;
  color: inherit; text-decoration: none;
}
#hometable td.map { white-space: normal; }
/* Map cell: terrain thumbnail beside the name. The cell's link becomes a flex
   row, and the thumb is capped at 18px — under the text line's own height, so
   it cannot make the rows taller. */
#hometable td.map a, #hometable td.map > span { display: flex; align-items: center; gap: 7px; }
#hometable td.map .mapthumb {
  width: 18px; height: 18px; object-fit: cover; border-radius: 3px;
  flex: none; background: var(--raised);
}
/* Settings badges: small muted pills, wrapping within the row's link. */
/* Shared with the Games section (#gamestable): the same flag must look the
   same in both lists, so the per-flag colours below are written once. */
#hometable td.settings, #gamestable td.settings { white-space: normal; }
#hometable td.settings a, #hometable td.settings > span, #gamestable td.settings > span { display: flex; flex-wrap: wrap; gap: 4px; }
#hometable .badge, #gamestable .badge, #queuetable .badge {
  font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 8px;
  background: var(--raised); border: 1px solid var(--line); color: var(--muted);
  white-space: nowrap;
}
/* Per-flag badge colours: lava glows orange, mods (tweaked unit/def tables)
   warns yellow — the same yellow family as the energy bars; zombies rise in
   purple, ruins moss over in green, an explicitly unranked lobby cools blue. */
#hometable .badge-lava, #gamestable .badge-lava, #queuetable .badge-lava {
  color: #ffc46a; border-color: #b85c1a;
  /* ember gradient: hot orange rim fading into deep red coals */
  background: linear-gradient(180deg, #7a2606, #4a1204 55%, #38150a);
  text-shadow: 0 0 4px rgba(255, 140, 30, .5);
  box-shadow: 0 0 8px rgba(255, 110, 20, .35), inset 0 1px 3px rgba(255, 150, 40, .35), inset 0 -3px 6px rgba(230, 60, 0, .4);
}
#hometable .badge-mods, #gamestable .badge-mods, #queuetable .badge-mods { color: #e3c34a; border-color: #6e5c1e; background: #332b10; }
#hometable .badge-zombies, #gamestable .badge-zombies, #queuetable .badge-zombies { color: #c58aff; border-color: #5e3d85; background: #2a1a40; }
#hometable .badge-ruins, #gamestable .badge-ruins, #queuetable .badge-ruins { color: #7fd58a; border-color: #33663c; background: #16311b; }
#hometable .badge-unranked, #gamestable .badge-unranked, #queuetable .badge-unranked { color: #7fb2ff; border-color: #2f4d7a; background: #16243a; }
/* "processing": not a game setting at all — the pipeline is working on this
   game right now — so it leads the cell and is the one FILLED pill in a row
   of dark muted ones. Bright ground against the table's near-black is what
   makes it findable while skimming; the pulse is what makes it read as
   something happening rather than another label. Reduced motion keeps the
   colour and drops the animation, which loses nothing but the movement. */
#hometable .badge-processing {
  color: #06231a; border-color: #7bf5c0;
  background: linear-gradient(180deg, #2ee79b, #14c47c);
  font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  box-shadow: 0 0 0 1px rgba(46, 231, 155, .35), 0 0 10px rgba(46, 231, 155, .45);
  animation: badgepulse 1.6s ease-in-out infinite;
}
@keyframes badgepulse {
  0%, 100% { box-shadow: 0 0 0 1px rgba(46, 231, 155, .35), 0 0 6px rgba(46, 231, 155, .30); }
  50%      { box-shadow: 0 0 0 1px rgba(46, 231, 155, .60), 0 0 16px rgba(46, 231, 155, .75); }
}
@media (prefers-reduced-motion: reduce) {
  #hometable .badge-processing { animation: none; }
}
/* Players column: one span per side, "v" separators; the side that recorded
   the current upload gets a viewpoint dot. Full roster in the tooltip. */
#hometable td.players { white-space: normal; }
/* The Players header is a switch (players ⇄ lobby name) when the backend
   serves lobby names; the affordance is the cursor + a dotted underline. */
#hometable th.swappable { cursor: pointer; text-decoration: underline dotted; user-select: none; }
#hometable th.swappable:hover { color: var(--text-2); }
#hometable td.players .vs { color: var(--faint); padding: 0 6px; font-size: 11px; }
#hometable td.players .side.uploader::before { content: '◉ '; }
/* Alternate uploads of the same game (superseded revisions keep playing). */
#hometable td.links a.alt {
  display: inline-block; padding: 6px 5px; font-size: 11px;
  color: var(--lilac); text-decoration: none;
}
#hometable td.links a.alt:hover { text-decoration: underline; color: var(--accent); }
/* Admin-only columns (admin mode): the refresh-button column and the Data
   (bundle size) column stay hidden unless the page is in admin mode. */
#hometable th.admin, #hometable td.admin, #gamestable th.admin, #gamestable td.admin,
#hometable th.data, #hometable td.data { display: none; }
body.admin-mode #hometable th.admin, body.admin-mode #hometable td.admin,
body.admin-mode #gamestable th.admin, body.admin-mode #gamestable td.admin,
body.admin-mode #hometable th.data, body.admin-mode #hometable td.data { display: table-cell; }
#hometable td.admin button.refresh, #gamestable td.admin button {
  font: inherit; font-size: 13px; line-height: 1; padding: 4px 7px; margin: 2px 4px;
  color: var(--muted); background: var(--raised); border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer;
}
#gamestable td.admin { white-space: nowrap; }
#gamestable td.admin button { font-size: 11px; white-space: nowrap; }
#hometable td.admin button.refresh:hover:enabled,
#gamestable td.admin button:hover:enabled { color: var(--text); border-color: var(--line-hi); }
#hometable td.admin button.refresh:disabled,
#gamestable td.admin button:disabled { opacity: .6; cursor: default; }
/* External per-game links (gex, BAR): real links, visually distinct from the
   invisible row-link anchors filling the other cells. */
#hometable td.links a.ext {
  display: inline-block; padding: 6px 5px;
  color: var(--accent); text-decoration: none;
}
#hometable td.links a.ext:hover { text-decoration: underline; }
#hometable th.num, #hometable td.num { text-align: right; }
#hometable td { font-variant-numeric: tabular-nums; }
#hometable tbody tr { cursor: pointer; }
#hometable tbody tr:hover { background: var(--raised); }
/* A row with nothing published yet: no link, so nothing about it should
   behave like one — no pointer, and the hover highlight that promises a click
   is replaced by a flat tint. */
#hometable tbody tr.unopenable { cursor: default; }
#hometable tbody tr.unopenable:hover { background: var(--panel); }
#hometable td.dim { color: var(--faint); }
#homemsg { padding: 12px 8px; font-size: 13px; max-width: 1650px; margin: 0 auto; }

/* ---- games section: the mirror of what BAR published ----
   Same table look as the replay list. The Here column is the one that is
   coloured: "which of these do we have" is the question this view answers. */
#gamestable { border-collapse: collapse; width: 100%; max-width: 1650px; margin: 0 auto; font-size: 13px; }
#gamestable th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--faint); font-weight: 600; padding: 4px 14px 6px 8px; border-bottom: 1px solid var(--line);
}
#gamestable td {
  padding: 5px 14px 5px 8px; border-bottom: 1px solid var(--line-soft); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
#gamestable tbody tr:hover { background: var(--raised); }
#gamestable td.dim { color: var(--faint); }
#gamestable td.map .mapthumb { height: 18px; width: 18px; object-fit: cover; border-radius: 2px; vertical-align: middle; margin-right: 6px; }
/* Eleven columns of nowrap cells: the section scrolls sideways inside itself
   rather than clipping the Links and Here columns off the page. */
#tab-games { overflow-x: auto; }
#gamestable td.players { max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
#gamestable td.players .vs { color: var(--faint); margin: 0 5px; font-size: 11px; }
#gamestable td.lobby { max-width: 200px; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
#gamestable td.engine { color: var(--dim); font-size: 12px; }
#gamestable td.links a { color: var(--dim); text-decoration: none; font-size: 12px; margin-right: 8px; }
#gamestable td.links a:hover { color: var(--accent); text-decoration: underline; }
#gamestable td.links a.here { color: var(--accent); }
#gamestable .have {
  display: inline-block; font-size: 11px; line-height: 1; padding: 3px 6px; border-radius: 3px;
  border: 1px solid var(--line); color: var(--muted); background: var(--raised);
}
#gamestable .have-published { color: #7fd18b; border-color: #33663c; background: #16311b; }
#gamestable .have-processing { color: var(--lilac); border-color: var(--lilac-line); background: var(--lilac-soft); }
#gamestable .have-pending { color: #d3b98f; border-color: #5c4a2f; background: #2b2417; }
#gamestable .have-error { color: #ff9c8f; border-color: #7a3428; background: #34160f; }
#gamespager {
  display: flex; align-items: center; gap: 10px; max-width: 1650px; margin: 0 auto 8px; padding: 0 8px;
}
#gamespager button {
  background: var(--raised); border: 1px solid var(--line); border-radius: 4px; color: var(--muted);
  padding: 4px 10px; font-size: 12px;
}
#gamespager button:hover:not(:disabled) { color: var(--text); border-color: var(--line-hi); }
#gamespager button:disabled { opacity: .45; }
#gamesmsg { padding: 12px 8px; font-size: 13px; max-width: 1650px; margin: 0 auto; }

/* ---- queue section: the ingest jobs behind the drag&drop uploads ----
   Same table look as the replay list; only the state cell is coloured, since
   "is anything stuck" is the question this view answers. */
/* Full width, unlike the lists: a job row carries a game's facts AND the
   job's, and every column it gains is one the 1650px cap would squeeze. */
#queuetable { border-collapse: collapse; width: 100%; font-size: 13px; }
#queuetable th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--faint); font-weight: 600; padding: 4px 14px 6px 8px; border-bottom: 1px solid var(--line);
}
#queuetable td {
  padding: 6px 14px 6px 8px; border-bottom: 1px solid var(--line-soft);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
#queuetable td.dim { color: var(--faint); }
/* The failure detail is the one cell that may be long: let it wrap. */
#queuetable td.detail { white-space: normal; color: var(--muted); max-width: 640px; }
#queuetable td.detail.error { color: #e2785b; }
/* The game's own facts, joined on beside the job's. Quieter than the state
   and detail columns: they say WHICH game, not what is happening to it. */
#queuetable td.size, #queuetable td.dur { color: var(--muted); }
#queuetable td.links a.ext { color: var(--dim); text-decoration: none; font-size: 12px; }
#queuetable td.links a.ext:hover { color: var(--accent); text-decoration: underline; }
#queuetable td.game a { color: var(--accent); text-decoration: none; }
#queuetable td.game a:hover { text-decoration: underline; }
/* Copy-the-id button beside the game id: quiet until hovered, like the row's
   action button, so a column of them does not read as the point. */
#queuetable .copyid {
  font: inherit; font-size: 12px; line-height: 1; margin-left: 6px; padding: 2px 4px;
  color: var(--dim); background: none; border: 1px solid transparent; border-radius: 3px;
  cursor: pointer; opacity: .35; transition: opacity .12s;
}
#queuetable tr:hover .copyid, #queuetable .copyid:focus-visible { opacity: 1; }
#queuetable .copyid:hover { color: var(--accent); border-color: var(--line); }
#queuetable .copyid.done { opacity: 1; color: var(--accent); }
/* The game's map, start/end and settings, joined on from the games mirror. */
#queuetable td.map > span { vertical-align: middle; }
#queuetable td.map .mapthumb {
  width: 18px; height: 18px; object-fit: cover; border-radius: 3px;
  vertical-align: middle; margin-right: 7px; background: var(--raised);
}
#queuetable td.map, #queuetable td.when { color: var(--muted); }
#queuetable td.settings { white-space: normal; }
#queuetable td.settings > span { display: flex; flex-wrap: wrap; gap: 4px; }
/* The Started/Ended header is its own switch; it reads as a header. */
#queuetable th .thtoggle {
  font: inherit; text-transform: inherit; letter-spacing: inherit; color: inherit;
  background: none; border: none; padding: 0; cursor: pointer;
}
#queuetable th .thtoggle:hover { color: var(--accent); }
#queuetable .state {
  font-size: 10px; line-height: 1; padding: 3px 7px; border-radius: 8px;
  background: var(--raised); border: 1px solid var(--line); color: var(--muted);
}
#queuetable .state-processing { color: var(--lilac); border-color: var(--lilac-line); background: var(--lilac-soft); }
#queuetable .state-done { color: #7fd18b; border-color: #33663c; background: #16311b; }
#queuetable .state-error { color: #ff9c8f; border-color: #7a3428; background: #34160f; }
/* Which daemon a job waits on. Deliberately quieter than the state chips: the
   kind explains a row, the state is what you scan the column for. */
#queuetable .kind {
  font-size: 10px; line-height: 1; padding: 3px 7px; border-radius: 8px;
  background: var(--raised); border: 1px solid var(--line); color: var(--dim);
}
#queuetable .kind-resim { color: #d3b98f; border-color: #5c4a2f; background: #2b2417; }
/* Why a job failed, beside the state that says it did. Same weight as the kind
   chip — it explains a row, it is not what you scan the column for. */
#queuetable .errkind {
  margin-left: 5px; font-size: 10px; line-height: 1; padding: 3px 7px; border-radius: 8px;
  color: #e0b070; border: 1px solid #6b4f2a; background: #2e2415;
}
/* Held back by hand: nothing will pick this job up. Deliberately the quietest
   of the state chips — it is a row that is NOT going anywhere, so it should not
   compete with the ones that are. */
#queuetable .state-disabled { color: #9a8fb0; border-color: #4a3f5c; background: #241f2e; }
/* The row's one control. Ghosted until the row is hovered, so a column of them
   does not read as the point of the table; the enabled-again variant stays lit,
   since a held-back row is the one a person is looking for. */
#queuetable td.act { text-align: right; padding-right: 10px; }
#queuetable .jobbtn {
  font: inherit; font-size: 11px; line-height: 1; padding: 4px 9px; border-radius: 4px;
  color: var(--dim); background: var(--raised); border: 1px solid var(--line); cursor: pointer;
  opacity: 0; transition: opacity .12s;
}
#queuetable tr:hover .jobbtn, #queuetable .jobbtn:focus-visible, #queuetable .jobbtn.on { opacity: 1; }
#queuetable .jobbtn:hover { color: var(--text); border-color: var(--line-hi); background: var(--raised-hi); }
#queuetable .jobbtn.on { color: #b9a9d0; border-color: #4a3f5c; background: #241f2e; }
#queuetable .jobbtn:disabled { cursor: default; opacity: .6; }
/* The row's action menu: the ⋯ button opens it in place, anchored to the
   cell, above the rows below. One open at a time (closeJobMenus). */
#queuetable td.act { position: relative; }
#queuetable .jobmenubtn { padding: 4px 7px; font-size: 13px; }
/* A failed row's retry, always lit: it is what a red row is waiting for. */
#queuetable td.act { white-space: nowrap; }
#queuetable .jobretrybtn { opacity: 1; margin-right: 6px; color: var(--accent); border-color: var(--line-hi); }
#queuetable .jobmenubtn[aria-expanded] { opacity: 1; color: var(--text); border-color: var(--line-hi); }
#queuetable .jobmenu {
  position: absolute; right: 10px; top: calc(100% - 4px); z-index: 5; min-width: 120px;
  background: var(--float); border: 1px solid var(--line-hi); border-radius: 5px; padding: 4px;
  box-shadow: 0 6px 18px rgba(0,0,0,.45); display: flex; flex-direction: column; gap: 2px;
}
#queuetable .jobmenu button {
  font: inherit; font-size: 12px; text-align: left; padding: 6px 10px; border-radius: 3px;
  color: var(--text-2); background: none; border: 0; cursor: pointer; white-space: nowrap;
}
#queuetable .jobmenu button:hover, #queuetable .jobmenu button:focus-visible { background: var(--raised-hi); color: #fff; outline: none; }
/* A running job's live self-report, in the same cell a finished one puts its
   failure detail — the two never coexist (the worker clears progress when a job
   reaches a terminal state). The bar is deliberately thin and unlabelled: the
   numbers are spelled out on the line under it, and the bar is only there to be
   read at a glance down a column of rows. */
#queuetable .jobprog {
  height: 4px; border-radius: 2px; background: var(--line-soft); overflow: hidden;
  margin: 0 0 4px; max-width: 260px;
}
#queuetable .jobprog > span { display: block; height: 100%; background: var(--lilac); }
#queuetable .jobprogtext { color: var(--lilac); font-size: 12px; }
/* The processing record: how long it took in the row, the rest expanded from
   it. The whole row is the toggle, so it advertises itself with a cursor. */
#queuetable tr.hasstats { cursor: pointer; }
#queuetable tr.hasstats:hover { background: var(--panel); }
#queuetable td.took.expand { color: var(--text-2); }
#queuetable tr.statsrow > td { padding: 2px 8px 14px 22px; border-bottom: 1px solid var(--line-soft); }
#queuetable tr.statsrow:hover { background: none; }
.statsgrid {
  display: grid; grid-template-columns: max-content 1fr; gap: 2px 14px;
  font-size: 12px; color: var(--text-2); max-width: 900px;
}
.statsgrid .k { color: var(--faint); }
/* A job's healthcheck history: three small multiples of the same run, sharing
   an X (the beats) and nothing else. Never one plot with three scales — bytes,
   percent-of-a-core and percent-of-a-game have no common axis, and overlaying
   them would invent a correlation the data does not contain.
   One hue for all three: each chart has a single series, so colour carries no
   information and the title is what says what is plotted. The value is
   lilac of in-flight work, on the --well surface. */
.jobcharts {
  display: flex; flex-wrap: wrap; gap: 14px; margin: 12px 0 0; position: relative;
  padding: 10px 12px; background: var(--well); border: 1px solid var(--line-soft); border-radius: 4px;
  width: max-content; max-width: 100%; outline: none;
}
.jobcharts:focus-visible { border-color: var(--accent-line); }
.charttitle { color: var(--faint); font-size: 11px; margin: 0 0 2px 2px; }
.chartnote { color: var(--faint); font-size: 12px; margin: 12px 0 0; }
svg.chart { display: block; touch-action: none; }
/* Chrome stays recessive: solid hairlines one step off the surface, never
   dashed — dashing reads as "threshold" when it is only a grid. */
svg.chart .chgrid { stroke: var(--line-soft); stroke-width: 1; }
svg.chart .chtick {
  fill: var(--faint); font-size: 9px; font-variant-numeric: tabular-nums; text-anchor: end;
}
svg.chart .chline { fill: none; stroke: var(--lilac); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
/* A wash, never a saturated block. */
svg.chart .charea { fill: var(--lilac); fill-opacity: .10; stroke: none; }
/* The peak, labelled on the chart so the number is readable without hovering.
   The ring is the surface colour, so the mark stays legible over its own line. */
svg.chart .chpeak { fill: var(--lilac); stroke: var(--well); stroke-width: 2; }
svg.chart .chlabel { fill: var(--text-2); font-size: 10px; font-variant-numeric: tabular-nums; }
svg.chart .chhair { stroke: var(--faint); stroke-width: 1; display: none; }
svg.chart .chdot { fill: var(--lilac); stroke: var(--well); stroke-width: 2; display: none; }
/* One tooltip for all three charts: the reader aims at a moment, not a line. */
.charttip {
  display: none; position: absolute; top: 8px; z-index: 2; pointer-events: none;
  background: var(--float); border: 1px solid var(--line); border-radius: 4px;
  padding: 6px 9px; font-size: 11px; color: var(--muted); min-width: 150px;
}
.charttip .tiphead { color: var(--text-2); margin-bottom: 3px; }
.charttip .tiprow { display: flex; align-items: baseline; gap: 6px; }
/* A short stroke of the series colour keys the row; at this density a filled
   box is data-weight ink doing a label's job. */
.charttip .tipkey {
  width: 10px; height: 2px; border-radius: 1px; background: var(--lilac); flex: none;
  transform: translateY(-3px);
}
.charttip strong { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
/* The size report is a fixed-width table `pack -stats` formats with spaces;
   it scrolls inside itself rather than widening the page. */
pre.sizereport {
  margin: 10px 0 0; padding: 10px 12px; background: var(--well); border: 1px solid var(--line-soft);
  border-radius: 4px; font-size: 11px; line-height: 1.45; color: var(--muted);
  overflow-x: auto; max-height: 420px; overflow-y: auto; white-space: pre;
}
/* Pager + the read stamp. The section never refreshes itself, so "read at
   <clock>" is part of the contract, not decoration: it says how old what you
   are looking at is, and Reload is how it moves. */
#queuepager {
  margin: 10px 0 0; padding: 0 8px;
  display: flex; align-items: center; gap: 8px;
}
#queuepager button {
  background: var(--raised); color: var(--muted); border: 1px solid var(--line);
  border-radius: 4px; padding: 3px 9px; font-size: 12px; cursor: pointer;
}
#queuepager button:hover:enabled { color: var(--text); border-color: var(--line-hi); }
#queuepager button:disabled { opacity: .45; cursor: default; }
#queuepager #q_range { margin-right: auto; }
#queuemsg { padding: 12px 8px; font-size: 13px; }
/* Re-sim request box: the queue's own intake, for a game nobody uploaded.
   Framed like the dropzone above it — the two are the pipeline's two doors. */
#resimbox {
  margin: 0 0 14px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: 6px; font-size: 12px; color: var(--dim);
}
#resimbox label { display: block; margin-bottom: 7px; }
#resimbox .row { display: flex; gap: 8px; }
#resimbox input {
  flex: 1; min-width: 0; background: var(--well); color: var(--text);
  border: 1px solid var(--line); border-radius: 4px; padding: 4px 8px; font-size: 12px;
}
#resimbox input:focus { outline: none; border-color: var(--accent); }
#resimbox button {
  background: var(--raised); color: var(--muted); border: 1px solid var(--line);
  border-radius: 4px; padding: 4px 11px; font-size: 12px; cursor: pointer; white-space: nowrap;
}
#resimbox button:hover:enabled { color: var(--text); border-color: var(--line-hi); }
#resimbox button:disabled { opacity: .45; cursor: default; }
#resimstatus { margin-top: 8px; font-size: 12px; color: var(--text-2); }
#resimstatus.error { color: #e2785b; }
#resimstatus.ok { color: #6fd07f; }

/* The lavabalance upload preview (index.html #lbdialog, app.js
   previewLavabalance): a native <dialog>, so Escape and focus trapping come
   free. Two <pre> blocks — the curl wraps (a paste target, not a table) and
   the body scrolls, since a 16-player detail is a few hundred lines. */
#lbdialog {
  width: min(900px, calc(100vw - 40px)); max-height: calc(100vh - 40px); padding: 0;
  background: var(--float); color: var(--text-2); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 8px 30px rgba(0,0,0,0.6); font-size: 12px;
}
#lbdialog::backdrop { background: rgba(0,0,0,0.55); }
#lbdialog .lbhead {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--line); font-size: 13px; color: var(--text);
}
#lbdialog #lbtitle { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
#lbdialog #lbclose {
  font: inherit; font-size: 16px; line-height: 1; padding: 2px 8px; cursor: pointer;
  color: var(--muted); background: transparent; border: 1px solid transparent; border-radius: 6px;
}
#lbdialog #lbclose:hover { color: var(--text); border-color: var(--line-hi); }
#lbdialog #lbstatus { padding: 6px 14px 0; min-height: 1.4em; }
#lbdialog #lbstatus.error { color: #e2785b; }
#lbdialog #lbstatus.ok { color: #6fd07f; }
#lbdialog .lbsection { padding: 8px 14px 12px; }
#lbdialog .lblabel {
  display: flex; align-items: center; gap: 8px; margin-bottom: 5px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--dim);
}
#lbdialog #lbcopy {
  font: inherit; font-size: 11px; text-transform: none; letter-spacing: 0; line-height: 1;
  padding: 3px 8px; cursor: pointer; color: var(--muted); background: var(--raised);
  border: 1px solid var(--line); border-radius: 4px;
}
#lbdialog #lbcopy:hover:enabled { color: var(--text); border-color: var(--line-hi); }
#lbdialog pre {
  margin: 0; padding: 8px 10px; background: var(--well); border: 1px solid var(--line); border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px;
  line-height: 1.45; color: var(--text); overflow: auto;
}
#lbdialog #lbcurl { white-space: pre-wrap; word-break: break-all; }
#lbdialog #lbjson { max-height: 45vh; }

#wrap { display: flex; flex: 1; min-height: 0; }
/* min-width/min-height 0: the canvases carry the px size of the LAST resize
   inline, and a flex item's automatic minimum is its content, so #left would
   otherwise refuse to shrink below it. resize() measures #left, so the stale
   size fed itself back: a clip opened after the Replay tab had been shown kept
   the full-page width inside its small window and drew the map off to the
   right, mostly black. */
#left { flex: 1; position: relative; min-width: 0; min-height: 0; overflow: hidden; }
canvas { display: block; width: 100%; height: 100%; background: transparent; cursor: crosshair; }
/* WebGL icon overlay: sits on the 2D base canvas; input goes to #cv below. */
#glcv { position: absolute; left: 0; top: 0; background: transparent; pointer-events: none; }
#ovcv { position: absolute; left: 0; top: 0; background: transparent; pointer-events: none; }

/* The sidebar holds two CARDS on the page background rather than being a
   panel itself: the game (clock, the two sides, every player) and the chat.
   The game card takes the height it needs (scrolling inside itself past the
   column's height) and the chat takes what is left. */
#side {
  width: 352px; padding: 10px 10px 10px 0;
  display: flex; flex-direction: column; gap: 10px; min-height: 0; font-size: 13px;
}
.card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 10px 12px;
}
#teamcard { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
#chatwrap { flex: 1 1 160px; min-height: 120px; display: flex; flex-direction: column; }
.card h2, .cardhead h2 {
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--faint); margin: 0; font-weight: 600;
}
.cardhead { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
/* The clock: the moment the whole card is a reading of. */
.clock { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.clock b { font-size: 26px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums; line-height: 1; }
.clock .hint { font-variant-numeric: tabular-nums; }

/* ---- team statistics bar ----
   The two sides compared, one row per metric, on the team card between the
   clock and the player table and ruled off from the table: these rows are
   about the two SIDES, the table about the players.

   The shape is BAR's (see renderTeamStats): each value in a KNOB of its side's
   colour with the number written across it, the lead in a knob of its own
   riding the split, and between them two bars in a darkened version of each
   side's colour, sharing the leftover width in proportion to the values. A
   number on a block of team colour is what makes both readable at once — the
   colour says whose it is, the ink is picked off the colour's luminance, and
   neither has to compromise for the other. What is NOT copied is the widget's
   game-HUD finish: no bevels, no lit top edge, no gradients, since next to
   this sidebar's flat surfaces those read as a transplant. */
#teamstats {
  display: flex; flex-direction: column; gap: 3px;
  padding-bottom: 10px; margin-bottom: 4px; border-bottom: 1px solid var(--line-soft);
}
.tsrow { display: flex; align-items: center; gap: 6px; }
/* Same slot and muted tone as the player rows' OS column, so the metric names
   line up with the column of numbers beneath them. */
.tslabel {
  flex: 0 0 26px; text-align: right;
  color: var(--faint); font-size: 11px; line-height: 1;
}
.tsbar { flex: 1; min-width: 0; display: flex; align-items: center; gap: 2px; }
/* Knobs size to their own text, so "504k" and "7.0k" each get exactly the room
   they need and the bars take the rest; tabular figures keep a knob from
   breathing in and out as its last digit changes during playback. */
.tsknob {
  flex: 0 0 auto; padding: 2px 5px; border-radius: 3px;
  font-size: 11px; font-weight: 600; line-height: 1.35;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* min-width:0 with a zero basis is what lets a bar vanish entirely when its
   side has nothing, instead of forcing the row wider than the sidebar. */
.tsfill { flex-basis: 0; min-width: 0; height: 7px; border-radius: 2px; }

/* ---- player table ----
   Every player in one table under one header row (app.js renderPlayers):
   rank / flag / name / OS on the left, the metal + energy meters on the
   right, laid out like BAR's in-game player list. The columns are fixed
   widths, which is what lines them up across the two sides. Each side opens
   with a row of its own tinted in its colour (--c), and each player row
   carries its team colour as an edge. */
#players { display: flex; flex-direction: column; }
.pcols, .pside, .prow { display: flex; align-items: center; gap: 6px; padding: 2px 0 2px 8px; }
.pcols {
  padding-top: 4px; padding-bottom: 4px; font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--faint); font-weight: 600;
}
.pcname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Over a meter: its width, the text at its right edge like the meter's number. */
.pcres { display: block; width: 70px; padding-right: 4px; text-align: right; }
.pcols .os { color: inherit; font-size: inherit; }
.pgroup { margin-top: 4px; }
/* Sides of one (a free-for-all) have no side row to space them apart. */
.pgroup:not(:has(.pside)) { margin-top: 0; }
.pside {
  padding-top: 4px; padding-bottom: 4px; border-radius: 6px; color: var(--text);
  background: color-mix(in srgb, var(--c) 22%, transparent);
  box-shadow: inset 3px 0 0 var(--c);
}
.pside .pcname { font-size: 12px; font-weight: 700; }
.pside .pcount { margin-left: 6px; color: var(--muted); font-weight: 500; font-size: 11px; }
.pside .os { color: var(--text-2); }
.prow {
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--c) 80%, transparent);
}
.prow:hover { background: var(--raised); }
.prow .phead, .pcols .phead, .pside .phead {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 5px;
  font-size: 12px; line-height: 1.3;
}
/* The spectators: one quiet line at the table's foot, every name in its
   tooltip. */
#players .specs { margin-top: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#players .specs b { color: var(--muted); font-weight: 600; }
/* rank badge: BAR's chevron/star icon (served from /ranks/), constant-size slot
   so names align whether or not a player has a rank. */
.prow .rank {
  flex: 0 0 auto; width: 18px; height: 14px; object-fit: contain;
  image-rendering: auto;
}
span.rank { width: 18px; } /* empty placeholder keeps the column when no icon */
.prow .flag { flex: 0 0 auto; width: 15px; text-align: center; font-size: 12px; line-height: 1; }
.prow .os, .pcols .os, .pside .os {
  flex: 0 0 30px; text-align: right;
  color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11px;
}
.prow .pname {
  flex: 1; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* right block: the two resource meters, metal then energy, SIDE BY SIDE — one
   line per player, since stacking them made the list twice as tall as the roster
   it lists. The number sits ON its own bar — width = this player's income
   against the largest any team sustained in the last minute — so scanning the
   column ranks the economies at a glance. Hovering the row swaps BOTH halves at
   once to that resource's stock against the player's own storage cap, so the bar
   and the number under the cursor always mean the same thing; that is why all
   four elements share one fixed-width slot. Metal is silver, energy yellow (as
   in game), which is what tells the two meters apart on one line. */
.pres { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
.resrow { display: flex; align-items: center; font-size: 10px; line-height: 1; }
.rmeter {
  position: relative; display: block; width: 70px; padding: 3px 4px;
  background: var(--raised); border-radius: 3px; overflow: hidden;
  text-align: right; font-variant-numeric: tabular-nums;
}
.rmeter > i { position: absolute; inset: 0 auto 0 0; opacity: .32; } /* the bar */
.rmeter > b { position: relative; font-weight: 600; } /* above the bar */
/* Income bar + income number by default; stock bar + stock number on hover. The
   pairs are swapped together — a storage fill under an income figure would read
   as one measurement and be neither. */
.bstore, .rstore { display: none; }
.prow:hover .binc, .prow:hover .rinc { display: none; }
.prow:hover .bstore { display: block; }
.prow:hover .rstore { display: inline; }
/* Numbers follow the bar colour: metal white/silver, energy yellow. */
.resrow.metal .rmeter > i { background: #cfd6dd; }
.resrow.metal .rmeter > b { color: #cfd6dd; }
.resrow.energy .rmeter > i { background: #e3c34a; }
.resrow.energy .rmeter > b { color: #e3c34a; }

#tooltip {
  position: absolute; pointer-events: none; display: none;
  background: var(--float); border: 1px solid var(--line-hi); border-radius: 4px;
  padding: 7px 9px; font-size: 12px; line-height: 1.5; color: var(--text);
  max-width: 240px; z-index: 10; box-shadow: 0 4px 10px rgba(0,0,0,0.5);
}
#tooltip h3 { margin: 0 0 3px; font-size: 12px; color: #fff; }
/* The internal def name next to the human one ("Construction Bot armck"). */
#tooltip h3 .code { color: var(--dim); font-weight: normal; margin-left: 5px; }
#tooltip .row { display: flex; justify-content: space-between; gap: 12px; }
#tooltip .label { color: var(--dim); }
#tooltip .bar { height: 5px; background: var(--raised); border-radius: 3px; margin: 3px 0 1px; overflow: hidden; }
#tooltip .bar > div { height: 100%; }
/* The shield row: label, a charge bar filling the middle, the number. */
#tooltip .row.shield { align-items: center; gap: 8px; }
#tooltip .shbar { flex: 1; min-width: 50px; height: 5px; background: var(--raised); border-radius: 3px; overflow: hidden; }
#tooltip .shbar > i { display: block; height: 100%; }

#zoomlabel {
  position: absolute; left: 10px; bottom: 8px; pointer-events: none;
  background: var(--float); border: 1px solid var(--line); border-radius: 999px;
  padding: 3px 10px; font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums; z-index: 5;
}

/* ---- the map's Layers popover (index.html #layers) ----
   The handful of view toggles that earn a control rather than a constant at
   the top of app.js, floating in the map's top-right corner. */
#layers { position: absolute; top: 10px; right: 10px; z-index: 6; }
#layersbtn {
  display: flex; align-items: center; gap: 6px; padding: 5px 11px 5px 9px; border-radius: 999px;
  font-size: 12px; color: var(--muted); background: var(--float); border-color: var(--line);
}
#layersbtn:hover, #layersbtn[aria-expanded="true"] { color: var(--text); border-color: var(--line-hi); }
#layersbtn svg {
  width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round;
}
#layerspanel {
  position: absolute; right: 0; top: calc(100% + 6px); width: 230px; padding: 8px 10px;
  background: var(--float); border: 1px solid var(--line-hi); border-radius: 10px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5); font-size: 13px;
}
#layerspanel[hidden] { display: none; }
#layerspanel label { display: flex; align-items: center; gap: 8px; padding: 4px 0; cursor: pointer; }
#layerspanel input { accent-color: var(--accent); margin: 0; }
#layerspanel .hint { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line-soft); }
#clipbody #layers { display: none; }

/* ---- chat transcript ----
   The whole conversation, not a ticker: lines past the playhead stay listed but
   dimmed, so the panel reads as a transcript you can scroll and click (a click
   seeks to when the line was said). It scrolls itself to keep the playhead in
   view during playback. */
#chat {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  font-size: 12px; line-height: 1.45;
}
/* The filter (app.js setChatFilter): the box carries the one it applies, and
   the rows it does not keep drop out. */
#chat.only-ally .chatrow:not(.ally), #chat.only-spec .chatrow:not(.byspec) { display: none; }
.pills { display: flex; gap: 4px; }
.pills button {
  padding: 2px 9px; border-radius: 999px; font-size: 11px;
  background: var(--raised); border-color: var(--line); color: var(--muted);
}
.pills button.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.chatrow { padding: 2px 0; cursor: pointer; word-break: break-word; }
.chatrow:hover { background: var(--raised); }
/* Not said yet at the playhead: not shown at all, so the log never spoils
   what is about to happen and the newest line is always the last one. */
.chatrow.future { display: none; }
.chattime {
  color: var(--faint); font-variant-numeric: tabular-nums; margin-right: 5px;
  font-size: 11px;
}
.chattag {
  color: var(--muted); background: var(--raised); border-radius: 3px; padding: 0 4px;
  margin-right: 4px; font-size: 10px; text-transform: uppercase;
}

.hint { font-size: 11px; color: var(--faint); line-height: 1.5; }
#empty {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  text-align: center; color: var(--faint); font-size: 14px; padding: 24px;
}

/* Catalog filters above the list. One wrapping row of controls, sized to the
   table it filters; every control narrows the SQL query behind GET
   /api/replays rather than hiding rows client-side, so the count reported at
   the end is the true number of matching replays, not what fits on a page. */
#filters {
  max-width: 1650px; margin: 0 auto 10px; padding: 8px 12px;
  border: 1px solid var(--line); border-radius: 6px; background: var(--panel);
  display: flex; flex-direction: column; gap: 8px;
  font-size: 12px; color: var(--dim);
}
/* Two rows: the fields, then the settings chips with the actions pushed to
   the far end. Both wrap on their own, so a narrow window folds each row
   rather than mixing chips back in among the inputs. */
#filters .filterrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
#filters .filterfill { flex: 1 1 auto; }
/* Pager: Prev/Next around the range being shown. It sits above the table and
   hides itself entirely when everything fits on one page. */
#homepager {
  max-width: 1650px; margin: 0 auto 6px;
  display: flex; align-items: center; gap: 10px;
  font-size: 12px; color: var(--dim);
}
#homepager button {
  background: var(--raised); color: var(--muted); border: 1px solid var(--line);
  border-radius: 4px; padding: 3px 9px; font-size: 12px; cursor: pointer;
}
#homepager button:hover:enabled { color: var(--text); border-color: var(--line-hi); }
#homepager button:disabled { opacity: .4; cursor: default; }
#homepager #p_range { font-variant-numeric: tabular-nums; }
#filters label { display: flex; align-items: center; gap: 5px; }
#filters input, #filters select {
  background: var(--well); color: var(--text-2); border: 1px solid var(--line);
  border-radius: 4px; padding: 3px 5px; font-size: 12px; font-family: inherit;
}
#filters input:focus, #filters select:focus { outline: none; border-color: var(--accent); }
#filters input[type=search] { width: 130px; }
/* Map names run long ("All That Glitters v2.2.3"); the combobox needs the room. */
#filters #f_map { width: 200px; }
/* Players: two range inputs stacked over one track, so a single strip carries
   both bounds. The inputs are stripped bare (no native track, no background,
   the box-model padding above undone) and made transparent to the pointer —
   only their THUMBS take presses, which is what lets the track and the
   selected span below show through, and what makes two separate thumbs
   grabbable on one strip. */
#filters .dualrange {
  position: relative; width: 132px; height: 18px; flex: none;
}
#filters .dualrange .rangetrack {
  position: absolute; left: 0; right: 0; top: 50%; height: 4px; margin-top: -2px;
  background: var(--well); border: 1px solid var(--line); border-radius: 3px;
}
/* The selected span. Dim while it spans everything (that is not a filter) and
   lit once either thumb has narrowed it, so "am I filtering" is answerable
   from across the bar. */
#filters .dualrange .rangefill {
  position: absolute; top: 0; bottom: 0; background: var(--line-hi); border-radius: 3px;
}
#filters .dualrange.narrowed .rangefill { background: var(--accent); }
#filters .dualrange input[type=range] {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  margin: 0; padding: 0; border: none; background: transparent;
  -webkit-appearance: none; appearance: none; pointer-events: none;
}
#filters .dualrange input[type=range]::-webkit-slider-runnable-track { background: transparent; border: none; }
#filters .dualrange input[type=range]::-moz-range-track { background: transparent; border: none; }
#filters .dualrange input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; pointer-events: auto; cursor: ew-resize;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--text); border: 1px solid var(--well); box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}
#filters .dualrange input[type=range]::-moz-range-thumb {
  pointer-events: auto; cursor: ew-resize;
  width: 13px; height: 13px; border-radius: 50%; border: 1px solid var(--well);
  background: var(--text); box-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}
#filters .dualrange input[type=range]:focus { outline: none; }
#filters .dualrange input[type=range]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 55%, transparent); }
#filters .dualrange input[type=range]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 55%, transparent); }
/* The chosen bounds, in the label. Fixed width so the bar does not shuffle
   sideways as the numbers change while dragging. */
#filters #f_sizeout, #filters #f_durout {
  min-width: 52px; font-variant-numeric: tabular-nums; color: var(--text-2);
}
#filters #f_map { max-width: 200px; }
#filters button {
  background: var(--raised); color: var(--muted); border: 1px solid var(--line);
  border-radius: 4px; padding: 3px 9px; font-size: 12px; cursor: pointer;
}
#filters button:hover:enabled { color: var(--text); border-color: var(--line-hi); }
#filters button:disabled { opacity: .45; cursor: default; }
#filters button.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
/* Admin-only controls in the bar (admin mode), same gate as the list's
   per-row refresh button. */
#filters .admin { display: none; }
body.admin-mode #filters .admin { display: inline-block; }
#filters #f_count { margin-left: auto; }
/* Settings filters reuse the row badges' look, but as toggles: dim until
   picked, so the bar reads as "nothing selected" at a glance. */
#filters .chips { display: flex; flex-wrap: wrap; gap: 5px; }
#filters .chip {
  padding: 2px 7px; border-radius: 9px; font-size: 11px; cursor: pointer;
  border: 1px solid var(--line); background: var(--well); color: var(--dim); user-select: none;
}
#filters .chip:hover { color: var(--text-2); border-color: var(--line-hi); }
#filters .chip.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* Drag&drop publishing (the landing view's dropzone). */
#dropzone {
  display: none;
  max-width: 1650px; margin: 0 auto 14px; padding: 14px 18px;
  border: 1px dashed var(--line-hi); border-radius: 6px;
  font-size: 12px; color: var(--dim); text-align: center;
}
#dropzone.drag, #dropzone.busy { display: block; }
#dropzone.busy:not(.drag) #droplabel { display: none; }
#dropzone code { color: var(--muted); }
#dropzone.drag { border-color: var(--accent); background: var(--accent-soft); color: var(--text-2); }
#dropzone .browse { color: var(--accent); cursor: pointer; text-decoration: underline; }
#dropzone a { color: var(--accent); }
#dropstatus { margin-top: 8px; font-size: 12px; color: var(--text-2); }
#dropstatus.error { color: #e2785b; }
#dropstatus.ok { color: #6fd07f; }

/* Point-of-view marking in the header. Pushed to the right so it never
   crowds the replay title, which is the thing being read. */
#viewmark {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}
#viewmark select { font-size: 12px; }
/* Reserves its own width so the header does not reflow as the text changes
   between "saving…" and a result. */
#viewstatus { min-width: 8ch; font-size: 11px; color: var(--faint); }
#viewstatus.ok { color: #7fd18b; }
#viewstatus.err { color: #ff9c8f; }

/* ---- SQL cost table (admin) ------------------------------------------------
   The queue's dress, with every column but the method a number: right-aligned,
   tabular digits. The totals row closes the table like a ledger line. */
#sqlstatshead { max-width: 1100px; margin: 0 auto 10px; }
#sqlstatstable { border-collapse: collapse; margin: 0 auto; font-size: 13px; }
#sqlstatstable th {
  text-align: right; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--faint); font-weight: 600; padding: 4px 8px 6px 16px; border-bottom: 1px solid var(--line);
}
#sqlstatstable td {
  padding: 6px 8px 6px 16px; border-bottom: 1px solid var(--line-soft); text-align: right;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
#sqlstatstable th:first-child, #sqlstatstable td:first-child { text-align: left; padding-left: 8px; }
#sqlstatstable td.op { color: var(--lilac); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
#sqlstatstable td.dim { color: var(--faint); }
#sqlstatstable tfoot td { border-top: 1px solid var(--line); border-bottom: none; color: var(--text); font-weight: 600; }
#sqlstatstable tfoot td.op { color: var(--text); font-family: inherit; font-size: 13px; }
#sqlstatsbar { max-width: 1100px; margin: 10px auto 0; text-align: center; }
#sqlstatsbar button {
  background: var(--raised); color: var(--muted); border: 1px solid var(--line); border-radius: 4px;
  padding: 4px 12px; font-size: 12px; cursor: pointer;
}
#sqlstatsbar button:hover { color: var(--text); border-color: var(--line-hi); }
