/* ===========================================================================
   /data — THE CHARTS. The third document stylesheet, after site.css and
   archive.css, and it obeys the same rules they do.
   ===========================================================================

   site.css carries the chrome — nav, masthead, tally, prose, footer, both
   themes and the whole token ramp. NOTHING IN THAT FILE IS RESTATED HERE. This
   sheet is only the vocabulary /data adds: a bar row, a table of weapons, a
   country list, and the live indicator. /data loads both, in that order.

   WHY THE CHARTS ARE CSS AND NOT A CHART LIBRARY. The game has no build step and
   no runtime dependencies — `package.json` exists for dev-only tooling and is in
   `.vercelignore`, and `vendor/` is Three.js vendored verbatim for the game
   itself. Adding a charting bundle to a page whose entire job is nine bar lists
   would mean shipping ~80 KB of general-purpose layout engine, a second set of
   colour decisions that do not know about `--gold`, and a `script-src` argument
   with the CSP. A bar is a div with a width. Every chart on this page is that
   div, so they inherit the palette for free and there is nothing to keep in sync.

   THE RULES THIS SHEET IS HELD TO — scripts/validate-site-pages.mjs enforces all
   of them on this file, because it is in that script's SHEETS list:
     · nothing rounds past 2px
     · no coloured edge stripe wider than 1px — a row is distinguished by its
       surface, never by a bar down one side
     · no 1px border paired with a soft wide shadow
     · the dark palette must equal the game's, to the digit

   MEASURED, NOT ASSUMED. Every bar carries its own number as text beside it, and
   every grouping ends in a TOTAL row. A bar whose value has to be estimated from
   its length is decoration; the length is the comparison and the number is the
   fact, and this page is about facts.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   THE LIVE INDICATOR
   ---------------------------------------------------------------------------
   A ring that expands and fades out from under a solid dot — the sweep of a
   radar rather than a blinking LED, which is the thing that says "being counted"
   instead of "notification".

   IT IS NOT A CLAIM OF REAL TIME AND MUST NOT LOOK LIKE ONE. /api/player-data is
   cached at the edge for five minutes (sixty seconds for the menu pair), so the
   honest reading is "this is live data, minutes old". The label beside it says
   LIVE and the page states the refresh interval in words; the animation is the
   only part that could overpromise, so it is slow — a two-second sweep, not a
   heartbeat.

   The dot is only shown once real numbers have arrived. `[data-live='pending']`
   holds it still, so a failed fetch cannot leave a pulsing indicator over a row
   of dashes claiming to be live.
   --------------------------------------------------------------------------- */
.live-dot {
  position: relative;
  display: inline-block;
  flex: none;
  width: 7px;
  height: 7px;
  /* THE RING NEEDS ROOM OF ITS OWN. The sweep grows to 2.3× a box that is already
     inset -3px, so at its widest it is ~30px across — with a 7px gap it ran
     straight through the first letter of the label. The margin reserves the
     space the animation actually uses instead of the space the dot occupies at
     rest, which is the measurement that matters. */
  margin-right: 9px;
  border-radius: 50%;
  background: var(--live);
}

.live-dot::after {
  content: '';
  position: absolute;
  inset: -3px;
  border: 1px solid var(--live);
  border-radius: 50%;
  opacity: 0;
}

[data-live='ok'] .live-dot::after { animation: live-sweep 2.4s var(--ease) infinite; }

/* A stalled feed goes amber and stops moving. Silence and a still dot say
   "these numbers are the last good ones" without a paragraph of apology. */
[data-live='stale'] .live-dot { background: var(--gold); }
[data-live='error'] .live-dot { background: var(--ink-4); }

/* Nothing is live until numbers have arrived, so the pending dot is grey and
   still — a green pulse over a row of skeletons would be claiming to be live
   about nothing. */
[data-live='pending'] .live-dot { background: var(--ink-4); }

@keyframes live-sweep {
  0%   { opacity: 0.9; transform: scale(0.55); }
  70%  { opacity: 0;   transform: scale(2.3); }
  100% { opacity: 0;   transform: scale(2.3); }
}

/* No `gap`: the dot carries its own `margin-right` because the space it needs is
   the space its RING needs, not its own width. A gap here would add to that and
   the two would have to be kept in step by hand. */
.live-tag {
  display: inline-flex;
  align-items: center;
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   THE HEADLINE TALLY
   ---------------------------------------------------------------------------
   `.tally` itself is site.css's, unchanged — the same component the About page
   uses, so the two pages open with the same shape. What is added is the loading
   state and the two numbers that carry the live mark.
   --------------------------------------------------------------------------- */

/* Tabular figures on EVERY number on this page, and it is not a nicety. These
   values reload in place; proportional digits make the whole row reflow when
   42,123 becomes 42,124, which reads as the page glitching rather than as a
   number going up. */
.tally b,
.bar-val,
.dt td,
.dt th { font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
   LOADING
   ---------------------------------------------------------------------------
   A BAR THAT SWEEPS, NOT AN EM-DASH. The first version printed "—" in each tile,
   which is indistinguishable from a number that came back empty — the reader
   cannot tell "still counting" from "nothing to count", and those mean opposite
   things on this page. A moving skeleton can only mean the former.

   Sized to the digits it is standing in for, so the tally does not jump when the
   real figure lands: the tile is already the height of a number and roughly its
   width. A spinner would have been a different shape from its own result and
   would have made the whole row reflow on arrival.
   --------------------------------------------------------------------------- */
.tally b[data-pending]::after {
  content: '';
  display: block;
  width: 2.4em;
  height: 0.68em;
  margin: 0.17em 0;
  background: linear-gradient(90deg, var(--s2) 0%, var(--s3) 42%, var(--s2) 84%);
  background-size: 240% 100%;
  animation: skeleton 1.15s linear infinite;
}

@keyframes skeleton {
  from { background-position: 120% 0; }
  to   { background-position: -120% 0; }
}

/* The spinner beside "READING THE FEED…". One small rotating arc — the page's
   only indeterminate indicator, and it is shown only while the first request is
   outstanding. `currentColor` so it inherits the label's colour in both themes
   rather than declaring its own. */
.spinner {
  display: none;
  flex: none;
  width: 10px;
  height: 10px;
  margin-right: 8px;
  border: 1.5px solid color-mix(in srgb, currentColor 28%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin 620ms linear infinite;
}

[data-live='pending'] .spinner { display: inline-block; }
[data-live='pending'] .live-dot { display: none; }

@keyframes spin { to { transform: rotate(360deg); } }

/* Panels waiting on their first paint get the same treatment: three skeleton
   rows the height of the bars that will replace them, so the page does not
   collapse and then jump when nine charts arrive at once. */
.panel-body:empty::after {
  content: '';
  display: block;
  height: 96px;
  background:
    linear-gradient(90deg, var(--s2) 0%, var(--s3) 42%, var(--s2) 84%) 0 6px / 240% 17px no-repeat,
    linear-gradient(90deg, var(--s2) 0%, var(--s3) 42%, var(--s2) 84%) 0 39px / 240% 17px no-repeat,
    linear-gradient(90deg, var(--s2) 0%, var(--s3) 42%, var(--s2) 84%) 0 72px / 240% 17px no-repeat;
  animation: skeleton 1.15s linear infinite;
}

/* The number swaps behind a short fade rather than snapping, so a refresh that
   changes one tile does not read as a flicker. */
.tally b { transition: color var(--med) var(--ease); }
.tally > div[data-fresh] b { color: var(--gold-ink); }

/* ---------------------------------------------------------------------------
   PANELS
   ---------------------------------------------------------------------------
   A panel is a heading, a one-line note saying what the number actually means,
   and a chart. The note is not optional decoration: half of these figures are
   easy to misread — `deaths by weapon` is the weapon the victim was HOLDING, the
   country is IP-derived — and a chart that invites a wrong reading is worse than
   no chart.
   --------------------------------------------------------------------------- */
.panels {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  background: var(--rule);
}

.panel {
  display: flex;
  flex-direction: column;
  /* `min-width: 0` IS LOAD-BEARING AND IS THE FIX FOR A REAL OVERFLOW BUG.
     A grid item defaults to `min-width: auto`, which means it refuses to shrink
     below its content's minimum. The weapon table sets `white-space: nowrap` on
     every cell, so its min-content width is ~640px — the panel grew to that, the
     grid grew with it, and the whole PAGE scrolled sideways on a phone (645px of
     content in a 375px viewport) while `.dt-scroll` sat there with nothing to
     scroll. Zeroing the minimum lets the panel be as narrow as the column and
     hands the overflow back to `.dt-scroll`, which is built for it. */
  min-width: 0;
  padding: clamp(20px, 2.4vw, 30px);
  background: var(--s1);
}

/* Same reason, one level down: a flex child defaults to `min-width: auto` too, so
   without this the body would pass the table's min-content width back up. */
.panel-body { min-width: 0; }

/* The two widest tables get the full row on a multi-column grid. A twenty-row
   weapon table squeezed into half a viewport wraps its own headers. */
.panel.wide { grid-column: 1 / -1; }

.panel h2 {
  font: 400 clamp(21px, 2.1vw, 27px)/1.05 var(--display);
  letter-spacing: 0;
  text-transform: uppercase;
}

.panel-note {
  max-width: 68ch;
  margin-top: 7px;
  color: var(--ink-3);
  font-size: 12.5px;
  line-height: 1.65;
  text-wrap: pretty;
}

.panel-body { margin-top: 20px; }

/* A panel whose query failed says so IN THE PANEL. The endpoint returns the nine
   questions independently, so a timeout on weapons leaves the country table
   true — blanking the page for one failure would throw away eight correct
   answers, and showing zeroes would be a lie. */
.panel-error {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px solid var(--rule-strong);
  color: var(--ink-3);
  font-size: 12.5px;
  line-height: 1.6;
}

/* ---------------------------------------------------------------------------
   THE BAR ROW — the shape most of this page is made of
   ---------------------------------------------------------------------------
   label · track · value, in one grid so every row's bar starts and ends on the
   same two lines regardless of how long the label is. A per-row flex layout
   would ragged-edge the track and destroy the only thing a bar chart is for,
   which is comparing lengths at a glance.
   --------------------------------------------------------------------------- */
.bars { display: grid; gap: 2px; }

.bar {
  display: grid;
  align-items: center;
  column-gap: 14px;
  grid-template-columns: minmax(88px, 148px) 1fr minmax(56px, auto);
  padding: 6px 8px;
  /* Pulled back out by the same amount, so the hover surface extends to the
     panel's padding edge and the row reads as a full-width target rather than a
     floating strip. */
  margin: 0 -8px;
  transition: background var(--fast) var(--ease);
}

/* THE ROW IS THE TARGET, and it is a real one: `tabindex=0` in the markup, so
   the tooltip is reachable by keyboard and not only by a mouse. A chart whose
   detail exists only on hover is a chart half the readers cannot fully read. */
.bar[tabindex]:hover,
.bar[tabindex]:focus-visible {
  background: var(--s2);
  outline: none;
}

.bar[tabindex]:focus-visible { box-shadow: inset 0 0 0 1px var(--gold-ink); }

/* The bar itself brightens with the row rather than staying flat, so the eye is
   pulled to the length being read and not merely to the strip behind it. */
.bar[tabindex]:hover .bar-fill,
.bar[tabindex]:focus-visible .bar-fill { filter: saturate(1.25) brightness(1.08); }

.bar-label {
  overflow: hidden;
  color: var(--ink-2);
  font-size: 12.5px;
  letter-spacing: 0.2px;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.bar-track {
  position: relative;
  height: 17px;
  background: var(--s3);
}

.bar-fill {
  height: 100%;
  min-width: 2px;
  background: var(--gold);
  /* Grows from nothing on first paint. The stagger is applied per row inline, so
     the chart draws left to right like something being counted. */
  transform-origin: left center;
  animation: bar-grow 620ms var(--ease) both;
}

.bar-fill.alt { background: var(--blue); }
.bar-fill.quiet { background: var(--ink-4); }

@keyframes bar-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.bar-val {
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 600;
  text-align: right;
}

.bar-sub {
  display: block;
  color: var(--ink-3);
  font-size: 10.5px;
  font-weight: 400;
}

/* Two series on one row — kills against deaths — as two thin bars stacked inside
   the track's height rather than side by side. Side by side halves the length of
   both and makes the comparison harder, which is the entire question here. */
.bar-track.dual { display: grid; align-content: center; gap: 2px; height: 19px; }
.bar-track.dual .bar-fill { height: 7px; }

/* ---------------------------------------------------------------------------
   THE TOTAL ROW
   ---------------------------------------------------------------------------
   ASKED FOR EXPLICITLY, AND IT IS ALSO THE HONEST THING. Several of these
   groupings are truncated — `by_country` is LIMIT 40 — so a reader adding the
   column up by eye would get a number that disagrees with the headline. The
   total states the sum of what is SHOWN, and where that is less than the
   lifetime figure the page says so rather than letting the two numbers argue.
   --------------------------------------------------------------------------- */
.bar.total,
.dt tr.total td {
  border-top: 1px solid var(--rule-strong);
  margin-top: 6px;
  padding-top: 10px;
}

.bar.total .bar-label,
.dt tr.total td {
  color: var(--ink);
  font-weight: 600;
}

.bar.total .bar-track { background: none; }

/* ---------------------------------------------------------------------------
   DATA TABLES — where a row is genuinely several numbers
   ---------------------------------------------------------------------------
   K/D and accuracy by weapon is six columns; drawing it as six bar charts would
   scatter one question across six panels. The bar lives INSIDE the cell instead,
   as a thin rule under the figure, so the table still scans as a ranking.
   --------------------------------------------------------------------------- */
.dt-scroll {
  overflow-x: auto;
  /* The table is the one thing here that can legitimately exceed the viewport on
     a phone. It scrolls ITSELF; the page body never scrolls sideways. */
  -webkit-overflow-scrolling: touch;
}

.dt {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}

.dt th {
  padding: 0 12px 9px 0;
  border-bottom: 1px solid var(--rule-strong);
  color: var(--ink-3);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-align: right;
  text-transform: uppercase;
  white-space: nowrap;
}

.dt th:first-child,
.dt td:first-child { text-align: left; padding-right: 18px; }

.dt td {
  padding: 9px 12px 9px 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-2);
  text-align: right;
  white-space: nowrap;
}

.dt td:first-child {
  color: var(--ink);
  font-weight: 600;
  letter-spacing: 0.2px;
  text-transform: uppercase;
}

.dt tbody tr { transition: background var(--fast) var(--ease); }
.dt tbody tr:hover { background: var(--s2); }

/* The in-cell magnitude rule. 2px, under the number, never a filled cell
   background — a heat-map here would put colour on a value whose scale the
   reader has not been told. */
.cell-bar {
  display: block;
  height: 2px;
  margin-top: 5px;
  margin-left: auto;
  background: var(--gold);
}
.cell-bar.alt { background: var(--blue); }

/* ---------------------------------------------------------------------------
   COUNTRY LIST
   ---------------------------------------------------------------------------
   The flag is generated from the two-letter code as regional-indicator
   characters — no image request, no flag sprite, and it degrades to the letters
   themselves on a platform with no flag font rather than to a broken image.
   --------------------------------------------------------------------------- */
.bar-flag {
  margin-right: 8px;
  font-size: 13px;
  /* Colour emoji have their own metrics and drag the row's baseline around. */
  line-height: 1;
  vertical-align: -1px;
}

/* ---------------------------------------------------------------------------
   THE CAVEAT BLOCK
   ---------------------------------------------------------------------------
   Reuses `.notice` from site.css — the same component the fan-project notice
   uses — because the GeoIP caveat is exactly that kind of statement: something
   the reader must have in order to read the numbers correctly, not a footnote.
   --------------------------------------------------------------------------- */
.data-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  align-items: center;
  justify-content: space-between;
  margin-top: 26px;
  color: var(--ink-3);
  font-size: 11.5px;
}

/* ---------------------------------------------------------------------------
   THE SECTION NAV
   ---------------------------------------------------------------------------
   Four jumps, sticking under the site header once the masthead has scrolled
   past. The page is nine charts and ~120 rows; a reader who came for the weapon
   table should not have to scroll past the country list to find out it exists.

   STICKY, NOT FIXED, and offset by `--nav-h` — site.css already declares the
   header's height as a token, so this cannot drift from it. `fixed` would have
   needed its own copy of that number and would have covered the masthead on a
   short viewport.

   Real anchors, so every entry is a link somebody can copy, open in a new tab,
   or land on from outside. `:target` handling is the browser's own.
   --------------------------------------------------------------------------- */
/* The BAND bleeds to both edges; the LINKS inside it sit in `.wrap`, so they
   start on the same line as the masthead and every panel. */
.data-nav {
  position: sticky;
  z-index: 20;
  top: var(--nav-h);
  border-bottom: 1px solid var(--rule);
  background: color-mix(in srgb, var(--s1) 92%, transparent);
  backdrop-filter: blur(8px);
}

.data-nav-inner {
  display: flex;
  overflow-x: auto;
  gap: 2px;
  scrollbar-width: none;
}

/* The first link's TEXT lands on the content line, not its hit box.
   NOT a negative margin on `.data-nav-inner`: that is a `.wrap`, and `.wrap`
   centres itself with `margin-inline: auto`. Setting `margin-left` overrides the
   auto and the whole bar slides to the viewport edge — which is exactly what it
   did. Dropping the padding on one link leaves the centring alone. */
.data-nav-inner a:first-child { padding-left: 0; }

.data-nav-inner::-webkit-scrollbar { display: none; }

.data-nav a {
  flex: none;
  padding: 13px 14px;
  border-bottom: 2px solid transparent;
  color: var(--ink-3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.3px;
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}

.data-nav a:hover,
.data-nav a:focus-visible { color: var(--ink); border-bottom-color: var(--rule-strong); }

/* The band the reader is actually in. Set by js/player-data.js from an
   IntersectionObserver — a `:target` rule would only ever highlight a section
   somebody CLICKED to, and would go stale the moment they scrolled on. */
.data-nav a[data-current] { color: var(--ink); border-bottom-color: var(--gold); }

/* Anchored sections must not land under the two stacked sticky bars. */
.cat { scroll-margin-top: calc(var(--nav-h) + 52px); }

/* ---------------------------------------------------------------------------
   CATEGORY BANDS
   ---------------------------------------------------------------------------
   Nine panels in one undifferentiated grid is a wall. They answer four separate
   questions — how much has been played, what the fighting looks like, where it
   happens, and who is playing — so the page is banded by those, and each band
   states its question the way the dashboards in scripts/posthog-dashboards.mjs
   do. A reader who only wants the weapon numbers can find them by scanning four
   headings instead of nine.
   --------------------------------------------------------------------------- */
.cat { padding: clamp(34px, 3.6vw, 54px) 0 0; }

.cat-head {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  align-items: baseline;
  padding-bottom: 16px;
}

.cat-head h2 {
  font: 400 clamp(26px, 3.2vw, 40px)/0.98 var(--display);
  letter-spacing: -0.005em;
  text-transform: uppercase;
  /* The heading is two or three words and must never break across lines while
     there is room for it — a wrapped two-word display heading reads as a layout
     bug, not as emphasis. */
  text-wrap: balance;
}

.cat-head p {
  color: var(--ink-3);
  font-size: 12.5px;
  /* Sits on the heading's baseline while there is room and drops under it when
     there is not, rather than being squeezed into a narrow column beside it. */
  flex: 1 1 30ch;
  line-height: 1.6;
  text-wrap: pretty;
}

/* ---------------------------------------------------------------------------
   THE TOOLTIP
   ---------------------------------------------------------------------------
   ONE element for the whole page, moved and refilled — not one node per row.
   With nine charts and ~120 rows, a tooltip per row is 120 absolutely-positioned
   subtrees the browser lays out on every resize to show at most one of them.

   It follows the pointer on the axis that has room and flips before it can leave
   the viewport, so a row at the bottom of a phone screen does not open a panel
   underneath the fold. Positioned in `position: fixed` co-ordinates because the
   page scrolls under it while it is open.

   `pointer-events: none` — it must never be the thing the cursor is over, or
   moving toward a value would make the tooltip flicker as it steals its own
   trigger's hover.
   --------------------------------------------------------------------------- */
.tip {
  position: fixed;
  z-index: 60;
  top: 0;
  left: 0;
  min-width: 168px;
  max-width: min(300px, calc(100vw - 24px));
  padding: 11px 13px 12px;
  border: 1px solid var(--rule-strong);
  background: var(--s0);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.19);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--fast) var(--ease);
  /* Never mid-word, and never a one-word orphan line: the tooltip is the one
     place on this page where the text is generated and its width is not known
     in advance. */
  overflow-wrap: normal;
  text-wrap: pretty;
}

.tip[data-open] { opacity: 1; }

.tip-title {
  color: var(--ink);
  font: 400 16px/1.05 var(--display);
  letter-spacing: 0.3px;
  text-transform: uppercase;
  white-space: nowrap;
}

.tip-rows {
  display: grid;
  gap: 3px;
  margin-top: 8px;
  grid-template-columns: 1fr auto;
}

.tip-k {
  color: var(--ink-3);
  font-size: 11px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  /* A key like "KILLS PER GAME" must stay on its line — wrapping it turns a
     two-column grid into a ragged block. */
  white-space: nowrap;
}

.tip-v {
  color: var(--ink);
  font-size: 11.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* The share-of-total line, separated because it is derived rather than measured. */
.tip-foot {
  margin-top: 9px;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
  color: var(--ink-3);
  font-size: 10.5px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   COPY THAT MUST NOT WRAP BADLY
   ---------------------------------------------------------------------------
   Asked for directly. Three separate rules, because "wrapping" has three
   different causes here:
     · a display heading breaking two words across two lines  → text-wrap: balance
     · a label or figure breaking mid-token                    → white-space: nowrap
     · a paragraph leaving one word alone on the last line     → text-wrap: pretty
   --------------------------------------------------------------------------- */
.masthead h1,
.panel h2 { text-wrap: balance; }

.panel-note,
.masthead-lede { text-wrap: pretty; }

/* A number and its unit are one token to a reader and must break as one. */
.bar-val,
.bar-sub,
.live-tag,
.tally span { white-space: nowrap; }

/* ---------------------------------------------------------------------------
   THE MENU PULSE — the same indicator, on the game's own menu
   ---------------------------------------------------------------------------
   This block is loaded by /data. The MENU's copy of these rules is in style.css,
   because index.html does not load this sheet and must not: it would be a second
   render-blocking request in front of the game.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   MOTION
   ---------------------------------------------------------------------------
   Everything above that moves is a bar growing once and a ring sweeping. Both
   are suppressed outright rather than shortened — a reader who has asked for no
   motion is not asking for less of it.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .bar-fill { animation: none; }
  [data-live='ok'] .live-dot::after { animation: none; opacity: 0.5; }
  .tally b, .dt tbody tr, .bar, .tip { transition: none; }
}

/* ---------------------------------------------------------------------------
   NARROW
   ---------------------------------------------------------------------------
   Under ~560px the label column cannot hold a weapon name and leave a track
   worth looking at, so the row becomes two lines: label and value on one, the
   full-width track under it. The bars stay comparable because they still share
   their left and right edges.
   --------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .bar {
    column-gap: 10px;
    grid-template-columns: 1fr auto;
    row-gap: 5px;
  }
  .bar-label { grid-column: 1; }
  .bar-val { grid-column: 2; }
  .bar-track { grid-column: 1 / -1; }
  .panels { grid-template-columns: 1fr; }
}
