/* Live dashboard styles. Loaded alongside shell.css + app.css by live.html.
   Kept separate so the main replay viewer doesn't carry tile-grid CSS it
   never uses, and so serve.py's CSP style-src can stay 'self' only (no
   'unsafe-inline' needed).

   All colors resolve through the CSS variables in app.css :root + the
   :root[data-theme="light"] overrides, so a host that flips
   <html data-theme="..."> retypes the dashboard automatically. Translucent
   overlays use color-mix() against var(--text) so the hover lift goes from
   "white wash on dark" to "ink wash on white" without a per-theme rule. */

body.live { background: var(--bg); color: var(--text); }

/* Toolbar row above the grid: holds the display-option toggles, the
   fullscreen button, and the status bar. Lives inside <main> rather than
   the shell header so leagues that strip the generic top bar still get a
   functional control row. Max-width aligns with .live-grid below. */
.live-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 10px 14px 0;
  max-width: 2400px;
  margin: 0 auto;
}

.live-grid {
  display: grid;
  /* auto-fit (not auto-fill) collapses empty columns so one tile grows to
     fill the row instead of squatting at the min-width. Min is the smallest
     readable size; columns otherwise share the available width equally.
     The min is exposed as --tile-min-w so the header slider can resize all
     tiles at once; live.js writes the var on :root from localStorage. */
  grid-template-columns: repeat(auto-fit, minmax(var(--tile-min-w, 360px), 1fr));
  gap: 14px;
  padding: 14px;
  max-width: 2400px;
  margin: 0 auto;
}

.tile {
  background: var(--card);
  border: 1px solid var(--line-2);
  border-radius: 10px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
  /* Cap a single tile so it doesn't balloon on a wide monitor when only
     one server is connected. The tile centers within its column via the
     auto margins. */
  max-width: 960px;
  margin-inline: auto;
  width: 100%;
  /* Container-query context so the reaction bar can collapse when the
     tile itself is narrow (mobile, or many-tiles grid where each column
     shrinks). Inline-size only — we don't constrain by height. */
  container-type: inline-size;
  container-name: tile;
}
.tile.stale { opacity: 0.6; }

.tile header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  font-size: 13px;
  line-height: 1.2;
}
/* Wide tiles: each .head-row uses display:contents so its children flatten
   into the header's single flex line. CSS `order` on .score / .shot-btn /
   .focus-btn (paired with display:none on .spacer-main) reproduces the
   original sequence — name, meta, count, watchers, tickrate, buffer,
   [spacer-chips: flex 1], score, shot, focus. The narrow override below
   undoes both, so each .head-row becomes its own stacked flex row. */
.tile header .head-row { display: contents; }
.tile header .spacer-main { display: none; }
.tile header .spacer-chips { flex: 1; order: 50; }
.tile header .score { order: 90; }
.tile header .shot-btn { order: 100; }
.tile header .focus-btn { order: 101; }
@container tile (max-width: 520px) {
  .tile header .head-row {
    display: flex;
    flex: 1 1 100%;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
  }
  .tile header .head-row.main { flex-wrap: nowrap; }
  .tile header .head-row.main .name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  .tile header .head-row.chips { flex-wrap: wrap; }
  /* Reset orders inside each row so DOM order rules each row independently:
     row 1 = name, meta, [spacer-main: flex 1], score (score hard-right);
     row 2 = count, watchers, tickrate, buffer, [spacer-chips: flex 1],
     shot, focus (buttons hard-right). */
  .tile header .score,
  .tile header .shot-btn,
  .tile header .focus-btn,
  .tile header .spacer-chips { order: 0; }
  .tile header .spacer-main { display: block; flex: 1; }
}
.tile .name { font-weight: 700; color: var(--text); }
.tile .meta { color: var(--muted); font-size: 12px; }
.tile .players-count { color: var(--muted); font-size: 12px; }
/* Watcher count: eye-icon glyph + integer. Same muted styling as the
   sibling player count, with tabular-nums so the value doesn't dance
   when ticking up/down. */
.tile .watchers-count {
  color: var(--muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.tile .tickrate { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
/* Buffer-fill ring: visible only while a tile is in warmup (renderTs has
   not yet caught up to the oldest buffered frame). Fixed-size SVG glyph
   instead of percentage text so the header doesn't reflow as the value
   ticks. Gold to read as a transient "loading" cue, distinct from the
   muted always-on tickrate. */
.tile .buffer-fill {
  display: none;
  width: 14px;
  height: 14px;
  align-self: center;
}
.tile .buffer-fill svg {
  width: 100%;
  height: 100%;
  display: block;
  /* Start the arc at 12 o'clock and fill clockwise as stroke-dashoffset
     drops from circumference to 0. */
  transform: rotate(-90deg);
}
.tile .buffer-fill svg .bg {
  fill: none;
  stroke: color-mix(in srgb, #f5c542 25%, transparent);
  stroke-width: 2.25;
}
.tile .buffer-fill svg .fg {
  fill: none;
  stroke: #f5c542;
  stroke-width: 2.25;
  stroke-linecap: round;
  transition: stroke-dashoffset 120ms linear;
}
.tile.buffering .buffer-fill { display: inline-block; }
body.live.fullscreen .tile .buffer-fill { display: none; }

/* Render-option toggles in the header. A single flip applies to every tile
   because the renderer reads state.* each frame — no per-tile plumbing. */
.live-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  /* Allow the controls row to shrink and wrap freely inside the toolbar's
     flex parent — without min-width: 0 it would refuse to wrap below its
     intrinsic content width and push the status-bar off-screen on phones. */
  min-width: 0;
}
/* Each control unit (toggle/slider/select/button) is treated as an
   indivisible chip — wrapping happens between chips, not in the middle of
   "follow puck" or "tile size 360". */
.live-controls > * { white-space: nowrap; }
.live-controls .toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 12px;
  cursor: pointer;
  user-select: none;
}
.live-controls .toggle input {
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}
/* Tile-size slider. Lives next to the toggles in .live-controls and writes
   --tile-min-w via live.js. Hidden in fullscreen because that mode computes
   its own column count (recomputeFsCols) and the slider would conflict. */
.live-controls .tile-size {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 12px;
  user-select: none;
}
.live-controls .tile-size input[type="range"] {
  cursor: pointer;
  accent-color: var(--accent);
  /* Was a fixed 160px which wrapped awkwardly on narrow viewports. Clamp
     gives the slider a sensible width on desktop without forcing a row
     break on a phone. */
  width: clamp(96px, 30vw, 160px);
  min-width: 0;
}
/* Buffer-size dropdown: jitter-absorption window for live playback. Mirrors
   .tile-size visually so the toolbar stays uniform; the <select> is plain
   to honor the user's OS dropdown styling. */
.live-controls .buffer-size {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 12px;
  user-select: none;
}
.live-controls .buffer-size select {
  cursor: pointer;
  background: transparent;
  color: var(--text);
  border: 1px solid var(--line-2);
  border-radius: 4px;
  padding: 1px 4px;
  font: inherit;
}
/* Rink-mode dropdown: same shape as buffer-size and tile-size, used to
   pin the rink palette to dark or light overriding the page theme, or
   leave it 'auto' to follow the theme. */
.live-controls .rink-mode {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: 12px;
  user-select: none;
}
.live-controls .rink-mode select {
  cursor: pointer;
  background: transparent;
  color: var(--text);
  border: 1px solid var(--line-2);
  border-radius: 4px;
  padding: 1px 4px;
  font: inherit;
}
/* Header-level fullscreen toggle. Mirrors the per-tile .focus-btn glyph-swap
   so users pick up the same visual language ("⤢/⛶" enter, "✕" exit). */
.live-controls .fs-btn {
  border: 1px solid var(--line-2);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  padding: 2px 8px;
  font-size: 14px;
  line-height: 1;
  border-radius: 4px;
}
.live-controls .fs-btn:hover {
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text);
}
.live-controls .fs-btn::before { content: "\26F6"; }
body.live.fullscreen .live-controls .fs-btn::before { content: "\2715"; }

/* Toolbar reflow for narrow viewports.
   Tablet & below: tighten the toolbar's outer gaps, then force a line break
   before the status bar so it owns its own row at predictable alignment
   instead of dangling at whatever residual width is left next to a wrapped
   controls row. The .navspacer is flex:1 in app.css; pinning flex-basis:100%
   converts it from "fill the gap" to "wrap to next line" without a JS toggle. */
@media (max-width: 720px) {
  .live-toolbar { gap: 8px; padding: 8px 10px 0; }
  .live-controls { gap: 4px 8px; }
  .live-toolbar .navspacer { flex-basis: 100%; height: 0; }
  .status-bar { padding: 4px 10px 0; width: 100%; gap: 12px; }
}
/* Phone widths: the auto-fit grid is already at 1 column (minmax 360px ≥
   typical phone width), so the tile-size slider has no visible effect on
   layout. Hide it to reclaim a row. Buffer dropdown stays — mobile networks
   often need the latency-vs-smoothness knob more than desktop does. */
@media (max-width: 480px) {
  .live-controls .tile-size { display: none; }
  .live-toolbar { padding: 6px 8px 0; }
  .live-controls .toggle,
  .live-controls .buffer-size,
  .live-controls .rink-mode { font-size: 11px; }
}
.tile .spacer { flex: 1; }
.tile .score { font-variant-numeric: tabular-nums; }
.tile .score .b { color: var(--team-blue); font-weight: 700; }
.tile .score .r { color: var(--team-red);  font-weight: 700; }

/* Focus toggle: the ⤢ glyph is injected via ::before so the JS stays text-
   free and so we can swap it to ✕ when focused purely in CSS. */
.tile .focus-btn {
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  padding: 2px 6px;
  font-size: 13px;
  line-height: 1;
  border-radius: 4px;
  flex: 0 0 auto;
}
.tile .focus-btn:hover {
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text);
}
.tile .focus-btn::before { content: "\2922"; }
.tile.focused .focus-btn::before { content: "\2715"; }

/* Screenshot button: mirrors .focus-btn (same chrome, different glyph).
   Camera glyph (U+1F4F7) injected via ::before so JS stays text-free. */
.tile .shot-btn {
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  padding: 2px 6px;
  font-size: 13px;
  line-height: 1;
  border-radius: 4px;
  flex: 0 0 auto;
}
.tile .shot-btn:hover {
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text);
}
.tile .shot-btn::before { content: "\1F4F7"; }

/* When any tile is focused, hide the rest and let the focused tile grow to
   fill the grid row. display:none removes the others from the grid so the
   focused tile's max-width cap is the only remaining constraint. */
.live-grid.has-focused { grid-template-columns: 1fr; }
.live-grid.has-focused .tile:not(.focused) { display: none; }
.tile.focused { max-width: none; }

/* "Populated only" mode: hide tiles whose latest frame had zero players
   on team Blue/Red (idle servers + spectator-only servers). The .empty
   class is maintained in live.js from frame ingestion + applyTileState. */
.live-grid.populated-only .tile.empty { display: none; }

/* "Rink only" mode: strip the per-tile roster and reaction controls so the
   canvas and tile header are all that's left. Driven by a single class on
   the grid (set by the rinkOnly toggle in live.js) — no per-tile JS. The
   tile header (name + score + focus/screenshot buttons) and on-canvas
   overlays (chat bubbles, post-hit DINGs, idle "no players") are kept. */
.live-grid.rink-only .tile .roster,
.live-grid.rink-only .tile .reaction-bar,
.live-grid.rink-only .tile .reaction-trigger { display: none; }

/* Reaction bar lives between the rink canvas and the roster. Collapsed
   behind a single trigger button by default so the emoji row doesn't
   dominate the tile chrome — click the trigger to reveal the full row
   (flex layout applied via .tile.reactions-open below). */
.tile .reaction-bar {
  display: none;
  flex-wrap: wrap;
  gap: 4px;
  justify-content: center;
}
.tile.reactions-open .reaction-bar { display: flex; }
.tile .reaction-bar button {
  border: 1px solid var(--line-2);
  background: var(--card-2);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 6px;
  transition: background-color 120ms, transform 80ms;
}
.tile .reaction-bar button:hover  { background: color-mix(in srgb, var(--text) 10%, var(--card-2)); }
.tile .reaction-bar button:active { transform: scale(0.92); }
/* On idle / stale tiles the relay-side reaction still fires, but the
   incoming-reaction handler skips animation because the tile is already
   dimmed. Fade the bar here to signal that clicks won't produce visible
   reactions for anyone watching. */
.tile.idle .reaction-bar,
.tile.stale .reaction-bar { opacity: 0.45; }

/* Single button that expands the reaction bar. Always visible so the
   emoji row stays collapsed by default on every tile width — clicking
   toggles .reactions-open on the tile wrap, which reveals the bar. */
.tile .reaction-trigger {
  display: inline-flex;
  align-self: center;
  border: 1px solid var(--line-2);
  background: var(--card-2);
  color: inherit;
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  padding: 4px 12px;
  border-radius: 6px;
  transition: background-color 120ms, transform 80ms;
}
.tile .reaction-trigger:hover  { background: color-mix(in srgb, var(--text) 10%, var(--card-2)); }
.tile .reaction-trigger:active { transform: scale(0.96); }
.tile.idle .reaction-trigger,
.tile.stale .reaction-trigger { opacity: 0.45; }
.tile.reactions-open .reaction-trigger { background: color-mix(in srgb, var(--text) 10%, var(--card-2)); }

/* Floating reaction animations live in an absolute overlay over the canvas.
   pointer-events:none keeps them from swallowing clicks on roster rows or
   the focus button underneath. Each `.reaction-float` is added/removed
   dynamically; the CSS keyframe handles the full lifecycle. */
.reactions-overlay {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  border-radius: 6px;
}
.reaction-float {
  position: absolute;
  bottom: 8%;
  font-size: 28px;
  line-height: 1;
  transform: translate(-50%, 0);
  animation: reaction-rise 2500ms ease-out forwards;
  will-change: transform, opacity;
  user-select: none;
}
@keyframes reaction-rise {
  0%   { transform: translate(-50%, 0)                           scale(0.7); opacity: 0; }
  12%  { transform: translate(-50%, -10%)                        scale(1.1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--reaction-drift, 0px)), -140%) scale(0.9); opacity: 0; }
}

/* Chat bubbles. Stacked bottom-up over the canvas (left-aligned because
   reactions own the bottom-center area). pointer-events:none so the canvas
   below stays interactive. Capped to LIVE_CHAT_MAX_VISIBLE bubbles in JS.
   Kept visually subtle — bubbles share screen space with the live render,
   so the background is mostly transparent and text is small. Hovering the
   overlay lifts the opacity so a viewer actively reading can see clearly.
   Translucent backdrops use the canvas-bg color (--bg2) at low alpha so
   they read consistently in light + dark over the rink ice. */
.chat-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 45%;                    /* cap vertical coverage of the canvas */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  gap: 3px;
  padding: 4px 8px 8px;
  pointer-events: none;
  overflow: hidden;
  border-radius: 6px;
  opacity: 0.5;
  transition: opacity 180ms ease-out;
}
.tile:hover .chat-overlay { opacity: 0.95; }
.chat-bubble {
  max-width: 70%;
  padding: 2px 6px;
  font-size: 11px;
  line-height: 1.25;
  background: color-mix(in srgb, var(--bg2) 78%, transparent);
  color: var(--text);
  border-radius: 3px;
  border-left: 2px solid color-mix(in srgb, var(--muted) 55%, transparent);
  word-wrap: break-word;
  overflow-wrap: anywhere;
  transition: opacity 350ms ease-out;
  animation: chat-bubble-in 180ms ease-out;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55);
}
.chat-bubble-blue { border-left-color: color-mix(in srgb, var(--team-blue) 70%, transparent); }
.chat-bubble-red  { border-left-color: color-mix(in srgb, var(--team-red)  70%, transparent); }
.chat-bubble-team { background: color-mix(in srgb, var(--bg2) 65%, transparent); font-style: italic; }
/* System chat (goal announcements, offside/icing calls, votes, admin). The
   mod emits these with team="system"; no username prefix, so visually cue
   them as a ticker line: gold accent, tabular-leaning body, subtly brighter
   background than player bubbles so they read as "the server said this". */
.chat-bubble-system {
  border-left-color: color-mix(in srgb, var(--gold) 85%, transparent);
  background: color-mix(in srgb, var(--bg2) 55%, transparent);
  color: var(--gold);
  font-weight: 600;
}
.chat-bubble-system .chat-bubble-message { color: var(--gold); }
.chat-bubble-username { color: var(--text); font-weight: 600; }
.chat-bubble-blue .chat-bubble-username { color: var(--team-blue); }
.chat-bubble-red  .chat-bubble-username { color: var(--team-red); }
.chat-bubble-tag { color: var(--gold); font-weight: 700; margin-right: 4px; }
.chat-bubble-fade { opacity: 0; }
@keyframes chat-bubble-in {
  0%   { opacity: 0; transform: translateY(4px); }
  100% { opacity: 1; transform: translateY(0); }
}
/* Don't paint chat bubbles over the "no players" / "stale" overlays — the
   JS dispatcher already gates on state, but a tile that flips mid-fade
   shouldn't keep showing them. */
.tile.idle .chat-overlay,
.tile.stale .chat-overlay { opacity: 0; }

/* Ref-signal text overlay. Sits at the bottom-right of the canvas, just
   above the linesman PNGs that drawRefSignals() / renderRefSignalImages()
   paint into the canvas bitmap. The 22% bottom offset lines up with the
   ~20% PNG height the live tile renderer uses (heightFraction:0.20) plus
   a small gap. Mirrors replay's #refOverlay positioning at a smaller scale. */
.ref-overlay {
  position: absolute;
  bottom: 22%;
  right: 6px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: flex-end;
  gap: 3px;
  pointer-events: none;
  overflow: hidden;
  max-height: 30%;
  z-index: 3;
  /* Match the chat overlay so the rink visuals stay readable; lift on
     tile hover for the same reading-mode bump chat uses. */
  opacity: 0.5;
  transition: opacity 180ms ease-out;
}
.tile:hover .ref-overlay { opacity: 0.95; }
.ref-bubble {
  background: color-mix(in srgb, var(--bg2) 78%, transparent);
  color: var(--gold);
  font-weight: 600;
  text-align: right;
  font-size: 11px;
  line-height: 1.25;
  padding: 2px 6px;
  border-radius: 3px;
  border-right: 2px solid color-mix(in srgb, var(--gold) 55%, transparent);
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55);
  transition: opacity 350ms ease-out;
}
.ref-bubble.fading { opacity: 0; }
.tile.idle .ref-overlay,
.tile.stale .ref-overlay { opacity: 0; }

.canvas-wrap {
  position: relative;
}
.tile canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1000 / 620;
  background: var(--bg2);
  border-radius: 6px;
}
/* Idle overlay: covers the rink with a translucent backdrop and centered
   "No players connected" text when the mod has gated its pushes because
   the game server has no clients. Hidden by default; .tile.idle reveals. */
.tile-overlay {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  color: var(--text);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  pointer-events: none;
}
/* Overlay shows in two cases: explicit "idle" from the mod (no clients on the
   game server) and stale (no frames for >3s). In both cases the roster has
   been zeroed via clearRoster(), so the "No players connected" message
   matches the numbers in the header. */
.tile.idle .tile-overlay,
.tile.stale .tile-overlay { display: flex; }
/* When idle, suppress the .stale dim so an idle-but-connected server
   doesn't look like a broken one — the overlay already reads as "empty". */
.tile.idle { opacity: 1; }

.status-bar {
  padding: 10px 14px;
  font-size: 13px;
  color: var(--muted);
  display: flex;
  gap: 16px;
  align-items: center;
}
.status-bar .dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--muted); display: inline-block;
}
.status-bar.ok .dot   { background: var(--good); }
.status-bar.warn .dot { background: var(--gold); }
.status-bar.err .dot  { background: var(--bad); }
/* Viewer count sits to the right of the "connected" text. Muted tone so it
   reads as supplementary — it's a status indicator, not a primary control. */
.status-bar .viewer-count {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.status-bar .viewer-count[hidden] { display: none; }

.empty-hint {
  grid-column: 1 / -1;
  text-align: center;
  color: var(--muted);
  padding: 48px 16px;
}

/* Roster: two side-by-side columns under the canvas, one per team. Shows
   flag, number, username (linked to poncepuck via steamId), G·A, and ping.
   Skaters are sorted by number; goalies pinned to the top of their column
   via the pos-chip "G" prefix. Font is small-but-still-readable because
   tiles can pack 4+ per row on wide monitors. */
.tile .roster {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 10px;
  font-size: 12px;
  line-height: 1.35;
}
.tile .roster-col { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tile .roster-col.blue { border-left: 2px solid var(--team-blue); padding-left: 6px; }
.tile .roster-col.red  { border-left: 2px solid var(--team-red);  padding-left: 6px; }
.tile .roster-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  color: var(--text);
  cursor: pointer;
  border-radius: 3px;
  padding: 0 2px;
}
.tile .roster-row:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
/* Match the replay viewer's gold-accent selection so clicking a row on
   the live dashboard reads the same way it does in the offline stats table
   (see app.css .match-table tr.selected). */
.tile .roster-row.selected { background: color-mix(in srgb, var(--gold) 14%, transparent); }
.tile .roster-row.selected .pname { color: var(--gold); font-weight: 700; }
.tile .roster-row .flag {
  width: 14px; height: 10px;
  object-fit: cover;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(0,0,0,.35);
  flex: 0 0 auto;
}
/* Keep rows aligned when the country is missing or invalid. */
.tile .roster-row .flag-spacer { width: 14px; height: 10px; flex: 0 0 auto; display: inline-block; }
.tile .roster-row .num {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  min-width: 22px;
  flex: 0 0 auto;
}
.tile .roster-row .pos-chip {
  font-size: 10px;
  font-weight: 700;
  padding: 1px 4px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--gold) 18%, transparent);
  color: var(--gold);
  flex: 0 0 auto;
}
.tile .roster-row .pname {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1 1 auto;
  min-width: 0;
  color: inherit;
}
/* Profile link is a compact ↗ chip to the right of the name. Split out from
   the name so row-level click-to-highlight doesn't conflict with navigation.
   The :hover expands a tiny hit-target halo; the ↗ glyph itself is dim at
   rest so the row reads as "name first, link secondary". */
.tile .roster-row .plink {
  flex: 0 0 auto;
  color: var(--muted);
  text-decoration: none;
  font-size: 11px;
  line-height: 1;
  padding: 1px 4px;
  border-radius: 3px;
}
.tile .roster-row .plink:hover {
  color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 10%, transparent);
}
/* Keep the profile-link color neutral even inside a selected row so it
   doesn't get recolored gold by the pname rule above. */
.tile .roster-row.selected .plink { color: var(--muted); }
.tile .roster-row .ga {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  flex: 0 0 auto;
  min-width: 26px;
  text-align: right;
}
.tile .roster-row .ping {
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  flex: 0 0 auto;
  min-width: 40px;
  text-align: right;
}
.tile .roster-row .ping.ok   { color: var(--good); }
.tile .roster-row .ping.warn { color: var(--gold); }
.tile .roster-row .ping.bad  { color: var(--bad); }

/* On narrow viewports drop to a single column so ellipsis doesn't kick in
   on every row. The tile is already vertical-heavy on mobile — the roster
   just reads as a continuous list. */
@media (max-width: 560px) {
  .tile .roster { grid-template-columns: 1fr; }
}

/* ---------- Fullscreen mode ----------
   Entered via the Fullscreen API on <html>. JS adds `.fullscreen` to <body>.
   Same auto-fit minmax pattern as the default grid so the size slider
   (--tile-min-w) drives tile width smoothly — repeat(N, 1fr) would only
   change tile size on integer column-count crossings, which felt like the
   slider was "stepping" through two or three positions in fullscreen. The
   aim is scoreboard-style "see every rink at once", so the roster and
   reaction bar are hidden and the tile chrome is trimmed. Exit via the
   same button, F11, or Esc. */
body.live.fullscreen .live-grid {
  grid-template-columns: repeat(auto-fit, minmax(var(--tile-min-w, 360px), 1fr));
  max-width: none;
  padding: 6px;
  gap: 6px;
}
body.live.fullscreen .tile {
  max-width: none;
  padding: 4px;
  gap: 3px;
}
body.live.fullscreen .tile header { font-size: 12px; gap: 8px; }
body.live.fullscreen .tile .meta,
body.live.fullscreen .tile .players-count,
body.live.fullscreen .tile .watchers-count,
body.live.fullscreen .tile .tickrate,
body.live.fullscreen .tile .reaction-bar,
body.live.fullscreen .tile .roster { display: none; }
/* The sticky header shrink rule for fullscreen moved to shell.css so it
   only applies when the generic top bar is present. */
