@import url(/assets/fonts/fonts.css);


:root {
  /* The one declaration that fixes the native controls.
     A <select>'s OPEN option list, the date picker, the number spinner and
     the scrollbars are painted by the browser, not by this stylesheet, and
     the browser picks light or dark from the page's declared scheme. This
     page never declared one, so Chrome assumed light: a pale popup, into
     which the site's own light-on-dark `color` inherited -- near-white text
     on near-white, which is what the tank filter on #/players looked like.
     No amount of styling the <select> itself reaches inside that popup;
     color-scheme does, and it fixes every native control at once. */
  color-scheme: dark;

  --bg: #10141c;
  --panel: #1a1e2e42;
  --panel2: #272c4142;
  --border: #3d4766;
  --border-light: #868fbf;
  --text: #d6dcf5;
  --dim: #7f89b3;
  --blue: #4f7cff;
  --violet: #a06bff;
  --acc: #00FFF2;
  --alt-acc: #d6f902;
  /* Aliases under the names a suite file is documented to use, so a suite
     never has to know what this theme happens to call things. */
  --fg: var(--text);
  --line: var(--border);
  --accent: var(--blue);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background-color: var(--bg);
  background-image:
      repeating-linear-gradient(#151a25 0 1px, transparent 1px 100%),
      repeating-linear-gradient(90deg, #151a25 0 1px, transparent 1px 100%);
  background-size: 40px 40px;
  color: var(--text);
  min-width: 320px;
}

body {
  font-family: "POE Vetica New", sans-serif;
  letter-spacing: 1px;
  line-height: 1.45;
}

.mono, .id-full, .id-short, td.num, .stat-value, table td.tank {
  font-family: ui-monospace, "Cascadia Mono", "Consolas", "SFMono-Regular", Menlo, monospace;
}

.num, td.num, .stat-value {
  font-variant-numeric: tabular-nums;
}

a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---- top nav ---- */
.topnav {
  position: sticky;
  top: 0;
  z-index: 10;
  backdrop-filter: blur(6px);
}
.topnav-inner {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0px 20px;
  display: flex;
  align-items: center;
  gap: 28px;
  flex-wrap: wrap;
}
.wordmark {
  font-weight: 800;
  font-size: 1.25rem;
  letter-spacing: 0.04em;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  white-space: nowrap;
}
.wordmark:hover { text-decoration: none; filter: brightness(1.15); }
.wordmark-icon {
  height: 2.7rem;
  transform: translateY(7px);
}
.wordmark-dim {
  color: var(--dim);
  -webkit-text-fill-color: var(--dim);
  font-weight: 600;
  margin-left: 0.35em;
}
.tabs {
  display: flex;
  gap: 0px;
  flex: 1;
  flex-wrap: wrap;
  padding: 14px 0px;
}
.tabs a {
  color: var(--dim);
  padding: 6px 12px 6px 14px;

  flex: 1;
  max-width: 150px;

  font-size: 0.92rem;
  letter-spacing: 2px;
  font-weight: 400;
  transition: color 120ms ease, background 120ms ease;

  position: relative;
}
.tabs a::before {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 15;
  height: 20%;
  border: var(--border-light) 1px solid;
  border-top: none;
  border-left: none;
}
.tabs a:first-child::before {
  border-left: var(--border-light) 1px solid;
}

.tabs a::after {
  left: 0;
  width: 0;
  content: "";
  transition: .12s ease;
}

.tabs a[hidden] { display: none; }
.tabs a:hover { color: var(--text); text-decoration: none; background: rgba(79, 124, 255, 0.08); }
.tabs a:hover::after {
  width: 100%;
  content: "";
  position: absolute;
  left: 0;
  bottom: -7px;
  height: 7px;
  background: var(--alt-acc);
}

.tabs a.active { 
  color: var(--text); 
  font-weight: 500;
  background: #ffffff;
  background: linear-gradient(0deg,rgb(from var(--text) r g b / 40%) 0%, rgba(255, 255, 255, 0) 100%);
}
.tabs a.active::after {
  width: 100%;
  content: "";
  position: absolute;
  left: 0;
  bottom: -7px;
  height: 7px;
  background: var(--text);
}

.updated {
  color: var(--dim);
  font-size: 0.8rem;
  white-space: nowrap;
}

/* The label is a link to the changelog now. Kept looking like the label it
   already was -- underlined on hover only -- because a blue link in the middle
   of the header reads as another nav item, which it is not. */
.updated a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted var(--border);
}
.updated a:hover { color: var(--text); border-bottom-color: var(--border-light); }

/* ---- changelog (#/changelog) ---- */
.chg-list { list-style: none; margin: 0; padding: 0; }
.chg-item {
  display: flex;
  gap: 14px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.chg-item:first-child { border-top: none; padding-top: 0; }
/* flex: none, not a min-width. The body's 1px letter-spacing means an ISO
   date is wider than 10ch of the mono font, so any ch-based guess still let
   the flex line shrink it and the title overprinted the last digits. */
.chg-date {
  color: var(--dim);
  font-size: 0.78rem;
  white-space: nowrap;
  padding-top: 2px;
  flex: none;
}
.chg-text p { margin: 4px 0 0; }

/* The undeployed entries. Closed by default and stating what they are in the
   summary itself, so the distinction cannot be missed by someone who only
   skims -- these describe pages that are not on the site. */
.chg-soon { margin-top: 22px; border-top: 1px solid var(--border); padding-top: 14px; }
.chg-soon > summary {
  cursor: pointer;
  color: var(--dim);
  font-size: 0.85rem;
}
.chg-soon > summary:hover { color: var(--text); }
.chg-soon .chg-list { margin-top: 6px; opacity: 0.75; }

@media (max-width: 560px) {
  .chg-item { flex-direction: column; gap: 2px; }
}

/* ---- auth widget (top-right sign-in link / settings button) ---- */
.auth-widget {
  position: relative;
  display: flex;
  align-items: center;
  font-size: 0.8rem;
  color: var(--dim);
  white-space: nowrap;
}
.auth-widget a { color: var(--blue); }

.settings-btn {
  background: var(--panel2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 5px 10px;
  font-size: 0.8rem;
  cursor: pointer;
}
.settings-btn:hover { border-color: var(--blue); }

.settings-panel {
  /* .auth-widget sets white-space: nowrap to keep the sign-in row on one
     line. The panel inherits it, which killed wrapping on every label and
     paragraph in here: overflow-wrap does nothing against nowrap, so the
     content ran to 780px inside a 378px box and overflow:hidden cut it off. */
  white-space: normal;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 20;
  width: min(380px, calc(100vw - 32px));
  max-width: 380px;
  box-sizing: border-box;
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 14px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  /* Wrapping the labels made this tall enough to run off the bottom of a
     laptop screen, which put Sign out past the edge with no way to reach it. */
  max-height: calc(100vh - 70px);
  overflow-x: hidden;
  overflow-y: auto;
}
.settings-signedin { font-weight: 700; color: var(--text); margin-bottom: 10px; }
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.settings-select {
  background: var(--panel2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 3px 8px;
  font-size: 0.8rem;
  max-width: 100%;
}
.settings-group { margin-top: 10px; }
.settings-group-title {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--dim);
  margin-bottom: 4px;
}
.settings-check-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 8px;
}
.settings-check {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.8rem;
  color: var(--text);
  padding: 3px 0;
  cursor: pointer;
  min-width: 0;
}
.settings-check span, .settings-check { overflow-wrap: anywhere; }
.settings-check input:disabled { cursor: not-allowed; }
.settings-check-item { display: flex; flex-direction: column; margin: 2px 0 6px; min-width: 0; }
.settings-check-label { font-size: 0.8rem; color: var(--text); padding: 3px 0; overflow-wrap: anywhere; }
.settings-subcheck { font-size: 0.68rem; color: var(--dim); padding: 1px 0 1px 2px; gap: 5px; }
.settings-subcheck input { width: 12px; height: 12px; }
/* The per-tank / per-map breakdown is a LEVEL, not a switch, so its row
   carries two <select>s where the others carry two checkboxes. Two selects do
   not fit in one 180px half of the panel's two-column grid, so this row takes
   the full width and puts each select on the right of its own scope label. */
.settings-level-item { grid-column: 1 / -1; }
.settings-level-item .settings-subcheck {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}
.settings-level-select { font-size: 0.68rem; padding: 1px 4px; flex: 0 0 auto; }

/* ---- layout ---- */
main#app {
  max-width: 1400px;
  margin: 0 auto;
  min-height: 100vh;
  padding: 24px 20px 64px;
}

.site-footer {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0 20px 32px;
  font-size: 0.8rem;
  display: flex;
  justify-content: space-between;
}
.site-footer a { color: var(--dim); }
.site-footer a:hover { color: var(--blue); }
.site-footer p {
  margin: 0;
  color: var(--dim);
}
/* The footer is space-between over two children; the disclaimer is a third and
   takes the middle. It is allowed to shrink but never to vanish -- it is the
   one line here that has to be legible, so it keeps its own colour rather than
   inheriting the dimmer one the credits use, and it wraps rather than
   truncating. gap keeps it off the credits once all three are on one row. */
.site-footer { gap: 24px; }
.site-unofficial {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 60ch;
  text-align: center;
  color: var(--text-dim, var(--dim));
  opacity: 0.85;
}
@media (max-width: 780px) {
  .site-footer { flex-wrap: wrap; justify-content: center; text-align: center; }
  .site-unofficial { order: 3; flex-basis: 100%; text-align: center; }
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 18px 20px;
  margin-bottom: 20px;
}

h1, h2, h3 { margin: 0 0 14px; }
h1 {
  font-size: 1.5rem;
  background-clip: text;
  color: var(--text);
  letter-spacing: 2px;
  display: inline-block;
  border-bottom: 2px var(--border) solid;
  position: relative;
  margin-left: 47px;
}
h1::before {
  position: absolute;
  content: "";
  left: -47px;
  width: 37px;
  height: 37px;
  background-color: var(--border);
}

h2 {
  font-size: 1.05rem;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  position: relative;
}

.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}

.page-head h1 {
  flex: 1;
}

/* Sideways route out of a page, for features that deliberately have no nav
   entry (Parties). Sits at the end of the page-head's flex row. */
.head-link {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--dim);
  white-space: nowrap;
}
.head-link:hover { color: var(--text); }

h1.map::before,h1.skull::before,h1.sword::before,h1.tank::before,h1.star::before,h1.search::before,h1.upload::before {
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;

  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
}
h1.map::before {
  -webkit-mask-image: url("/assets/icons/map.svg");
  mask-image: url("/assets/icons/map.svg");
}
h1.skull::before {
  -webkit-mask-image: url("/assets/icons/skull.svg");
  mask-image: url("/assets/icons/skull.svg");
}
h1.sword::before {
  -webkit-mask-image: url("/assets/icons/sword.svg");
  mask-image: url("/assets/icons/sword.svg");
}
h1.tank::before {
  -webkit-mask-image: url("/assets/icons/tank.svg");
  mask-image: url("/assets/icons/tank.svg");
}
h1.star::before {
  -webkit-mask-image: url("/assets/icons/star.svg");
  mask-image: url("/assets/icons/star.svg");
}
h1.search::before {
  -webkit-mask-image: url("/assets/icons/search.svg");
  mask-image: url("/assets/icons/search.svg");
}
h1.upload::before {
  -webkit-mask-image: url("/assets/icons/upload.svg");
  mask-image: url("/assets/icons/upload.svg");
}



.empty-state {
  text-align: center;
  padding: 64px 24px;
  color: var(--dim);
}
.empty-state .big { font-size: 2rem; margin-bottom: 8px; }
.empty-state .sub { font-size: 0.95rem; }
#retry-load {
  background-color: var(--panel2);
  border: 1px var(--border) solid;
  color: var(--text);
  font-size: 14px;
  padding: 10px 60px;

  transition: transform 120ms ease, border 120ms ease;
}
#retry-load:hover {
  transform: translateY(-3px);
  border-color: var(--acc);
}

/* ---- search ---- */
.search-box {
  background: var(--panel2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 8px 12px;
  font-size: 0.9rem;
  min-width: 220px;
  transition: border-color 120ms ease;
}
.search-box:focus { outline: none; border-color: var(--blue); }
.search-box::placeholder { color: var(--dim); }
/* Belt to color-scheme's brace. Some engines honour these inside the option
   list and some ignore them entirely; where they are honoured the list
   matches the panels rather than merely being dark. */
select.search-box option,
select option {
  background-color: #1b2030;
  color: var(--text);
}


/* ---- players online ---- */
a.steam-panel { display: block; color: inherit; text-decoration: none; }
a.steam-panel:hover { border-color: var(--blue); text-decoration: none; }

.online-chart { width: 100%; height: auto; }

.ol-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-bottom: 12px;
  color: var(--dim);
}
.ol-key { display: inline-flex; align-items: center; gap: 6px; }
.ol-sw {
  width: 14px;
  height: 10px;
  border: 1px solid transparent;
  display: inline-block;
}
.ol-sw-line {
  height: 0;
  width: 16px;
  border: 0;
  border-top: 2px dashed #c98b3a;
  border-radius: 0;
}

/* ---- matches: controls + card grid ----------------------------------
   The controls wrap onto as many rows as they need. The search box is the
   one that grows, because it is the one people type long things into. */
.m-controls {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 16px;
}
.m-controls .search-box { min-width: 150px; }
.m-search { flex: 1 1 260px; min-width: 220px; }
.m-date {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  color: var(--dim);
}
.m-date .search-box { min-width: 0; padding: 7px 10px; }
.m-count { color: var(--dim); font-size: 0.85rem; }

.match-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: 12px;
}

.match-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  background: var(--panel);
  border: 1px solid var(--border);
  color: var(--text);
  text-decoration: none;
  transition: border-color 120ms ease, transform 120ms ease;
}
a.match-card:hover {
  border-color: var(--accent);
  transform: translateY(-5px);
  text-decoration: none;
}

.mc-top { display: flex; align-items: center; gap: 10px; }
.mc-thumb {
  width: 44px;
  height: 44px;
  object-fit: cover;
  flex: 0 0 auto;
}
.mc-head { min-width: 0; flex: 1 1 auto; }
.mc-map {
  font-weight: 600;
  font-size: 0.95rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mc-when { color: var(--dim); font-size: 0.75rem; margin-top: 2px; }
.mc-type { flex: 0 0 auto; align-self: self-start; }

.mc-mid {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.mc-score { font-variant-numeric: tabular-nums; font-size: 0.9rem; }

.mc-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  padding-top: 9px;
  border-top: 1px solid var(--border);
  color: var(--dim);
  font-size: 0.74rem;
}
/* The credit gets its own line on EVERY card, rather than sharing the meta
   row and pushing right. Sharing meant a long credit wrapped and a short one
   did not, so cards came out 144px or 167px depending on how long somebody's
   name happened to be -- "from [RNGR DNGR] Rodee543" and "recorded by
   legomaster188" wrapped, "from WarMechanic" did not, and the grid went
   ragged. 52 of 192 cards were the tall kind.

   Forcing the row to nowrap instead did equalise the height, but overflowed
   44 cards horizontally at desktop width: text spilling out of a card is
   worse than an uneven grid. So the credit takes a full-width line and every
   card is the same height, at the cost of ~23px each.

   A merged match credits every uploader, so this line can now hold two names
   instead of one. Two is the cap the card face renders (a third becomes "+N",
   see creditNames) but two long ones still would have wrapped and put those
   cards 23px taller than the rest, so the line clips with an ellipsis instead.
   Nothing is lost by the clip: the title on this span always spells out the
   complete list, uploaders and recorders both. */
.mc-up {
  flex-basis: 100%;
  margin-left: 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mc-more {
  grid-column: 1 / -1;
  padding: 14px;
  text-align: center;
  color: var(--dim);
  font-size: 0.85rem;
}

/* ---- tables ---- */
.table-scroll {
  overflow-x: auto;
}
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  min-width: 560px;
}
thead th {
  text-align: left;
  color: var(--dim);
  font-weight: 600;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 8px 12px;
  border-bottom: 2px solid transparent;
  border-image: var(--acc) 1;
  white-space: nowrap;
}
/* The Tanks table is 15 columns. At 12px of side padding it needs about
   1290px, so on a 1280 laptop it ran past the panel and the last column got
   cut. Tighten the gutters below the full shell width instead of letting it
   clip. */
@media (max-width: 1420px) {
  #tanks-table thead th,
  #tanks-table tbody td { padding-left: 7px; padding-right: 7px; }
}

thead th.sortable { cursor: pointer; user-select: none; }
thead th.sortable:hover { color: var(--text); }
thead th .arrow { margin-left: 4px; opacity: 0.7; }
tbody td {
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
tbody tr { transition: background 120ms ease; }
tbody tr:hover { background: rgba(79, 124, 255, 0.08); }
tbody tr:last-child td { border-bottom: none; }
td.rank { color: var(--dim); width: 1%; }
td.tank { color: var(--violet); }

/* ---- chips ---- */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  font-size: 0.8rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  border: 1px solid transparent;
}
.chip-gray { background: rgba(127, 137, 179, 0.16); color: var(--dim); border-color: rgba(127, 137, 179, 0.3); }
.chip-victory { background: rgba(79, 124, 255, 0.16); color: #8fb0ff; border-color: rgba(79, 124, 255, 0.4); }
.chip-defeat { background: rgba(224, 90, 130, 0.16); color: #ff9db8; border-color: rgba(224, 90, 130, 0.4); }
.chip-custom { background: rgba(160, 107, 255, 0.16); color: #cbaaff; border-color: rgba(160, 107, 255, 0.4); font-size: 0.7rem; padding: 2px 8px; }
.chip-standard { background: rgba(79, 124, 255, 0.10); color: #8fb0ff; border-color: rgba(79, 124, 255, 0.25); font-size: 0.7rem; padding: 2px 8px; }
.chip-capture { background: rgba(160, 107, 255, 0.16); color: #cbaaff; border-color: rgba(160, 107, 255, 0.4); }

/* ---- tank / map icons ---- */
.tank-icon {
  width: 18px;
  height: 18px;
  object-fit: cover;
  vertical-align: middle;
  flex: none;
}
.tank-cell { display: inline-flex; align-items: center; gap: 6px; }
.map-thumb {
  width: 26px;
  height: 18px;
  object-fit: cover;
  vertical-align: middle;
  margin-right: 8px;
  flex: none;
}

/* ---- popular tanks strip ---- */
.strip-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--dim);
  margin: 0 0 10px;
}
.tank-strip {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding-bottom: 10px;
  margin-bottom: 20px;
}
/* themed scrollbar for any horizontally-scrolling strip (tank cards, wide
   tables) -- the default OS scrollbar is light/grey and clashes hard with
   this dark theme. Firefox + WebKit/Blink variants. */
.tank-strip, .table-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.tank-strip::-webkit-scrollbar, .table-scroll::-webkit-scrollbar {
  height: 8px;
  width: 8px;
}
.tank-strip::-webkit-scrollbar-track, .table-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.tank-strip::-webkit-scrollbar-thumb, .table-scroll::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 8px;
}
.tank-strip::-webkit-scrollbar-thumb:hover, .table-scroll::-webkit-scrollbar-thumb:hover {
  background: var(--blue);
}
.tank-card {
  flex: none;
  width: 140px;
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 12px;
  color: var(--text);
  transition: border-color 120ms ease, transform 120ms ease;
}
.tank-card:hover { border-color: var(--blue); text-decoration: none; transform: translateY(-2px); }
.tank-card-img, .tank-card-img-empty {
  width: 100%;
  height: 72px;
  object-fit: cover;
  background: var(--panel2);
  margin-bottom: 8px;
  display: block;
}
.tank-card-name { font-weight: 700; font-size: 0.9rem; margin-bottom: 4px; }
.tank-card-meta { color: var(--dim); font-size: 0.76rem; }

/* ---- maps section (index grid + per-map page) ---- */
.maps-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 14px;
}
.map-card {
  background-color: var(--panel);
  border: 1px solid var(--border);
  padding: 12px;
  color: var(--text);
  transition: border-color 120ms ease, transform 120ms ease, background-color 120ms ease;
}
.map-card:hover { 
  border-color: var(--acc); 
  text-decoration: none; 
  transform: translateY(-5px); 
  background-color: rgb(from var(--acc) r g b / 15%);
}
.map-card-img {
  width: 100%;
  height: 120px;
  object-fit: cover;
  background: var(--panel2);
  margin-bottom: 10px;
  display: block;
}
.map-card-name { font-weight: 700; font-size: 1rem; margin-bottom: 4px; }
.map-card-meta { color: var(--dim); font-size: 0.78rem; }

/* ---- steam player chart ---- */
.steam-panel { padding: 16px 20px; }
.steam-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.steam-latest {
  font-size: 2rem;
  line-height: 2rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: var(--acc);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.steam-chart { width: 100%; height: 60px; display: block; }

/* ---- past names ---- */
.past-names { 
  color: var(--dim); 
  margin-top: -18px;
  margin-bottom: 18px;
}

/* ---- per-tank page header ---- */
.tank-stats {
  display: flex;
  flex-direction: row;
  gap: 12px;
}

.tank-head {
  background: rgb(from var(--panel) r g b / 20%);
  border: 1px solid var(--border);
  margin-bottom: 20px;
  position: relative;
  flex: 2;
}

@media (width < 1200px) {
  .tank-stats {
    flex-direction: column;
  }
  .tank-head {
    margin-bottom: 0px;
  }
}

.tank-head .player-label {
  position: absolute;
  font-size: 40px;
  right: 40px;
  /* Absolutely positioned over the whole hero, so it swallowed clicks meant
     for anything underneath -- which is how the 3D viewer's own button became
     unpressable the moment the viewer moved into this box. It is a label; it
     was never meant to be a click target. */
  pointer-events: none;
}

.tank-head-img {
  width: 100%;
  object-fit:contain;
  margin-bottom: 10px;
  display: block;
}

/* ---- stat cards ---- */
.stat-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 20px;
}
.stat-card {
  background: var(--panel2);
  border: 1px solid var(--border);
  min-width: 150px;
  flex: 1;
  padding: 14px 16px;
}
.stat-card .label {
  color: var(--dim);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 6px;
}
.stat-card .stat-value {
  font-size: 1.4rem;
  font-weight: 700;
}

/* ---- player header ---- */
.player-head {
  padding: 22px 22px 18px 47px;
  display: flex;
  gap: 20px;
  /* .page-head has always wrapped; this row did not, so on a phone its
     trailing controls were clipped off the right edge rather than moving to
     a second line. Nothing changes above ~700px, where the h1's flex:1
     absorbs the slack and there is no wrapping to do. */
  flex-wrap: wrap;
}
.player-label {
  font-size: 1.8rem;
  font-weight: 800;
  margin: 0 0 4px;
  flex: 1;
}
.id-copy {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--panel2);
  border: 1px solid var(--border);
  padding: 5px 10px;
  cursor: pointer;
  color: var(--dim);
  font-size: 0.78rem;
  transition: border-color 120ms ease, color 120ms ease;
}
.id-copy:hover { border-color: var(--blue); color: var(--text); }
.id-copy .copied { color: var(--blue); }

/* ---- matches ---- */
.match-card {
  background: var(--panel);
  border: 1px solid var(--border);
  overflow: hidden;
}
.match-card summary {
  list-style: none;
  cursor: pointer;
  padding: 14px 18px;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.match-card summary::-webkit-details-marker { display: none; }
.match-card summary .datetime { color: var(--dim); font-size: 0.85rem; min-width: 150px; }
.match-card summary .map { font-weight: 700; flex: 1; }
.match-card summary .score { font-variant-numeric: tabular-nums; color: var(--text); font-weight: 700; }
.match-card summary .caret { color: var(--dim); transition: transform 120ms ease; margin-left: auto; }
.match-card[open] summary .caret { transform: rotate(90deg); }
.match-card .roster-wrap { padding: 0 18px 16px; }
/* fixed layout so the Team A and Team B tables' columns always align with each other */
.roster-table { table-layout: fixed; width: 100%; min-width: 620px; }
.roster-table .c-player { width: 26%; }
.roster-table .c-tank { width: 14%; }
/* One c-player + one c-tank + SIX c-stat. At 12% that summed to 112% and
   fixed layout squeezed the columns back unevenly, which is the thing the
   shared widths exist to prevent. */
.roster-table .c-stat { width: 10%; }
.roster-table td, .roster-table th { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-card h4 {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--dim);
  margin: 14px 0 6px;
}
/* matches list rows + full match page */
.match-row td { vertical-align: middle; }
.match-link { font-weight: 700; }
.roster-wrap {
  padding: 5px 0px 30px 0px;
}
.roster-wrap h4 {
  font-size: 1.5rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text);
  margin: 25px 0 6px;
}
.map-head {
  display: flex;
  flex-direction: row;
  gap: 12px;
}
.map-head > * {
  flex: 1;
}
.match-head {
  background: none;
  border: none;
  padding: 0;
}
@media (width < 1050px) {
  .map-head {
    flex-direction: column;
  }
  .match-head {
    margin: 0;
  }
}
.match-head-row { 
  display: flex; 
  align-items: center; 
  gap: 18px; 
  flex-wrap: wrap; 
  position: relative;
}
/* The map's splash art, cropped to a banner. Unbounded it renders at its own
   aspect ratio -- 680 px tall on a desktop -- so an entire screen of scenery
   came before anything a reader came for, and the map's own controls sat
   1,500 px further down. A banner still says which map this was. */
.match-head-map {
  width: 100%;
  object-fit: cover;
  object-position: center 40%;
  border: 1px solid var(--border);
}
.match-head-facts {
  position: absolute;
  top: 1px;
  left: 1px;
  padding: 10px 14px;
  backdrop-filter: blur(6px);
  background-color: #00000067;
}
.match-head-facts .score { 
  font-variant-numeric: tabular-nums; 
  font-weight: 700; 

  display: flex;
  justify-content: space-between;
  gap: 10px;
}
.map-page-stat {
  height: min-content;
}
.replay-dl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: var(--panel2);
  border: 1px solid var(--border);
  padding: 6px 12px;
  width: 100%;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text);
  transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
}
.replay-dl:hover { 
  border-color: var(--blue); 
  background: rgba(79, 124, 255, 0.1); 
  text-decoration: none;
  transform: translateY(-5px);
}

tr.side-a { background: rgba(34, 197, 94, 0.07); }
tr.side-b { background: rgba(239, 68, 68, 0.07); }
tr.side-a:hover { background: rgba(34, 197, 94, 0.15); }
tr.side-b:hover { background: rgba(239, 68, 68, 0.15); }

/* ---- match history rows on player page ---- */
.history-row .tank { color: var(--violet); font-weight: 600; }
.clickable-row { cursor: pointer; }

/* ---- map heat panel (per-match + per-map aggregate) ---- */
.map-panel { padding: 14px; }
.map-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.orient-btn {
  background: var(--panel2); color: var(--text); border: 1px solid var(--border);
  padding: 6px 12px; font-size: 0.85rem; font-weight: 600; cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.orient-btn:hover { border-color: var(--blue); background: rgba(79, 124, 255, 0.1); }
.orient-btn:active { transform: translateY(1px); }
/* map art sits UNDER a transparent tactical canvas (square top-down minimap) */
.map-stage {
  position: relative; width: 100%; max-width: 820px; margin: 0 auto;
  overflow: hidden; background: var(--panel2);
}
.mapbg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0.8; filter: saturate(0.95) brightness(0.95);
}
/* Scorch had a rule here insetting its background art to 98% and offsetting it
   by 1%, because the art "read a bit oversized next to its heat overlay". The
   image and #tacmap share one coordinate space, so that put the picture 18 px
   smaller and 9 px off from every heat, shot and kill point drawn on top of it
   -- a 2% error, an order of magnitude worse than anything in
   map_calibration.json, and cosmetic cover for a calibration bug that was
   fixed since (82000 -> 80000). Scorch's navmesh and LevelBounds now agree
   exactly at 80000. Do not re-add it: any fit done with this rule in place
   would be fitting the calibration to compensate for the CSS. */
#tacmap { position: relative; width: 100%; display: block; background: transparent; cursor: crosshair; }

/* ---- drag a box on the map to zoom into it (wireMapZoom in app.js) ----

   The band is a div OVER the canvas rather than a rectangle drawn into it: a
   13,000-route overlay cannot be repainted on every pointermove, and it does
   not need to be, because nothing underneath the box changes while the box is
   being drawn. .map-stage already clips (overflow: hidden), which is also what
   holds the zoomed .mapbg inside the square. */
.map-band {
  position: absolute; z-index: 3;
  border: 1px solid var(--blue);
  background: rgba(79, 124, 255, 0.14);
  pointer-events: none;   /* the drag is captured on the canvas underneath */
}
/* The way back out. It only exists while there is something to go back FROM,
   so a reader who has zoomed can always see how to undo it without having to
   guess that double-click works. */
.map-zoombtn {
  position: absolute; top: 8px; right: 8px; z-index: 4;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border-light);
  padding: 5px 10px; font: inherit;
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
  cursor: pointer;
}
.map-zoombtn:hover { border-color: var(--blue); background: rgba(79, 124, 255, 0.16); }
.maplegend { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.mchip {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 0.78rem; font-weight: 600; color: var(--dim);
  background: transparent; /* .mchip is a <button> -- without this the
    native UA gray button chrome shows through wherever no other rule
    sets a background (e.g. the unselected state below) */
  border: 1px solid var(--border); padding: 5px 11px;
  cursor: pointer; user-select: none;
  transition: color 120ms ease, border-color 120ms ease, opacity 120ms ease, background 120ms ease;
}
.mchip:hover { color: var(--text); border-color: var(--blue); }
.mchip.off { opacity: 0.4; }
/* TWO DIFFERENT FACTS, SO TWO DIFFERENT TREATMENTS.
   .off means "this layer is switched off" -- a state the reader chose and can
   undo by clicking. .mchip-idle means "this control cannot act right now",
   which no click will change: Tanks and Names draw at a MOMENT, so they can
   do nothing while the transport says "Whole match". A dashed border reads as
   unavailable rather than merely dimmed, and the chip keeps a title naming
   what would wake it. Collapsing these two into one opacity would tell the
   reader their click failed when in fact it was never offered. */
.mchip-idle {
  opacity: 0.3; cursor: default; border-style: dashed;
}
.mchip-idle:hover { color: var(--dim); border-color: var(--border); }
/* Best Battles category tabs: a single-select group (unlike the map
   legend's independent toggles above), so it gets its own color pair
   instead of relying on the dim/opacity distinction -- blue = selectable,
   violet = the one currently selected. */
#bb-tabs .mchip { 
  color: var(--blue); 
  border-color: var(--blue); 
  opacity: 1; 
  font-size: 0.9rem; 
  flex: 1; 

  transition: transform 120ms ease, border 120ms ease;
}
#bb-tabs .mchip:hover { color: var(--text); transform: translateY(-2px); border-color: var(--acc); }
#bb-tabs .mchip.off { opacity: 0.7; }
#bb-tabs .mchip:not(.off) { color: var(--violet); border-color: var(--violet); background: rgba(160, 107, 255, 0.18); }
.mdot { width: 9px; height: 9px; border-radius: 2px; }
/* Every swatch here is the colour that layer ACTUALLY draws on the canvas --
   a key that does not match the frame is worse than no key.

   These rules went missing: an unterminated comment ("kill segments run from
   the shooter to the tank that died, so the chip is a") swallowed the block,
   so every .mdot rendered as a 9px transparent square and the legend chips
   had no swatches at all. Keep the comment above closed. */
.mdot-heat { background: linear-gradient(90deg, rgb(34, 197, 94) 50%, rgb(239, 68, 68) 50%); }
/* The Shots layer draws three nested density contours, so its chip is three
   violet rules rather than a single hue or a block of scale -- a chip whose
   colour is not in the picture is a small lie about which layer it turns on.
   The three stops are SHOT_LINES (see mapShots in app.js), in the same
   outermost-to-innermost order the canvas draws them. */
.mdot-shots {
  background:
    linear-gradient(180deg, transparent 0 10%, rgb(140, 102, 214) 10% 32%,
      transparent 32% 40%, rgb(186, 142, 250) 40% 62%,
      transparent 62% 70%, rgb(244, 236, 255) 70% 92%, transparent 92%);
}
/* kill segments run from the shooter to the tank that died, so the swatch is
   a line rather than a block */
.mdot-kills { background: var(--dim); height: 3px; border-radius: 999px; }
/* live tank markers: a filled dot with a dark ring, same as the canvas */
.mdot-tanks { border-radius: 999px; background: #e9edff; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.85) inset; }
/* names are a label on that marker, not a layer of their own colour */
.mdot-names { border-radius: 999px; background: transparent; border: 1.5px solid #e9edff; }
/* Damages draws two glowing discs, green where a hit was dealt from and red
   where it landed, so the chip is those two hues as discs rather than as the
   split square Heat uses -- the two layers mean different things by the same
   pair of colours and the chips should not look interchangeable. */
.mdot-damage {
  border-radius: 999px;
  background:
    radial-gradient(circle at 30% 50%, rgb(52, 211, 120) 0 34%, transparent 36%),
    radial-gradient(circle at 72% 50%, rgb(244, 78, 78) 0 34%, transparent 36%);
}
/* The two halves of that layer, now that each is its own chip: one disc each,
   in the colour the canvas actually stamps. Green is where a hit was dealt
   FROM, red is where it landed -- the same pair the key names, so a reader can
   match chip to mark without a legend. */
.mdot-dmgDealt { border-radius: 999px; background: rgb(52, 211, 120); }
.mdot-dmgTaken { border-radius: 999px; background: rgb(244, 78, 78); }
/* Tracers are lines coloured by the dealer's tank class: amber Light, blue
   Medium, violet Heavy, in the order they appear in the key. */
.mdot-tracers {
  border-radius: 0;
  background:
    linear-gradient(180deg, transparent 0 12%, rgb(240, 190, 94) 12% 34%,
      transparent 34% 40%, rgb(79, 124, 255) 40% 62%,
      transparent 62% 68%, rgb(160, 107, 255) 68% 90%, transparent 90%);
}
/* team key pills in the caption -- Team A (green) / Team B (red), a
   stable team-id labelling, not relative to who recorded the replay */
.tkey { font-weight: 700; border-radius: 0px; border: none; padding: 1px 8px; font-size: 0.74rem; margin-right: 2px; }
.tkey-o { color: #ffbf6b; background: rgba(255, 165, 0, 0.14); border: 1px solid rgba(255, 165, 0, 0.4); }
/* Team A / Team B pills are <button> heat toggles (see teamChip in app.js).
   .tkey-o is a state indicator, not a button, so it stays a <span> and
   only ever gets .off driven by the two real toggles. */
button.tkey { font: inherit; font-weight: 700; font-size: 0.74rem; cursor: pointer; }
button.tkey:hover { filter: brightness(1.3); }
button.tkey:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.tkey.off { opacity: 0.35; text-decoration: line-through; }
/* Same distinction as .mchip-idle above: struck through means "this team is
   hidden", faded-without-the-strike means "there is no Heat layer on screen
   for this pill to hide". The pill is still the caption's colour key while it
   is idle, which is why it is dimmed rather than removed. Written
   .tkey.tkey-idle so it outranks .tkey.off: a pill can carry both classes
   (a hidden team, then Heat switched off), and "cannot act" is the fact that
   matters there -- with no Heat drawn, nothing is being hidden to strike out. */
.tkey.tkey-idle { opacity: 0.3; cursor: default; text-decoration: none; }
.tkey.tkey-idle:hover { filter: none; }

.spark { display: block; }
/* ---- footer misc ---- */
.small { font-size: 0.8rem; color: var(--dim); }
.not-found { text-align: center; padding: 64px 24px; }

@media (max-width: 560px) {
  .topnav-inner { gap: 12px; padding: 12px 14px; }
  main#app { padding: 16px 12px 48px; }
  .player-label { font-size: 1.4rem; }
  .match-card summary { gap: 8px; }
  .match-card summary .datetime { min-width: 0; }
  .tank-card { width: 116px; }
}

.stats-grid .panel h2 { margin-top: 0; }
.chart-svg { width: 100%; height: auto; display: block; }
.chart-axis-label { fill: var(--dim); font-size: 10px; font-family: inherit; }
.chart-legend { display: flex; flex-wrap: wrap; gap: 12px 16px; margin: 6px 0 10px; }
.chart-legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: 0.8rem; color: var(--dim); }
.chart-legend-dot { width: 10px; height: 10px; border-radius: 3px; display: inline-block; flex-shrink: 0; }

/* keystone/build share bars on the tank page (see keystonePanel in app.js) */
.keystone-row {
  display: grid; grid-template-columns: 110px 1fr 56px 52px;
  align-items: center; gap: 10px; margin-bottom: 7px;
}
.keystone-name { font-weight: 600; font-size: 0.88rem; }
.keystone-track { background: var(--panel2); border-radius: 999px; height: 10px; overflow: hidden; }
.keystone-fill { height: 100%; border-radius: 999px; transition: width 200ms ease; }
.keystone-pct { font-weight: 700; font-size: 0.85rem; text-align: right; }
.keystone-games { text-align: right; }
@media (max-width: 560px) {
  .keystone-row { grid-template-columns: 84px 1fr 48px; }
  .keystone-games { display: none; }
}

/* ---- map controls: one chip row, one slider row -------------------------
   (see mapPanel / replayControls / styleControls / mapSlidersHtml in app.js)

   This was four wrapped rows -- layers + brightness, transport, six selects,
   team key -- and the complaint was never that there were too few knobs, it
   was the scrolling. Measured under the canvas, panel bottom minus stage
   bottom: 1400px went 166px -> 120px, and 420px went 315px -> 182px on a map
   page and 218px on a match page (which carries two more layer chips).

   Two rules keep it that way:
     - a popover's body is ABSOLUTE, so opening one never makes the panel a
       pixel taller;
     - the only things that stay in the row are the ones a reader needs to READ
       rather than reach for: the live route count, and the map art. */

/* The play chip never hides. The whole transport once lived in a collapsed
   <details> labelled "Replay movement" and nobody found it, so the one control
   that IS the point of a battle map stays a plain chip. */
.play-chip { min-width: 40px; justify-content: center; font-size: 0.9rem; line-height: 1; }

/* A growing .mchip-gap used to push the settings popovers to the far end of
   this row so they did not read as more layers. Both popovers are gone -- see
   "WHAT IS NOT IN THIS BAR" in app.js -- and with them the reason for it. */

/* Disclosure popovers in the legend row. <details> rather than a hand-rolled
   listbox: it already opens on Enter and Space, moves focus nowhere it should
   not, and traps nothing -- only Escape and outside-click needed wiring (see
   wireMapPops). The body is positioned against .maplegend, not against the
   summary, so it can never hang off the side of a phone. */
.maplegend { position: relative; }
.mpop { display: inline-flex; }
/* A summary carries three or four things (name, value, caret) where a plain
   chip carries two, so it runs a tighter gap -- at .mchip's 7px the Replay and
   Style chips stop sharing a line on a 420px phone. */
.mpop > summary { list-style: none; gap: 5px; }
.mpop > summary::-webkit-details-marker { display: none; }
.mpop > summary::after {
  content: "\25be"; font-size: 0.7rem; color: var(--dim);
  transition: transform 120ms ease;
}
.mpop[open] > summary { color: var(--text); border-color: var(--blue); }
.mpop[open] > summary::after { transform: rotate(180deg); }
/* The value the summary carries so it can be read without opening: playback
   speed, and the heat mode. */
.mpop-val { color: var(--text); font-variant-numeric: tabular-nums; }
.mpop-body {
  position: absolute; z-index: 6; top: calc(100% + 6px); left: 0;
  width: min(100%, 360px);
  display: flex; flex-direction: column; gap: 10px;
  /* --panel is semi-transparent; a popover has to be opaque or the chart
     underneath reads straight through the controls. */
  background: var(--bg); border: 1px solid var(--border);
  padding: 12px 14px;
  box-shadow: 0 10px 28px rgba(8, 10, 16, 0.6);
}

/* One labelled range with a live readout -- the same shape the 3D panel uses
   for its Ground / Detail / Routes sliders (.m3d-ctrl in
   pages/map3d-embed.css). Two views of the same map should not have two
   slider idioms. */
.mctl { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.mctl-name { color: var(--dim); white-space: nowrap; }
.mctl input[type="range"] {
  flex: 1; min-width: 70px; accent-color: var(--blue); cursor: pointer;
}
.mctl input[type="range"]:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
/* The readout is the number, so it takes the text colour rather than the dim
   one, and a fixed-ish width so dragging does not shuffle the row. */
.mctl-val {
  color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap;
  min-width: 4.2em; text-align: right; font-weight: 600;
}
.mctl-tight { flex: 0 0 auto; }
/* .mctl-group grouped the layer and filter blocks inside the Layers popover.
   Both are gone -- see "WHAT IS NOT IN THIS BAR" in app.js -- and the only
   .mctl left on this page is the replay transport, which is three rows. */
.mctl-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.mctl select {
  background: var(--panel2); color: var(--text); border: 1px solid var(--border);
  padding: 4px 8px; font-size: 0.8rem; cursor: pointer;
}
.mctl select:focus { outline: none; border-color: var(--blue); }

/* The clock is the one label that changes every frame, so it is fixed-width
   and tabular -- otherwise the chip jitters as the digits change.
   Sized to hold the longest clock this can print ("10:29 / ~5:22") and no
   more: ten pixels wider and the Replay and Layers chips stop sharing a line
   on a 420px phone.

   THIS COMMENT USED TO END EARLY. A stray close-comment sequence sat after
   "digits change.", and the three lines under it were left as bare text,
   which the parser ate along with the selector below -- so .phase-label was
   written down here and doing nothing, and the clock jittered by a few pixels
   every second. Same failure as the .mdot block at :1144, and the fix broke it
   a second time by quoting the sequence in the explanation. Do not name it in
   here: there is no escaping inside a CSS comment, so the quoted copy closes
   the comment exactly as the original did. */
.phase-label {
  min-width: 84px; font-size: 0.74rem; color: var(--dim);
  font-variant-numeric: tabular-nums;
}

/* ---- THE LAYER TILE (see layerTile in app.js) --------------------------
   One layer, one object: its on/off chip, and under the chip the brightness
   of that layer. The two were a screen apart -- the chip in this row, the
   brightness inside a popover behind a tab strip -- and the reader's
   complaint was exactly that gap: he selected Heat and the bar offered him
   three settings that were not Heat's.

   A GROUP, NOT A CONTROL. All it does is bracket a layer chip with that
   layer's one view choice so the choice reads as belonging to the layer
   rather than to the panel, and hide the choice when the layer is off. It
   never breaks across lines; flex-wrap: nowrap is what guarantees that. */
.mlayer { display: inline-flex; align-items: center; flex-wrap: nowrap; gap: 6px; }
/* Same idea for the transport: play, the replay popover and the feed toggle
   are one thing and the row breaks before them rather than through them. */
.mgroup { display: inline-flex; align-items: center; flex-wrap: nowrap; gap: 6px; }
/* A view choice for a layer that is not on screen is a control that does
   nothing, so an off group collapses back to the plain chip it always was. */
.mlayer.off { gap: 0; }
/* The label goes with the group it names, or an off class filter leaves the
   word "Class" floating in the row pointing at nothing. */
.mlayer.off > .mpick, .mlayer.off > .mpick-lab { display: none; }

/* ---- A SINGLE-SELECT GROUP (see pickGroup in app.js) -------------------
   Three chips, pick one. It has to be visibly NOT the toggle beside it, and
   this site already has that distinction: #bb-tabs above marks the selected
   one violet on violet-18% and leaves the rest as ordinary chips. Same
   recipe, same colours, no new accent -- and the chips are smaller and share
   one border box, so the group reads as one control rather than as three
   more layers. */
.mpick { display: inline-flex; align-items: center; border: 1px solid var(--border); }
.mpick .mchip {
  border: 0; padding: 4px 9px; font-size: 0.72rem; letter-spacing: 0.03em;
}
.mpick .mchip + .mchip { border-left: 1px solid var(--border); }
.mpick .mchip:not(.off) {
  color: var(--violet); background: rgba(160, 107, 255, 0.18);
}
.mpick .mchip.off { opacity: 0.75; }
.mpick .mchip:hover { color: var(--text); }

/* A NAME ON A PICK GROUP, and only where one is needed. The range group needs
   none: its own identity chip says "Any range" and the other two are metres,
   so the dimension is on the chips. The class group sits directly beside it
   and, unlabelled, two .mpick boxes in a row read as one six-chip control --
   and this one acts on several layer chips rather than on the one it sits
   beside, which is exactly the thing a reader cannot infer from position.
   .mctl-name is the site's
   existing "name of a control" (dim, no wrap); this only brings it down to
   the size the chips inside the group run at. */
.mpick-lab { font-size: 0.72rem; letter-spacing: 0.03em; }

/* The routes count used to live in a slider row under the map, where it read
   as a fourth control. It is the heat layer's STATE, so it is written as the
   heat layer's key with every other layer's -- see heatLegendHtml. The
   honest-count argument that put it there has not changed: it is still on the
   page, still written per frame, still not behind a disclosure triangle. */

/* ---- ONE KEY ROW PER LAYER (see lkeyRow in app.js) ---------------------

   Four layers used to write into three elements, and the third held Damages
   and Tracers together, so the two ran on as one wrapped paragraph -- with
   "TRACERS . BY CLASS" starting mid-line inside the Damage block and nothing
   telling a reader where one layer's facts stopped. Every layer owns a row
   now, and the row is a two-part grid rather than one long flex line:

     [ dot  LAYER NAME ] [ everything that layer has to say, wrapping ]

   The name is its own column so a body that wraps indents under ITSELF. On
   one flex line a wrapped body starts again at the left margin, under the
   next row's name, which is the specific way the old block became unreadable.

   Nothing here is a new colour or a new component: .shot-key-t's type is what
   the name uses, and the chips inside the body are the same .shot-key-n /
   .dmg-key-n / .dmg-sw-lab they have always been. That distinction is load
   bearing and is NOT collapsed -- .shot-key-n is a clean, complete count and
   .dmg-key-n is a qualified or partial one. */
/* ONE grid for all four rows, not one per row, so every layer's facts start
   at the same x however long its name is. .lkey is display: contents purely
   so the name and the body become items of THAT grid -- the row element still
   exists as the thing the renderer writes into, and an empty one generates no
   box at all, which is how a layer that is off costs nothing. */
.lkeys {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline; gap: 8px 12px;
  margin-top: 10px; color: var(--dim);
}
.lkey { display: contents; }
/* The layer's own name and its own swatch, so the row is tied to the chip
   that switched it on without repeating the chip. Never wraps: a two-line
   name would put the body out of line with every other row. */
.lkey-name {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  font-size: 0.7rem; color: var(--text);
}
.lkey-name .mdot { flex: 0 0 auto; }
.lkey-body {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 14px;
  min-width: 0;
}
/* Under a phone width the name column would leave the body four words wide,
   so the two stack and the name becomes a heading for the lines under it. */
@media (max-width: 620px) {
  .lkeys { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  /* Stacked, the name is a heading for the lines under it, so the gap has to
     come back between one layer's block and the next. */
  .lkeys .lkey-name { margin-top: 6px; }
}
.shot-key-t { font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; font-size: 0.7rem; }
.shot-key-n { font-size: 0.72rem; font-variant-numeric: tabular-nums; }

/* match-history rows carry their map's art as a washed-out background
   (tomato.gg style). The gradient keeps text readable over any map. */
tr.map-bg-row {
  background-size: cover;
  background-position: center;
  position: relative;
}
tr.map-bg-row > td { position: relative; background: rgba(16, 22, 46, 0.86); }
tr.map-bg-row > td:first-child { background: linear-gradient(90deg, rgba(16,22,46,0.72), rgba(16,22,46,0.88)); }
tr.map-bg-row:hover > td { background: rgba(22, 30, 62, 0.80); }

/* Averages page: one big chart per row, full width */
.avg-panel .chart-svg { width: 100%; height: auto; }
/* centre the kill matrix in its panel rather than left-hanging */
.avg-panel .table-scroll:has(.matchup-grid) { display: flex; justify-content: center; }

/* ---- Upload ----------------------------------------------------
   Same vocabulary as the rest of the site: 10px radius, --panel/--panel2,
   the blue-violet --acc for anything that is the primary action. Status
   colour is spent only on the final verdict; the per-check marks stay blue
   and dim, because a page of red and amber ticks reads as an alarm rather
   than as a form that is working.                                       */

.up-drop {
  border: 1px solid var(--border);
  background: var(--panel2);
  padding: 30px 20px;
  text-align: center;
  cursor: pointer;
  transition: border-color 140ms, background 140ms;
}
.up-drop:hover, .up-drop.over {
  border-color: var(--blue);
  background: rgba(79, 124, 255, 0.07);
}
.up-drop .up-big { font-size: 1.05rem; font-weight: 650; margin-bottom: 4px; }
.up-drop .up-icon {
  width: 34px; height: 34px; margin: 0 auto 10px;
  background: var(--acc);
  display: grid; place-items: center; color: #fff;
}
.up-drop .up-icon svg { width: 17px; height: 17px; display: block; }

.up-sum { margin-top: 14px; color: var(--dim); }
.up-sum:empty { margin-top: 0; }

.up-job { margin-top: 16px; }
.up-jhead { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.up-jname { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.up-jstate {
  margin-left: auto; font-size: 0.72rem; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--dim); white-space: nowrap;
}
.up-jstate.running { color: var(--blue); }
.up-jstate.up-UNVERIFIED { color: var(--blue); }
.up-jstate.up-CORROBORATED { color: #57b97e; }
.up-jstate.up-REJECTED, .up-jstate.error { color: #d2687a; }

.up-bar {
  height: 4px; background: var(--panel2); border-radius: 99px;
  overflow: hidden; margin-bottom: 7px;
}
.up-bar i {
  display: block; height: 100%; width: 0; background: var(--acc);
  border-radius: 99px; transition: width 220ms ease;
}
.up-bar.done i { background: linear-gradient(100deg, #4f7cff, #57b97e); }
.up-bar.fail i { background: linear-gradient(100deg, #7a4f8f, #d2687a); }
.up-phase { font-size: 0.76rem; color: var(--dim); min-height: 1.1em; }

/* stages: quiet one-liners that only open when they need to */
.up-stage { border-top: 1px solid var(--border); }
.up-stage:first-child { border-top: 0; }
.up-stage > summary {
  display: flex; align-items: center; gap: 11px; cursor: pointer;
  padding: 8px 0; font-size: 0.88rem; color: var(--text); list-style: none;
}
.up-stage > summary::-webkit-details-marker { display: none; }
.up-stage > summary:hover .up-note { color: var(--text); }
.up-stage .up-ic {
  width: 15px; flex: 0 0 15px; text-align: center;
  font-size: 0.78rem; font-weight: 700; color: var(--blue);
}
.up-stage .up-ic.warn { color: #c79a52; }
.up-stage .up-ic.fail { color: #d2687a; }
.up-stage .up-ic.skip { color: var(--dim); }
.up-stage.skip > summary, .up-stage.skip .up-ic { color: var(--dim); }
.up-note { margin-left: auto; font-size: 0.74rem; color: var(--dim); }

.up-list-inner { list-style: none; margin: 0 0 10px 26px; padding: 0; }
.up-row { display: flex; gap: 11px; align-items: flex-start; padding: 3px 0; font-size: 0.84rem; }
.up-row .up-ic { color: var(--blue); }
.up-row.warn .up-ic { color: #c79a52; }
.up-row.fail .up-ic { color: #d2687a; }
.up-row.skip { opacity: 0.5; }
.up-row.skip .up-ic { color: var(--dim); }
.up-why { font-size: 0.8rem; color: #d2687a; margin-top: 2px; }
.up-row.warn .up-why { color: #c79a52; }

.up-verdict {
  margin-top: 14px; border: 1px solid var(--border);
  padding: 15px 17px; background: var(--panel2);
}
.up-verdict b { font-size: 1.05rem; }
.up-REJECTED { border-color: rgba(210, 104, 122, 0.45); }
.up-added {
  margin-top: 11px; padding-top: 10px;
  border-top: 1px solid var(--border); font-size: 0.85rem;
}

/* the match that came out of the file */
.up-match {
  margin-top: 14px; border: 1px solid var(--border);
  overflow: hidden;
  background-size: cover; background-position: center;
}
.up-minner { background: rgba(16, 22, 46, 0.88); padding: 15px 17px; }
.up-mtop { margin-bottom: 12px; }
.up-mtop b { font-size: 1.05rem; }
.up-teams { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.up-tside {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--dim); padding-bottom: 6px; margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.up-trow { font-size: 0.84rem; padding: 2px 0; }
.up-trow.me { color: var(--blue); font-weight: 650; }
.up-trow.me span { font-size: 0.66rem; opacity: 0.7; text-transform: uppercase; }


@media (max-width: 620px) { .up-teams { grid-template-columns: 1fr; } }

/* upload: the explanation panel spans the row, three columns of text so a
   full width block of prose does not turn into one long line */
.up-how { margin-top: 14px; }
.up-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 22px; }
.up-cols h3 { font-size: 0.86rem; margin: 0 0 5px; color: var(--text); }
.up-cols p { margin: 0; }
.up-note {
  margin: 18px 0 0; padding-top: 14px; border-top: 1px solid var(--border);
  color: var(--dim);
}

/* animated ellipsis on the loading state, so it reads as in progress
   rather than as a stuck screen */
.dots i { display: inline-block; animation: dot 1.2s infinite; opacity: 0.25; }
.dots i:nth-child(2) { animation-delay: 0.2s; }
.dots i:nth-child(3) { animation-delay: 0.4s; }
@keyframes dot { 0%, 60%, 100% { opacity: 0.25; } 30% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .dots i { animation: none; opacity: 1; } }

/* upload: signed-in state and the way in when signed out */
.up-signin-banner {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: var(--panel2); border: 1px solid var(--border);
  padding: 13px 16px; margin-bottom: 14px;
  font-size: 0.86rem;
}
.up-signin-banner div { flex: 1 1 260px; color: var(--dim); }
.up-signin-banner b { color: var(--text); }
.btn-signin {
  background: var(--acc); color: var(--bg); font-weight: 500; font-size: 0.85rem;
  padding: 8px 16px; text-decoration: none; white-space: nowrap;
}
.btn-signin:hover { filter: brightness(1.09); }
.up-signed {
  background: rgba(87, 185, 126, 0.08); border: 1px solid rgba(87, 185, 126, 0.35);
  padding: 11px 15px; margin-bottom: 14px;
  font-size: 0.85rem; color: var(--text);
}
.up-signin { color: var(--dim) !important; }
.up-signin a { color: var(--blue); }
.match-credit { color: var(--blue); }

/* upload tokens */
.tok-new { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.tok-new .search-box { flex: 1 1 220px; min-width: 0; }
.tok-new .btn-signin { border: none; cursor: pointer; }
.tok-table { min-width: 0; font-size: 0.85rem; }
.tok-table td { padding: 7px 10px 7px 0; border-bottom: 1px solid var(--border); white-space: normal; }
.tok-revoke {
  background: var(--panel2); border: 1px solid var(--border); color: var(--dim);
  padding: 4px 11px; font-size: 0.78rem; cursor: pointer;
}
.tok-revoke:hover { border-color: #b05252; color: #d98a8a; }
.tok-new-out {
  margin-top: 14px; padding: 13px 15px; font-size: 0.85rem;
  background: rgba(240, 180, 60, 0.08); border: 1px solid rgba(240, 180, 60, 0.35);
}
.tok-value {
  margin-top: 9px; padding: 10px 12px; font-size: 0.82rem;
  background: var(--bg); border: 1px solid var(--border); overflow-wrap: anywhere;
  user-select: all;
}

/* upload: stages that passed, folded onto one line.
   Six full rows saying "9 passed" pushed the map and rosters off screen, so
   the clean ones collapse to a strip and only failures keep a block each. */
.up-strip {
  border: 1px solid var(--border); border-radius: 9px;
  background: var(--panel2); margin-bottom: 8px;
}
.up-strip > summary {
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
  padding: 8px 12px; cursor: pointer; list-style: none;
  font-size: 0.8rem; color: var(--dim);
}
.up-strip > summary::-webkit-details-marker { display: none; }
.up-strip[open] > summary { border-bottom: 1px solid var(--border); }
.up-step { color: #6fd39b; white-space: nowrap; }
.up-arrow { color: var(--dim); opacity: .5; }
.up-strip .up-note { margin-left: auto; }
.up-bang {
  display: inline-block; background: rgba(240, 180, 60, .18);
  color: #f0be5e; border-radius: 4px; padding: 0 5px; font-weight: 700;
}
.up-substage {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.68rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--dim); padding: 8px 12px 2px;
}

/* dual back navigation on a match page */
.backnav { display: flex; gap: 18px; flex-wrap: wrap; align-items: center; }
.backnav a { font-size: 0.9rem; }
.backnav a + a { color: var(--dim); }
.backnav a + a:hover { color: var(--blue); }

/* ---- persistent upload dock (outside #app, survives navigation) ---- */
.jobs-dock {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 30;
  width: min(320px, calc(100vw - 36px));
  background: var(--panel);
  border: 1px solid var(--border);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  font-size: 0.84rem;
  overflow: hidden;
}
.jd-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.jd-head b { flex: 1; font-weight: 650; }
.jd-sum { color: var(--dim); font-size: 0.78rem; }
.jd-toggle {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--dim);
  padding: 2px 8px;
  font-size: 0.75rem;
  cursor: pointer;
}
.jd-toggle:hover { border-color: var(--blue); color: var(--text); }
.jd-rows { max-height: 220px; overflow-y: auto; }
.jd-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--border);
}
.jd-row:last-child { border-bottom: none; }
.jd-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.76rem;
}
.jd-state { font-size: 0.74rem; color: var(--dim); white-space: nowrap; }
.jd-state.running { color: var(--blue); }
.jd-state.waiting { color: var(--dim); }
.jd-state.up-CORROBORATED, .jd-state.up-VERIFIED { color: #6fd39b; }
.jd-state.up-UNVERIFIED { color: #f0be5e; }
.jd-state.up-REJECTED, .jd-state.error { color: #e08a8a; }
.jd-link { display: block; padding: 9px 12px; border-top: 1px solid var(--border); font-size: 0.8rem; }

/* ---- upload history ---- */
.tablewrap-uh { overflow-x: auto; }
.uh-table { width: 100%; min-width: 0; border-collapse: collapse; font-size: 0.84rem; }
.uh-table td {
  padding: 8px 10px 8px 0;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.uh-table tr:last-child td { border-bottom: none; }
.uh-map { color: var(--text); overflow: hidden; text-overflow: ellipsis; max-width: 18ch; }
.uh-when, .uh-state { color: var(--dim); font-size: 0.79rem; }
.uh-pending .uh-map { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 0.76rem; }
.uh-v {
  display: inline-block; min-width: 9ch; text-align: center;
  font-size: 0.72rem; padding: 2px 8px; font-weight: 600;
}
.uh-v.up-CORROBORATED, .uh-v.up-VERIFIED { color: #6fd39b; background: rgba(87,185,126,.12); }
.uh-v.up-UNVERIFIED { color: #f0be5e; background: rgba(240,180,60,.12); }
.uh-v.up-REJECTED { color: #e08a8a; background: rgba(200,90,90,.13); }

/* a repeat upload's checks: one line, still openable */
.up-thin { margin: 8px 0 0; }
.up-thin > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 0.82rem; color: var(--dim); padding: 5px 0; list-style: none;
}
.up-thin > summary::-webkit-details-marker { display: none; }
.up-thin .up-tick { color: #6fd39b; font-weight: 700; }
.up-thin .up-note { margin-left: auto; font-size: 0.74rem; opacity: 0.75; }
.up-thin[open] > summary .up-note::after { content: ""; }

.mu-chip.on { color: var(--text); border-color: var(--blue); background: rgba(79,124,255,.10); }
@media (min-width: 760px) { .mu-grid { grid-template-columns: 1fr 1fr; } }
.mu-h.good { color: #6fd39b; }
.mu-h.bad { color: #e08a8a; }

/* tank fingerprint: radar beside its own readout, stacking on a narrow screen */
#tank-fingerprint-slot {
  flex: 1;
  width: fit-content;
}
.fingerprint-wrap {
  display: flex;
  gap: 22px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.fingerprint-wrap .chart-svg { flex: 0 0 320px; max-width: 320px; }
.fingerprint-key { 
  flex: 1 1 220px; 
  min-width: 200px; 
}
.fingerprint-key > div {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid var(--line, rgba(255, 255, 255, 0.08));
}
.fingerprint-key > div:last-child { border-bottom: 0; }
.fingerprint-key b { font-variant-numeric: tabular-nums; }
.fingerprint-key .dim { color: var(--dim); min-width: 84px; text-align: right; }
.fingerprint-key .fingerprint-key-label {
  width: 80px;
}

@media (width < 1200px) {
  #tank-fingerprint-slot {
    flex: initial;
    width: 100%;
  }
}

.net-input {
  flex: 1 1 200px;
  min-width: 160px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  background: var(--panel-2, rgba(255, 255, 255, 0.04));
  color: var(--text);
  font: inherit;
}
#netcanvas { width: 100%; display: block; background: #05070b; cursor: pointer; }


/* roster gallery */
#tanks-roster {
  margin-top: 60px;
}
.roster-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 12px;
}
.roster-card {
  position: relative;
  display: block;
  padding: 12px;
  border: 1px solid var(--border);
  border-top: 3px solid var(--tc);
  background: linear-gradient(170deg, color-mix(in srgb, var(--tc) 16%, transparent), transparent 70%);
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.roster-card:hover { 
  transform: translateY(-5px); 
  border-color: var(--tc);
  text-decoration: none;
}
.rt-art { height: 74px; display: flex; align-items: center; justify-content: center; }
.rt-img { max-height: 74px; max-width: 100%; }
.rt-name { font-weight: 700; margin-top: 4px; color: var(--tc); font-size: 1.2rem; }
.rt-games { font-weight: 600; color: var(--text); }
.rt-bits { margin-top: 8px; display: grid; gap: 3px; }
.rt-bit { display: flex; justify-content: space-between; gap: 8px; }
.rt-bit span { color: var(--dim); }
.rt-bit b { font-variant-numeric: tabular-nums; }
.rt-class {
  font-weight: 500;
  font-size: 0.72rem;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.rt-ability {
  margin-top: 8px;
  padding-top: 7px;
  border-top: 1px solid var(--border);
  color: var(--tc);
}
/* tyrhq's own one-line description of the ability, under its name */
.rt-ability-text {
  display: block;
  margin-top: 3px;
  color: var(--dim);
  font-size: 0.72rem;
  line-height: 1.35;
}

.duel-table td.num { text-align: center; font-variant-numeric: tabular-nums; }
.team-chip.on { background: color-mix(in srgb, var(--tc) 26%, transparent); border-color: var(--tc); }
.tv-value.good, .team-pair b.good { color: #8fbf5f; }
.tv-value.bad, .team-pair b.bad { color: #d9737a; }
.play-result.good { color: #8fbf5f; }
.play-result.bad { color: #d9737a; }
.play-choice.right { background: rgba(111, 154, 74, 0.35); border-color: #6f9a4a; }
.play-choice.wrong { background: rgba(192, 57, 43, 0.3); border-color: #c0392b; }

.up-dl {
  display: inline-block;
  padding: 10px 18px;
  background: var(--blue, #6ea8fe);
  color: #10141a;
  font-weight: 700;
}
.up-dl:hover { filter: brightness(1.08); }
.tok-table { width: 100%; margin-bottom: 12px; }
.tok-table td { padding: 5px 10px 5px 0; }
.tok-new { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.tok-new-out {
  margin-top: 12px;
  padding: 12px;
  border: 1px solid var(--blue, #6ea8fe);
  background: rgba(110, 168, 254, 0.12);
}
.tok-value {
  margin-top: 8px;
  padding: 8px 10px;
  background: rgba(0, 0, 0, 0.4);
  word-break: break-all;
  user-select: all;
}

/* weight class chip */
.chip-ranked {
  background: rgba(160, 107, 255, 0.16);
  color: #c9b6ff;
  border: 1px solid rgba(160, 107, 255, 0.45);
}


/* ---- balance history -------------------------------------------------
   A change is four things: when, which tank, which stat, and which way it
   moved. Laid out as a grid so those four line up down the page and can be
   scanned vertically, instead of as sentences that have to be read. */
.bh-list { display: flex; flex-direction: column; }
.bh-row {
  display: grid;
  grid-template-columns: 5.6rem minmax(5rem, 7rem) 1fr auto;
  gap: 10px; align-items: baseline;
  padding: 7px 0; border-bottom: 1px solid var(--border);
}
.bh-row:last-child { border-bottom: none; }
.bh-date { color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
.bh-tank { font-weight: 600; }
.bh-stat { color: var(--text); }
.bh-move {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.bh-move b { font-weight: 700; padding: 0 2px; }
/* Up and down, not good and bad. More reload time is worse and more health is
   better, so colouring these as buff/nerf would be wrong about half the time. */
.bh-up   { color: #6ee7a0; }
.bh-down { color: #f0a3a3; }
.bh-new  { color: var(--blue); font-weight: 700; }
.bh-patch h2 { display: flex; align-items: baseline; gap: 10px; }
.bh-count { font-size: 12px; font-weight: 400; color: var(--dim); }
@media (max-width: 620px) {
  .bh-row { grid-template-columns: 4.6rem 1fr; row-gap: 2px; }
  .bh-move { grid-column: 1 / -1; }
}


/* A hairline gap between the layer toggles and the brightness group, so the
   legend reads as two controls rather than five chips of the same kind. */
.ol-sep {
  display: inline-block; width: 1px; height: 16px; margin: 0 6px -3px;
  background: var(--border);
}


/* ---- in-page tabs ----------------------------------------------------
   The site had grown one top-level nav entry per feature, and pages that
   scrolled for screens because everything on them competed to be visible at
   once. Those are the same problem. A tab strip shows one thing and NAMES the
   rest, so a page stays one screen tall and nothing has to be hunted for by
   scrolling.

   Built out of .mchip, which is already the site's "one of these is selected"
   control, rather than inventing a second vocabulary for the same idea. */
.chip-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 14px;
}

.chip-tabs .mchip { font-weight: 600; }

/* The panels themselves. Only one is ever in flow: hiding with [hidden]
   rather than unmounting keeps scroll position and any live canvas alive when
   the reader flips back and forth. */
.tab-panel[hidden] { display: none; }

/* A tab whose content has not been built yet says so in place, rather than
   rendering an empty panel that reads as broken. */
.tab-pending {
  color: var(--dim);
  font-size: 0.85rem;
  padding: 18px 0;
}

/* ---- Best Battles: category chips on one side, page size on the other ---- */
.bb-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.bb-bar .maplegend { margin: 0; }
.bb-size { display: flex; align-items: center; gap: 10px; }
.bb-size .search-box { min-width: 0; padding: 6px 10px; font-size: 0.82rem; }
/* Rows on screen against rows the build shipped. They disagree when the size
   control asks for more than BEST_BATTLES_TOP_N produced, and that is worth
   seeing rather than looking like a control that does nothing. */
.bb-count {
  color: var(--dim);
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- damage feed beside the battle map (damageFeedHtml in app.js) ----

   The map is a square top-down minimap and the panel it sits in is nearly
   twice as wide, so at any desktop width there was dead space either side of
   it. The feed goes there. Below 1080px there is no dead space left to fill,
   so it goes UNDER the map rather than squeezing it -- the map is the thing
   people came for and it never gets narrower to make room for the list. */
.map-with-side {
  display: grid;
  grid-template-columns: minmax(0, 820px) minmax(230px, 1fr);
  gap: 14px;
  align-items: stretch;      /* the feed ends up exactly as tall as the map */
}
.map-with-side .map-stage { max-width: none; margin: 0; }
/* The feed must not be what decides how tall this row is -- only the map is.
   A grid row sizes to its TALLEST item, and a 96-event match is a list three
   times the height of the map, so the panel grew to the length of the match
   and the "scrolling" list never scrolled. An absolutely positioned child
   contributes nothing to its parent's height, so .map-side measures zero, the
   row is exactly the map's square, and the feed fills it and scrolls inside. */
.map-side { position: relative; min-height: 0; }
.map-side .map-dmg { position: absolute; inset: 0; }

.map-dmg {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  border: 1px solid var(--border);
  background: var(--panel2);
  padding: 10px 12px;
}
/* .map-dmg sets display:flex, which OVERRIDES the UA's [hidden]{display:none}
   -- so without this the hide control would set the attribute and change
   nothing at all. Same for the wrapper the feed sits in. */
.map-dmg[hidden], .map-side[hidden] { display: none; }

.map-dmg-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.map-dmg-btns { display: inline-flex; gap: 6px; margin-left: auto; }
.map-dmg-btn {
  background: transparent; color: var(--dim);
  border: 1px solid var(--border);
  padding: 2px 7px; font: inherit;
  font-size: 0.68rem; font-weight: 600; letter-spacing: 0.04em; line-height: 1.5;
  cursor: pointer;
}
.map-dmg-btn:hover { color: var(--text); border-color: var(--blue); }

/* "Pop out": the feed leaves the grid and pins to a corner of the WINDOW, so
   the map gets .map-alone's whole width and the feed stays readable -- and
   stays readable after the reader scrolls past the map, which is the part a
   panel floating inside .map-stage could never do. applyDmgMode re-parents it
   to <body> so no ancestor's transform can capture the fixed positioning. */
.map-dmg-float {
  position: fixed; z-index: 60;
  width: 268px;
  max-height: min(56vh, 460px);
  background: var(--bg);
  border-color: var(--border-light);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
}
.map-dmg-float .map-dmg-head { cursor: move; }
.map-dmg-float .map-dmg-list { max-height: none; }
.map-dmg-head h4 {
  margin: 0;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.map-dmg-n { color: var(--dim); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
.map-dmg-lede { margin: 2px 0 8px; color: var(--dim); font-size: 0.74rem; }
/* flex:1 with min-height:0 is what makes this scroll INSIDE the panel rather
   than growing it. Without the min-height a flex item refuses to shrink below
   its content and the whole panel stretches to the length of the match. */
.map-dmg-list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* position:relative is load-bearing, not decoration: it makes this the
     offsetParent of its rows, which is what mapDamageFeed's scroll arithmetic
     measures against. Without it offsetTop is counted from somewhere further
     up the page and the list silently never scrolls. */
  position: relative;
  flex: 1 1 auto;
  min-height: 120px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.dmg-row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) 12px minmax(0, 1fr) 54px;
  align-items: center;
  gap: 6px;
  padding: 3px 5px;
  font-size: 0.76rem;
  border-left: 2px solid transparent;
  cursor: pointer;
}
.dmg-row:hover { background: rgba(79, 124, 255, 0.12); }
/* A row the class filter is excluding. The feed is the Damage layer's list --
   same events, same count in its header, click a row and the transport seeks
   to that mark -- so it has to show the same set the canvas is showing, or
   the two halves of one layer disagree in front of the reader.

   display: none rather than an opacity, deliberately. A dimmed row is still
   scrolled past and still counted by the eye, and this list is long; a
   greyed-out ledger reads as "these are the ones you cannot click", which is
   not what a filter says. The header keeps the "31 of 108" so the match is
   never made to look shorter than it was. */
.dmg-row-out { display: none; }
.dmg-clock { color: var(--dim); font-size: 0.72rem; }
.dmg-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The same two team colours the roster rows use, so a side reads the same
   way everywhere on this page. */
.dmg-a { color: #8fbf5f; }
.dmg-b { color: #d9737a; }
.dmg-arr { color: var(--dim); text-align: center; }
.dmg-amt { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }
/* Not yet reached by the playhead. Dimmed rather than hidden: the list would
   jump about as the replay ran, and how much is still to come is itself
   worth seeing. */
.dmg-future { opacity: 0.3; }
.dmg-now { background: rgba(79, 124, 255, 0.2); border-left-color: var(--blue); }

@media (max-width: 1080px) {
  .map-with-side { grid-template-columns: minmax(0, 1fr); }
  .map-with-side .map-stage { max-width: 820px; margin: 0 auto; }
  /* Stacked, there is no square to match, so the feed goes back to flowing
     and caps its own list instead. */
  .map-side, .map-side .map-dmg { position: static; }
  .map-dmg-list { max-height: 300px; }
}

/* ================================================================ #/global
   The archive page (see the "global" section of app.js).

   It used to be eight tabs of charts. It is now one scrolling page in four
   numbered parts, and it is styled to sit beside the tank and map pages
   rather than beside a dashboard: real art, a card that is about one game
   rather than one number, and a ranking you can read down.

   Everything here lays out in CSS rather than at a measured pixel width, so
   a chart reflows with its column and the page never scrolls sideways. The
   one SVG shape, the line plot, keeps all of its text OUTSIDE the SVG and its
   strokes non-scaling, so stretching the box never changes a font size or a
   stroke weight.

   Marks follow the dataviz specs: bars capped thin with a 4px rounded data
   end and a square baseline, 2px lines, hairline solid grid, and a 2px gap in
   the SURFACE colour separating touching fills rather than a stroke drawn
   around them. */

.g-page { margin-top: 4px; }

/* ---- a numbered part of the page ---- */
.g-sec { margin-bottom: 34px; }
.g-sec-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 14px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.g-sec-n {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--blue);
  padding: 2px 7px;
  border: 1px solid var(--border);
  background: var(--panel2);
}
.g-sec-head h2 {
  margin: 0;
  padding: 0;
  border: none;
  font-size: 1.35rem;
  letter-spacing: 1.5px;
}

/* ---- one block: a title, ONE sentence, the thing ---- */
.g-block h3 {
  margin: 0 0 2px;
  font-size: 0.95rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
}
/* The sentence. There is exactly one per block and nothing follows the chart,
   so this is the only prose on the page. */
.g-lede {
  margin: 0 0 14px;
  color: var(--dim);
  font-size: 0.82rem;
  max-width: 68ch;
}
.g-more { margin: 14px 0 0; font-size: 0.8rem; }

/* The stat strip. Cards are the site's existing .stat-card; the only addition
   is a second line for the denominator, which is a number and never a badge. */
.g-cards { margin-bottom: 0; }
/* A card row that sits ON TOP of a chart in the same block rather than being
   the block. It needs the gap under it that a standalone row does not. */
.g-cards-tight { margin-bottom: 16px; }
.g-sub {
  display: block;
  font-family: "POE Vetica New", sans-serif;
  font-size: 0.68rem;
  font-variant-numeric: normal;
  color: var(--dim);
  letter-spacing: 0.02em;
  margin-top: 3px;
}

.g-absent { display: flex; align-items: center; gap: 8px; color: var(--dim); font-size: 0.85rem; }
/* No number at all. Never a zero and never an empty cell. */
.g-em { color: var(--dim); font-family: ui-monospace, Consolas, monospace; }

/* ---- 01: a record is a story about one game ---- */
.g-recs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(262px, 1fr));
  gap: 12px;
}
.g-rec {
  position: relative;
  overflow: hidden;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-top: 3px solid var(--tc);
  background-color: var(--panel2);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  gap: 10px;
}
/* The map art is atmosphere, not data, so it sits under a scrim heavy enough
   that every value on top of it keeps its text contrast. */
.g-rec::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, rgba(16, 20, 28, 0.95) 45%, rgba(16, 20, 28, 0.7));
}
.g-rec > * { position: relative; }
.g-rec-body { min-width: 0; }
.g-rec-art { order: 2; margin-left: auto; flex: 0 0 auto; }
.g-rec-img { height: 62px; max-width: 108px; object-fit: contain; }
.g-rec-label {
  color: var(--dim);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
/* The number itself stays in the text colour. It briefly wore the tank's own
   colour, which looked good until Atlas turned up: its palette entry is a red,
   and a red 4,608 under "most blocked" reads as an error rather than a record.
   The tank's colour identifies the tank -- the stripe and the tank's name --
   and never a value. */
.g-rec-val {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.1;
  color: var(--text);
}
.g-rec-who {
  font-weight: 700;
  font-size: 0.95rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.g-rec-where { color: var(--dim); font-size: 0.74rem; }

/* ---- 02: best and worst, at the size the claim deserves ---- */
.g-heroes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 12px;
}
/* The top stripe here is the WIN-RATE side, not the tank's identity colour --
   the same blue and orange the ranking below uses. With the tank's own colour
   on it the worst tank on the roster wore a green stripe and the best wore an
   orange one, which is the chart saying the opposite of the data. */
.g-hero {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-top: 3px solid var(--wc);
  background: linear-gradient(170deg, color-mix(in srgb, var(--tc) 16%, transparent), transparent 70%);
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.g-hero:hover { transform: translateY(-4px); border-color: var(--tc); text-decoration: none; }
.g-hero-val em { font-style: normal; color: var(--wc); }
.g-hero-art { flex: 0 0 auto; }
.g-hero-img { height: 92px; max-width: 132px; object-fit: contain; }
.g-hero-body { min-width: 0; }
.g-hero-label {
  color: var(--dim);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.g-hero-name { font-size: 1.25rem; font-weight: 700; color: var(--tc); }
.g-hero-val {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 2.2rem;
  font-weight: 700;
  line-height: 1.05;
  color: var(--text);
}
.g-hero-n { color: var(--dim); font-size: 0.76rem; }

/* ---- 02: the ranking ---- */
.g-trows { display: grid; gap: 4px; }
.g-trow {
  display: grid;
  grid-template-columns: 26px 38px minmax(74px, 1fr) minmax(110px, 1.5fr) 46px 88px minmax(150px, 1.3fr);
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border: 1px solid transparent;
  border-left: 3px solid var(--tc);
  background: var(--panel2);
  color: var(--text);
}
.g-trow:hover { border-color: var(--tc); text-decoration: none; background: rgba(39, 44, 65, 0.7); }
.g-trow-rank {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
  color: var(--dim);
  text-align: right;
}
.g-trow-art { display: flex; align-items: center; justify-content: center; }
.g-trow-img { max-height: 34px; max-width: 38px; object-fit: contain; }
.g-trow-name {
  font-weight: 700;
  font-size: 0.92rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.g-trow-wr {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.88rem;
  font-weight: 700;
  text-align: right;
}
.g-trow-n {
  color: var(--dim);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.g-trow-vs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  color: var(--dim);
  font-size: 0.72rem;
  min-width: 0;
}
.g-vs-i { display: inline-flex; align-items: center; gap: 5px; min-width: 0; white-space: nowrap; }
.g-vs-i b {
  font-weight: 400;
  color: var(--border-light);
  text-transform: uppercase;
  font-size: 0.62rem;
  letter-spacing: 0.05em;
}
.g-vs-i .tank-icon { height: 18px; width: 18px; object-fit: contain; }

/* Win rate is a value with a SIDE, so it gets a diverging track centred on 50
   rather than a bar growing from zero: from zero every tank on this roster is
   the same length and the chart says nothing. Two hues and a neutral centre
   line, never a hue at the midpoint. */
.g-wt {
  position: relative;
  display: block;
  height: 12px;
  background: rgba(16, 20, 28, 0.55);
  border-radius: 3px;
  overflow: hidden;
}
.g-wt-zero {
  position: absolute;
  left: 50%;
  top: 0;
  width: 1px;
  height: 100%;
  background: var(--border-light);
  opacity: 0.6;
}
.g-wt-fill { position: absolute; top: 2px; height: 8px; border-radius: 2px; }

/* ---- horizontal bars ---- */
.gbars { display: grid; gap: 6px; }
.gbar-row {
  display: grid;
  grid-template-columns: 116px minmax(0, 1fr) 112px;
  align-items: center;
  gap: 10px;
}
.gbars-wide .gbar-row { grid-template-columns: 168px minmax(0, 1fr) 168px; }
.gbar-label {
  font-size: 0.82rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A map's own art beside its name, at the size of a line of text. */
.g-maplbl { display: flex; align-items: center; gap: 7px; min-width: 0; }
.g-maplbl > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-maplbl-img {
  height: 22px;
  width: 34px;
  object-fit: cover;
  flex: 0 0 auto;
  border: 1px solid var(--border);
}
/* A tank's own art beside its name in a bar label, same idea as .g-maplbl. */
.g-tklbl { display: flex; align-items: center; gap: 7px; min-width: 0; }
.g-tklbl > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.g-tklbl-img { height: 22px; width: 26px; object-fit: contain; flex: 0 0 auto; }

/* ---- the two matrices: tank x build, tank x map ----
   Both are the site's own <table> in .table-scroll, so almost nothing is new.
   What IS new is alignment, and it has to be said twice or it does not hold:
   `thead th` is text-align:left, and `td.num` sets tabular-nums but no
   alignment at all, so a right-aligned body under a left-aligned header is
   the default outcome rather than a mistake you have to make. Set both. */
.g-mx th.num,
.g-mx td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.g-mx td.num b { font-weight: 700; font-variant-numeric: tabular-nums; }
/* The games behind the figure above it. This is the ONE thing either table
   says about how much evidence is there, and it is a number -- never a chip,
   never a colour, never a "too few games" message in place of the rate. */
.g-mx .g-cell-n {
  display: block;
  margin-top: 2px;
  color: var(--dim);
  font-size: 0.7rem;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}
/* thead is uppercase and letter-spaced; the count under a column head is a
   number and should read as one. */
.g-mx th .g-cell-n { text-transform: none; letter-spacing: 0; }
.g-mx td { vertical-align: middle; }
.g-mx-name { min-width: 132px; }
/* The tank's own baseline, so a map cell can be read as a move away from it
   rather than as a bare number. */
.g-mx-base { border-right: 1px solid var(--border); }
/* The diverging move track. Fixed, because .g-wt is a block and would
   otherwise take whatever the last column happens to be. */
.g-mx-track { width: 120px; min-width: 120px; }

@media (max-width: 720px) {
  .g-mx-name { min-width: 96px; }
  .g-mx-track { width: 76px; min-width: 76px; }
}

/* ---- 100% stacked row ---- */
.gstack { background: var(--panel2); }
.gstack i { display: block; height: 100%; }
.gstack i + i { margin-left: 2px; }   /* the surface doing the separating */
.gbar-track {
  position: relative;
  height: 12px;
  background: var(--panel2);
  border-radius: 3px;
  overflow: hidden;
  display: flex;
}
.gbar-fill {
  display: block;
  height: 100%;
  border-radius: 0 4px 4px 0;   /* rounded data end, square at the baseline */
  min-width: 0;
}
/* An exact, measured zero. A tick at the baseline, in the dim colour rather
   than the series colour, so it says "this was measured and it is zero"
   without reading as a very short bar. */
.gbar-zero {
  display: block;
  width: 3px;
  height: 100%;
  background: var(--dim);
  border-radius: 1px;
}
.gbar-val {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.8rem;
  color: var(--text);
  text-align: right;
}
.gbar-note { color: var(--dim); font-size: 0.72rem; margin-left: 6px; }

/* ---- vertical columns ---- */
.gcolwrap {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  height: 150px;
  margin-bottom: 6px;
}
.gcol-y { position: relative; min-height: 0; }
.gcol-y span {
  position: absolute;
  right: 8px;
  transform: translateY(-50%);
  color: var(--dim);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.gcol-plot {
  position: relative;
  min-width: 0;
  min-height: 0;
  border-left: 1px solid var(--border);
}
.gcol-grid {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--border);
  opacity: 0.45;
}
.gcols {
  position: absolute;
  inset: 0;
  display: grid;
  gap: 2px;
  align-items: end;
}
.gcol { display: flex; align-items: flex-end; justify-content: center; height: 100%; min-width: 0; }
.gcol i { display: block; width: 100%; max-width: 56px; border-radius: 4px 4px 0 0; }
.gcol:hover i { filter: brightness(1.25); }
.gcol-axis {
  display: flex;
  justify-content: space-between;
  color: var(--dim);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  gap: 4px;
  overflow: hidden;
  margin-left: 46px;
}
.gcol-axis span { white-space: nowrap; }

/* ---- line plot ---- */
.gplot {
  position: relative;
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  height: var(--gplot-h, 220px);
}
.gplot-y { position: relative; min-height: 0; }
.gplot-y span {
  position: absolute;
  right: 8px;
  transform: translateY(-50%);
  color: var(--dim);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.gplot-area {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border-left: 1px solid var(--border);
}
/* Absolute, so the viewBox's own aspect ratio never gets a vote in how tall
   the row is. */
.gplot-area svg { position: absolute; inset: 0; height: 100%; width: 100%; display: block; }
.g-grid { stroke: var(--border); stroke-width: 1; opacity: 0.45; }
.g-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.gplot-x {
  display: flex;
  justify-content: space-between;
  margin: 6px 0 0 46px;
  color: var(--dim);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 900px) {
  /* Seven columns cannot fit a phone, and squeezing them leaves the win-rate
     track about 40px, which is not a chart. The row folds instead: the tank
     and its rate on top, the track under them, and who it hunts last. */
  .g-trow {
    grid-template-columns: 26px 38px minmax(0, 1fr) 46px 78px;
    grid-template-areas:
      "r a n w g"
      "t t t t t"
      "v v v v v";
    row-gap: 6px;
    padding: 8px 10px;
  }
  .g-trow-rank { grid-area: r; }
  .g-trow-art { grid-area: a; }
  .g-trow-name { grid-area: n; }
  .g-wt { grid-area: t; }
  .g-trow-wr { grid-area: w; }
  .g-trow-n { grid-area: g; }
  .g-trow-vs { grid-area: v; }
}

@media (max-width: 720px) {
  .gbar-row { grid-template-columns: 88px minmax(0, 1fr) 84px; gap: 8px; }
  .gbars-wide .gbar-row { grid-template-columns: 104px minmax(0, 1fr) 104px; }
  .gbar-label { font-size: 0.76rem; }
  .gbar-val { font-size: 0.72rem; }
  .g-maplbl-img { height: 18px; width: 26px; }
  .gcolwrap { height: 118px; grid-template-columns: 34px minmax(0, 1fr); }
  .gcol-axis { margin-left: 34px; }
  .g-sec-head h2 { font-size: 1.1rem; }
  .g-rec-val { font-size: 1.6rem; }
  .g-rec-img { height: 48px; max-width: 78px; }
  .g-hero-val { font-size: 1.7rem; }
  .g-hero-img { height: 68px; max-width: 96px; }
}

/* ================================================================ #/match Hits
   The Hits tab: the shell list, and the stage it puts one shell on.

   Everything here leans on what the site already has -- .table-scroll around a
   plain <table>, .mchip for a toggle, .mctl for a labelled control -- so the
   only rules below are the two-column layout, the stage box and the small
   colour key under it. */

.hits-lede { margin: 0 0 10px; max-width: 92ch; }
.hits-filters { margin-bottom: 12px; }
/* A sample size stays a NUMBER. It is bolder than its label and nothing else:
   a coloured pill here would read as a status, and 3 shots with a named
   shooter is a count, not a warning. */
.hits-n { color: var(--text); font-weight: 700; }
.mchip.off .hits-n { color: inherit; }

.hits-layout {
  display: grid;
  /* The list is seven columns and all seven have to be visible without a
     sideways scroll -- a column the reader has to go looking for is a column
     they will not read the distance in. */
  grid-template-columns: minmax(0, 1fr) minmax(360px, 600px);
  gap: 14px;
  align-items: start;
}
.hits-stage-col { min-width: 0; }
.hits-stage {
  position: relative;
  background: var(--panel2);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.hits-stage canvas {
  display: block;
  width: 100%;
  touch-action: none;             /* drag orbits the camera, it does not scroll */
  cursor: grab;
}
.hits-stage canvas:active { cursor: grabbing; }
.hits-view { margin-top: 10px; align-items: center; }

.hits-note { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 4px 14px; }
.hits-key { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.hits-sw { width: 10px; height: 10px; border-radius: 999px; display: inline-block; }
.hits-sw-near { background: #f0be5e; }
.hits-sw-terrain { background: #b4bcde; }
.hits-sw-sel { background: #e08a8a; box-shadow: 0 0 0 1.5px var(--text); }
.hits-sw-traj { background: var(--violet); border-radius: 2px; height: 3px; width: 16px; }
.hits-sw-first { background: transparent; border: 1.5px solid var(--violet); }

.hits-detail { margin-top: 12px; }
.hits-detail-line { color: var(--text); font-size: 0.92rem; }
.hits-sep { color: var(--dim); }
.hits-claims { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; }
.hits-claim b { color: var(--text); }

/* The list sits in a column narrower than the site's 560px table floor, and it
   is seven short columns, so it opts out of that floor and scrolls vertically
   inside its own box instead of stretching the panel to 400 rows. */
.hits-table { max-height: 560px; overflow-y: auto; }
.hits-table table { min-width: 0; font-size: 0.84rem; }
.hits-table thead th { position: sticky; top: 0; background: var(--bg); z-index: 1; padding: 7px 8px; }
.hits-table tbody td { padding: 6px 8px; }
.hits-table tbody tr { cursor: pointer; }
.hits-table tbody tr.hits-sel { background: rgba(79, 124, 255, 0.22); }
.hits-table tbody tr:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; }
.hits-none { color: var(--dim); text-align: center; padding: 18px 8px; }

@media (max-width: 1100px) {
  .hits-layout { grid-template-columns: minmax(0, 1fr); }
  .hits-table { max-height: 420px; }
}

/* ---- Damages / Tracers key (damageLegendHtml in app.js) ----------------

   Reuses .shot-key's row, so both keys sit under the map on the same
   baseline and neither has to invent a layout. What is added is the swatch
   pair -- a dot of the colour that is actually in the picture, and the count
   that colour stands for -- plus the separator between the two layers'
   halves when both are on.

   The counts are NOT badges. They are numbers beside a label, because a
   sample size dressed up as a chip reads as a category. */
/* .dmg-key set the row's colour when the damage key borrowed .shot-key's row.
   Each layer has an .lkey row of its own now and that row carries the colour. */
.dmg-sw-lab {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.72rem; white-space: nowrap; font-variant-numeric: tabular-nums;
}
/* On the same near-black the layer paints on: a swatch shown over the panel
   would be a different colour from the one in the picture. */
.dmg-sw {
  display: block; width: 11px; height: 11px; border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(8, 10, 16, 0.75);
}
/* "recording team only", beside the dealt swatch on a POOLED map.

   It is a qualifier on the swatch, not a status, so it deliberately does not
   get a chip, a border or a colour of its own -- the honesty chips on the
   labs pages are a different scale and borrowing their look here would read
   as one. Italic and dim: quiet enough not to compete with the count, loud
   enough that a reader cannot take the green half for both sides' fire. */
.dmg-sw-note { font-style: italic; opacity: 0.75; letter-spacing: 0.01em; }
/* .dmg-key-sep divided the Damages half of the key from the Tracers half when
   both were written into one element. They are two rows now and the divider
   has nothing left to divide. */
.dmg-key-n { font-size: 0.72rem; font-variant-numeric: tabular-nums; opacity: 0.85; }
