/* ── The display face, SHIPPED rather than fetched ───────────────────────────────────────────────
   This page is served by a manager running on somebody's own box, to strangers. A stylesheet link
   to fonts.googleapis.com therefore did two things at once and neither was visible from inside the
   page: it handed every visitor's IP address to a third party on every load, and on a server with
   no outbound internet — the ordinary state of a game server behind a firewall — it simply never
   arrived, so the console rendered in Segoe UI there and in Chakra Petch here. The faces are the same
   six the admin panel bundles, copied rather than shared: the two trees are separate static roots
   (`public-site` at `/`, `public` under `/admin`), so a path that reached across would depend on a
   mount rather than on the page it is written in.
   Weight 400 is served by the 500 face on purpose — the range is declared here rather than left to
   each engine's matching rules, which is what makes body text a chosen weight instead of a
   substituted one. Licence: fonts/ChakraPetch-OFL.txt. */
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:400 500;font-display:swap;src:url("fonts/chakra-petch-500-latin-ext.woff2") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:400 500;font-display:swap;src:url("fonts/chakra-petch-500-latin.woff2") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:600;font-display:swap;src:url("fonts/chakra-petch-600-latin-ext.woff2") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:600;font-display:swap;src:url("fonts/chakra-petch-600-latin.woff2") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:700;font-display:swap;src:url("fonts/chakra-petch-700-latin-ext.woff2") format("woff2");unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:700;font-display:swap;src:url("fonts/chakra-petch-700-latin.woff2") format("woff2");unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}

:root {
  /* WHAT THE BROWSER ITSELF PAINTS, which this document had never told it — and the admin panel
     had been telling it for months, which is how one product came to have two answers.
     Everything else in this file is our own ink on our own surfaces, and none of it reaches the
     parts a browser draws for us: a scrollbar, an UNTICKED checkbox or radio, a number field's
     spinner, a native <select>'s popup list, a date picker, an autofilled field — and any control
     this sheet has not dressed. With no `color-scheme` every one of those is drawn in the LIGHT
     scheme, which is why the marker form's name box came out WHITE on a near-black modal rather
     than merely undecorated: the missing base style is one defect, and the browser answering in
     the wrong scheme is the second, sitting under it.
     It is not a theme switch — this site has one look — it is telling the browser which one. */
  color-scheme: dark;

  /* Base RGB triplets — the whole palette's tints/lines/glows derive from these two,
     so the theme customizer only has to set a few colours to reskin everything. */
  --accent-rgb: 255, 106, 26;   /* accent (amber) */
  --fg-rgb:     241, 243, 245;  /* foreground/text — drives neutral lines + hovers */
  /* scumsa.com's own palette, shared with the admin panel — one product, one look. The ground and
     the panels went from a warm olive-black to a neutral one and the accent from #f0820c to the
     site's hotter #ff6a1a; every tint, line and glow follows through the two triplets above. */
  --bg:        #0b0c0e;
  --bg-2:      #0f1013;
  --panel:     #15171b;
  --panel-2:   #1c1f25;
  --inset:     #0b0c0e;
  --amber:     #ff6a1a;
  --amber-hi:  #ff9a3c;
  --on-amber:  #0b0c0e;   /* text on amber fills — same value as the admin panel */
  --online:    #54c98a;
  --alarm:     #ff5c5c;
  --standby:   #e6b23a;
  --gold:      #d9b24a;
  --silver:    #b8bcc2;
  --bronze:    #c2814a;
  --discord:    #5865f2;
  --discord-dn: #4752c4;
  --text:      #f1f3f5;
  --muted:     #8b9099;
  /* ONE DELIBERATE DIVERGENCE from scumsa's own sheet, and it is a measurement rather than a
     preference: the site's `--steel-dim` #5e636c reads 2.97:1 on a card here and fails the contrast
     gate. #6b707a is the same colour a step brighter — 3.61:1 — and is what the admin panel took
     for the same reason. Do not "correct" it back to the site's value. */
  --faint:     #6b707a;
  --line:      rgba(var(--fg-rgb), 0.09);
  --line-2:    rgba(var(--fg-rgb), 0.05);
  --line-amber: rgba(var(--accent-rgb), 0.32);
  --hover-bg:  rgba(var(--fg-rgb), 0.05);
  --glow:      rgba(var(--accent-rgb), 0.12);
  --scanline:  rgba(var(--fg-rgb), 0.014);
  --motd-rgb:    var(--accent-rgb);   /* MOTD banner tint (defaults to accent) */
  --motd-accent: var(--amber);        /* MOTD banner border/text */
  /* Share Tech Mono is NOT bundled — no woff2 for it exists in this tree — so it is a local
     preference that falls through to scumsa's own mono stack. It used to come from
     fonts.googleapis.com beside Chakra Petch, which meant the readouts rendered in Consolas for
     every visitor of a firewalled server while looking correct here. One uniform answer beats a
     console that looks different depending on the network. Same decision as the admin panel. */
  --mono: 'Share Tech Mono', 'SF Mono', 'JetBrains Mono', 'Consolas', ui-monospace, monospace;
  --ui:   'Chakra Petch', 'Segoe UI', system-ui, Tahoma, sans-serif;
  --radius: 4px;   /* aligned with the admin panel */
  --radius-md: 6px;
  --radius-lg: 8px;
  --pill: 999px;
  /* Live-map marker palette — shared with the admin map, and deliberately HELD when the rest of
     the sheet moved to scumsa's palette. These are not theme colours wearing another name: what
     they buy is the separation that tells a wild animal from a person and a puppet from a sentry,
     and re-pointing them at the new accent/alarm/online would collapse pairs of layers into one
     another. The same hold was measured and taken in the admin panel. The layer tokens further
     down this file (--c-squad, --c-cargo, --lk-*, --zn-*) are the same set and are held too. */
  --c-player:  #35d07f;
  --c-base:    #ff9a2e;
  --c-vehicle: #5aa0ff;
  --c-chest:   #c9a24b;
  --c-storage: #bd7be6;
  --offline:   #7c8594;
}
* { box-sizing: border-box; }
/* `hidden` MEANS HIDDEN — document-wide, and it has to be `!important`.
   The UA stylesheet's `[hidden] { display: none }` is specificity (0,1,0) from the
   USER-AGENT origin, so ANY author rule setting `display` beats it: a single class
   ties and wins on order, and a descendant selector like `.header-actions
   .lang-select-wrap` (0,2,0) wins outright. The symptom is an element JS believes it
   has hidden, still painted — an empty bordered box, or worse, STALE TEXT, because a
   render that ends in `el.hidden = true` has no reason to clear what it wrote.
   This used to be a hand-maintained list of the selectors somebody had noticed
   (`.player-chip`, `.discord-invite`); five more had grown past it, including a
   settings note that went on telling a player their alerts were limited to their
   flag area after they had turned that off. One rule cannot be grown past, and the
   admin panel's `.hidden { display: none !important }` is the same answer for its
   class-based convention. `check-field-console`'s `hidden-but-painted` rule fails
   the build if this is ever weakened. */
[hidden] { display: none !important; }
/* Media never overflows its container (bleed images opt out — they crop edge-to-edge). */
img:not(.bunker-img):not(.outpost-img) { max-width: 100%; }
/* ── THE CONTROLS, ONCE ─────────────────────────────────────────────────────────────────────
   A text box, a dropdown and a text area had NO base style on this site: every one that existed
   was dressed by the screen it sat on (`.scope select`, `.search input`), so anything new rendered
   as the browser's own white box in the browser's own font, in the middle of a dark panel. That is
   not a per-screen bug to be fixed per screen — it is a missing floor, and every control added
   after this one would have had it too.

   ⚠ IT IS WRITTEN AT ZERO SPECIFICITY, AND THE FIRST VERSION WAS NOT. It shipped as
   `input[type="text"], …` — one element plus one attribute, which is (0,1,1), EXACTLY what
   `.search input` carries. Equal specificity is settled by ORDER, so this floor beat the screens it
   exists to support: the leaderboard and squad search boxes, which hand their border and fill to
   the wrapper around them, were given a second set of both and a padding that grew the box. The
   owner reported it before any check did. Moving the block to the top of the file fixed it and left
   a rule nobody can see — one reordering away from coming back.

   `:where()` settles it instead of policing it: every component rule wins WITHOUT A FIGHT, wherever
   it sits, and only a control that states nothing is touched. It is the admin panel's own pattern,
   written there for the same reason months earlier; the rig's `control-floor` rule now fails the
   build if either sheet's floor is written any other way.

   ⚠ AND `input[type="text"]` DOES NOT MATCH AN INPUT WITH NO `type` ATTRIBUTE — the selector reads
   the attribute, and the HTML default that makes such an input behave as a text box is not one.
   `<input name="label">` in the marker form was the browser's own white box on a dark modal for
   exactly that reason, inside the block written to end it. The negated form above has no such hole:
   it names the types that must be left alone — the ones the browser draws WHOLE, where a background
   paints over the control itself — and covers everything else, typed or not, now or later.

   THE COMPONENT STILL HAS TO STATE ALL THREE. `:where()` fixes who wins a conflict; it does not
   invent a declaration. `.search input` set background and border and not padding, so the floor's
   padding went on applying and the box went on growing until that component said `padding: 0`. */
:where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="submit"]):not([type="button"]),
       select, textarea) {
  background: var(--inset); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.5rem 0.6rem;
  font-family: var(--ui); font-size: 0.95rem; line-height: 1.3;
  /* A control must never be the thing that makes a phone scroll sideways. */
  max-width: 100%;
}
textarea { resize: vertical; min-height: 4.5rem; }
:where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]),
       textarea)::placeholder { color: var(--faint); }
:where(input:not([type="checkbox"]):not([type="radio"]),
       select, textarea):focus-visible {
  outline: 2px solid var(--amber); outline-offset: 1px; border-color: var(--line-amber);
}
/* The one dark-on-dark trap a select carries: its POPUP is drawn by the operating system, and on
   Windows that list is white unless the control says otherwise. */
select option { background: var(--panel-2); color: var(--text); }
/* ── …AND THE ARROW, ONCE, for the same reason the box is ─────────────────────────────────────
   The owner reported it directly: *"v tech dropdown oknech ta sipka je natlacena moc doprava"*.
   The item database's dropdowns were the ONE set of three on this site with no `appearance: none`
   and no chevron of their own, so what they drew was the OPERATING SYSTEM's arrow — which sits hard
   against the control's border, where every other dropdown here sets its own a comfortable 0.8rem
   inside. Measured on the rendered page: a gap of 0px against 15.87px for its siblings.

   It was a per-screen list, and this is the third time this sheet has paid for one. `.sf-select`
   states it, `select#lb-category, .scope select` states it, `.items-toolbar select` did not, and
   `.tf-select` has no rule anywhere — so the fix that keeps working is the FLOOR, which nobody has
   to remember to join. The next dropdown added to this site gets the arrow by existing.

   ⚠ `[multiple]` AND `[size]` ARE NOT DROPDOWNS. They render as list boxes: there is no arrow to
   replace, and a chevron painted into the corner would sit on top of the rows. `.tf-select` — the
   map's per-layer filter — is exactly that, which is why it correctly has no chevron rule and must
   not be given one.

   ⚠ AND A COMPONENT'S `background:` SHORTHAND WIPES THIS. The floor is `:where()`, so it weighs
   nothing and every component wins — which is the point for colour and is an accident for the
   arrow: three rules below stated `background: var(--inset)` and would have reset `background-image`
   to `none` while changing nothing a reader wanted. All three say `background-color` now. The rig's
   `selectarrow` rule reads the RENDERED control, so a fourth written the old way is caught rather
   than trusted to this paragraph.

   The chevron is the same SVG the other two rules already carry, so every dropdown on the site is
   one picture. Its amber is a literal because a `data:` URI cannot read a custom property — the
   limitation `.cdd-box`'s tick records, and the reason the styled `.cdd` dropdown exists for the
   surfaces that need to follow a custom accent. */
:where(select:not([multiple]):not([size])) {
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ff9a2e' stroke-width='2.5'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 0.8rem center;
  padding-right: 2.3rem;
}
/* Keyboard focus stays visible on inputs that reset the native outline. */
.search:focus-within { border-color: var(--line-amber); }
.items-toolbar input:focus-visible,
.items-toolbar select:focus-visible,
.map-search-row input:focus-visible,
.sf-select:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
/* Base scale ≈ 20px (124% of 16px) — sized so the site reads comfortably at 100%
   browser zoom (matches what previously needed zooming to ~110%). Phones keep
   their own tuned scale; the mobile header/map layouts are tight already. */
html { scroll-behavior: smooth; font-size: 124%; background: var(--bg); }
@media (max-width: 900px) and (min-width: 641px) { html { font-size: 116%; } }
@media (max-width: 640px) { html { font-size: 106%; } }
body {
  margin: 0;
  background: transparent; /* the backdrop lives on body::before */
  color: var(--text); font-family: var(--ui); line-height: 1.5; letter-spacing: 0.01em;
  min-height: 100vh; display: flex; flex-direction: column;
}
/* Themed dark base (amber glow + scanlines) — always present, shown when a tab has
   no wallpaper / the server isn't premium. */
body::before {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-color: var(--bg);
  background-image:
    radial-gradient(1100px 500px at 82% -12%, rgba(var(--accent-rgb),0.10), transparent 60%),
    repeating-linear-gradient(0deg, rgba(var(--fg-rgb),0.014) 0 1px, transparent 1px 3px);
  background-repeat: no-repeat, repeat;
  background-position: center, top left;
  background-size: cover, auto;
  transition: opacity 0.25s ease;
}
/* Per-tab wallpaper (premium). A neutral dark overlay tones the (differently-coloured)
   photo to the theme and FADES to near-solid toward the bottom, so it melts into the
   page — its last stop IS --bg (11,12,14), so the photo lands on the page's own ground
   rather than on a colour of its own. The three stops were a warm olive-black and had
   to move with the palette or a wallpapered tab would have kept the old hue exactly
   where the eye compares it against an unwallpapered one.
   The image URL comes from --wallpaper, set per tab by the frontend. */
body.has-wallpaper::before {
  background-image:
    radial-gradient(1100px 500px at 82% -12%, rgba(var(--accent-rgb),0.12), transparent 60%),
    repeating-linear-gradient(0deg, rgba(var(--fg-rgb),0.014) 0 1px, transparent 1px 3px),
    linear-gradient(180deg, rgba(14,15,18,0.55) 0%, rgba(12,13,16,0.80) 48%, rgba(11,12,14,0.99) 92%),
    var(--wallpaper, none);
  background-repeat: no-repeat, repeat, no-repeat, no-repeat;
  background-position: center, top left, center, center;
  background-size: cover, auto, cover, cover;
}
main { flex: 1 0 auto; }
.site-footer { flex-shrink: 0; }
a { color: var(--amber); text-decoration: none; }
a:hover { color: var(--amber-hi); }
.muted { color: var(--muted); }
.small { font-size: 0.82rem; }
.svg-defs { position: absolute; }
.ico { width: 1.05em; height: 1.05em; flex: none; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

/* ── Header ─────────────────────────────────────────────────────────────── */
/* Header + nav stick together as one unit, so the nav can't slide under the
   header from a mismatched offset. */
.topbar { position: sticky; top: 0; z-index: 30; transition: transform 0.25s ease; }
.topbar.nav-hidden { transform: translateY(-100%); }
/* ⚠ **`position: relative; z-index: 1` IS WHAT KEEPS THE LANGUAGE MENU ABOVE THE NAV.**
   The menu hangs off a control in this header and opens over the nav bar underneath it, and it was
   painting UNDER it: "Kill Feed" with its icon, "Chat" and "Eventy" rendered straight across the
   open list between the Čeština and Deutsch rows, with the amber active-tab underline running
   through the gap — on the one control that has to work in seventeen languages.
   `.lang-dd-list` already carried `z-index: 60`, and that was not the problem: it was SCOPED. Two
   rules in the animation block below give `.hud-top > *` and `.mainnav .nav-btn` an animation on
   `transform`, and an element with an animation on a compositable property gets its own STACKING
   CONTEXT — at `z-index: auto`, so both sit at level 0 and the later one in the DOM wins. The
   dropdown's 60 could only ever compete inside its own ancestor. Raising the whole header one level
   above the nav is the fix that does not depend on which of them animates. */
.hud-top {
  display: flex; align-items: center; flex-wrap: wrap; gap: 1.25rem; padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--line-amber);
  background: linear-gradient(180deg, var(--panel), var(--bg-2));
  position: relative; z-index: 1;
}
.brand { display: flex; align-items: center; gap: 0.7rem; cursor: pointer; }
.brand img { height: 50px; }
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.brand-name { font-weight: 700; font-size: 1.05rem; letter-spacing: 0.04em; }
.brand-sub { font-family: var(--mono); font-size: 0.66rem; color: var(--amber); letter-spacing: 0.32em; }

.readout {
  margin-left: auto; display: flex; align-items: center; gap: 0.9rem;
  padding: 0.4rem 1rem 0.4rem 0.85rem; border: 1px solid var(--line); border-left: 2px solid var(--faint);
  background: var(--inset); border-radius: var(--radius);
}
.readout-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--faint); }
.readout.online { border-left-color: var(--online); }
.readout.online .readout-dot { background: var(--online); box-shadow: 0 0 10px var(--online); animation: pulse 2s infinite; }
.readout.offline { border-left-color: var(--alarm); }
.readout.offline .readout-dot { background: var(--alarm); box-shadow: 0 0 10px var(--alarm); }
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:.45;} }
.readout-pop { display: flex; flex-direction: column; }
.readout-num { font-family: var(--mono); font-size: 1.4rem; color: var(--amber-hi); line-height: 1; }
.readout-cap { font-size: 0.6rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.12em; margin-top: 2px; }
.readout-state-wrap { display: flex; flex-direction: column; padding-left: 0.9rem; border-left: 1px solid var(--line); line-height: 1.2; }
.readout-state { font-family: var(--mono); font-size: 0.9rem; letter-spacing: 0.06em; }
.readout-sub { font-size: 0.65rem; color: var(--muted); }

.header-right { display: flex; align-items: center; gap: 0.5rem; }
.header-actions { display: flex; align-items: center; gap: 0.5rem; }
/* Hamburger toggle — hidden on desktop, shown on phones (see the 640px block). */
.ham-btn {
  display: none; align-items: center; justify-content: center; padding: 0.45rem 0.55rem;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--muted); cursor: pointer;
}
.ham-btn .ico { width: 1.25em; height: 1.25em; }
.ham-btn.open, .ham-btn:hover { color: var(--amber-hi); border-color: var(--line-amber); }
.lang-select-wrap { display: inline-flex; }
.lang-select { display: inline-flex; align-items: center; gap: 0.35rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2); color: var(--muted); font-size: 0.85rem; }
.lang-select .ico { width: 1em; height: 1em; }
/* Custom language dropdown with flags (native selects can't show images). The
   BUTTON carries the border/background itself and mirrors .discord-invite /
   .admin-link metrics exactly (same padding, font and inherited line-height),
   so all header controls share one height — the wrapper is just an anchor. */
.lang-dd { position: relative; border: 0; background: none; }
.lang-dd-btn {
  display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.45rem 0.8rem; width: 100%;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.85rem; font-weight: 600; cursor: pointer;
  /* Buttons default to line-height:normal (~1.2) while the neighbouring <a>
     controls inherit the body's 1.5 — that few-px difference was the height
     mismatch. Match the anchors explicitly. */
  line-height: 1.5;
}
.lang-dd-btn:hover { color: var(--amber-hi); border-color: var(--line-amber); }
.lang-flag { width: 1.35em; height: 1em; border-radius: 2px; object-fit: cover; flex: none; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
.lang-dd-chev { margin-left: auto; color: var(--muted); font-size: 0.7rem; }
.lang-dd-list {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 60; min-width: 185px;
  max-height: 62vh; overflow-y: auto; display: flex; flex-direction: column; gap: 2px;
  padding: 0.35rem; background: var(--panel); border: 1px solid var(--line-amber);
  border-radius: var(--radius-lg); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}
.lang-dd-list[hidden] { display: none; }
/* Two columns in the header: nineteen languages in one column ran past the list's own height, and
   the last one was scrolled out of sight under the page. The form picker and the phone drawer keep
   one column, which is what a field that wide wants. */
.hud-top .lang-dd-list:not([hidden]) { display: grid; grid-template-columns: repeat(2, max-content); align-content: start; overflow-x: hidden; max-width: calc(100vw - 2rem); }
/* The alert-language picker is a FORM ROW, not a header control: its button is the width of the
   field, so a 185 px list pinned to the right edge reads as belonging to something else. Under the
   control and the same width as it. */
/* ══ CARD BLOCKS ═══════════════════════════════════════════════════════════════════════════════
   The shapes a label-and-value row cannot express, brought over from the admin map's cards so a
   garden bed reads the same on both. Same class names on purpose: one shape, one stylesheet. */

/* Three tallies side by side, because the question is a comparison. A full-width row whose value
   column is the whole grid rather than a figure. */
.mp-stack { display: block; }
.mp-stack > span:first-child { display: block; margin-bottom: 0.3rem; color: var(--muted); font-size: 0.72rem; }
.mp-bands { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.35rem; }
.mp-band {
  display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: 0.2rem 0.25rem; border-radius: var(--radius);
  background: var(--panel-2); border: 1px solid var(--line);
  line-height: 1.25; white-space: nowrap; color: var(--muted);
}
.mp-band b { font-size: 0.95rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.mp-band i { font-style: normal; font-size: 0.62rem; letter-spacing: 0.02em; }
/* This sheet's OWN tokens, not the admin's names: `--standby` and `--alarm` are what this page
   already paints a warning and a fault with, and `--c-player` is what `.mp-on` uses for the good
   state two hundred lines below. A hardcoded hex here would survive an owner recolouring their
   Field Console, which is the one thing this palette exists to make impossible. */
.mp-band.b75 b { color: var(--c-player); }
.mp-band.b50 b { color: var(--standby); }
.mp-band.b25 b { color: var(--alarm); }

/* One crop, in the same row shape a container's contents use — a picture, the name on its own
   line, its state under it. "Flowering · 83% grown · 2.1 L" is wider than a 320px card, and on a
   shared line it is the NAME that gives the width up. */
.mp-slot { align-items: flex-start; }
.mp-slot .mp-c-txt { display: flex; flex-direction: column; min-width: 0; gap: 0.05rem; }
.mp-c-sub { font-size: 0.7rem; color: var(--muted); }
.mp-slot-dead .mp-c-name { color: var(--muted); text-decoration: line-through; }
.mp-c-ph { width: 13px; height: 13px; color: var(--muted); opacity: 0.7; }

/* A face, not an item plate: a linked player's Discord picture on their own marker card and on a
   squadmate's. Round, and it fills the plate rather than sitting inside it. */
.mp-face { border-radius: 50%; overflow: hidden; }
.mp-face img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.notif-lang-dd { display: block; width: 100%; }
.notif-lang-dd .lang-dd-btn { width: 100%; }
.notif-lang-dd .lang-dd-list { left: 0; right: auto; width: 100%; min-width: 0; max-height: 40vh; }
.lang-dd-item {
  display: flex; align-items: center; gap: 0.55rem; padding: 0.45rem 0.6rem;
  background: none; border: 0; border-radius: var(--radius); color: var(--text);
  font-family: var(--ui); font-size: 0.88rem; cursor: pointer; text-align: left;
}
.lang-dd-item:hover { background: var(--hover-bg); }
.lang-dd-item.active { background: rgba(var(--accent-rgb), 0.12); color: var(--amber-hi); }
.player-chip {
  display: inline-flex; align-items: center; gap: 0.45rem; max-width: 180px;
  padding: 0.45rem 0.8rem; border: 1px solid var(--line-amber); border-radius: var(--radius);
  background: var(--panel-2); color: var(--text); font-weight: 600; font-size: 0.85rem;
}
/* The `[hidden]` guard these two used to carry is now document-wide — see the
   `[hidden] { display: none !important }` rule at the top of this file. */
.player-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-avatar { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; flex: none; margin: -2px 0 -2px -2px; }
.admin-link {
  display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.45rem 0.8rem;
  border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted);
  font-size: 0.85rem; font-weight: 600; background: var(--panel-2);
}
.admin-link:hover { color: var(--amber-hi); border-color: var(--line-amber); }

/* ── Main nav ───────────────────────────────────────────────────────────── */
.mainnav {
  display: flex; gap: 0.25rem; padding: 0 1.5rem; background: var(--bg-2);
  border-bottom: 1px solid var(--line); overflow-x: auto;
  /* ⚠ ELEVEN TABS IN A SCROLLER THAT SHOWS TWO AND A HALF OF THEM ON A PHONE.
     A visitor at 320px met *Overview*, *Leaderboards* and the left edge of *Squads*, with no
     scrollbar (phones paint none until you drag) and nothing else saying the other eight tabs are
     there — My Stats, the map, the Database and the kill feed were simply not discoverable. The
     width rules are all right about this: it is a scroller on purpose, it does not overflow the
     PAGE, and every label fits. What was missing was any sign that it scrolls.

     The classic two-layer trick, and it earns its keep by needing no JS and no resize listener: the
     `local` layers scroll WITH the content and sit flush against its two ends; the `scroll` layers
     are pinned to the box. At rest against the left edge the local cover sits exactly over the left
     shadow and hides it, and the moment the content moves the shadow is uncovered. Both are
     BACKGROUND layers, so they paint behind the buttons — `elementFromPoint` still answers with the
     tab, which is what the rig's `ontop` rule reads. */
  background-image:
    linear-gradient(to right, var(--bg-2) 45%, rgba(0, 0, 0, 0)),
    linear-gradient(to left, var(--bg-2) 45%, rgba(0, 0, 0, 0)),
    linear-gradient(to right, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0)),
    linear-gradient(to left, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  background-repeat: no-repeat;
  background-size: 26px 100%, 26px 100%, 14px 100%, 14px 100%;
  background-position: left center, right center, left center, right center;
  background-attachment: local, local, scroll, scroll;
}
.nav-btn {
  display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer; white-space: nowrap;
  background: transparent; border: 0; border-bottom: 2px solid transparent;
  color: var(--muted); font-family: var(--ui); font-size: 0.9rem; font-weight: 600;
  padding: 0.85rem 0.9rem; letter-spacing: 0.02em;
}
.nav-btn:hover { color: var(--text); }
.nav-btn.active { color: var(--amber); border-bottom-color: var(--amber); }
.nav-btn[hidden] { display: none; }
.nav-btn .ico { font-size: 1.05rem; }

main { width: 100%; max-width: 1600px; margin: 0 auto; padding: 1.5rem 1.5rem 3rem; }
.view[hidden] { display: none; }

/* ── Panels ─────────────────────────────────────────────────────────────── */
.panel { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem 1.2rem; }
.panel::before, .panel::after { content: ''; position: absolute; width: 10px; height: 10px; pointer-events: none; border-color: var(--line-amber); border-style: solid; border-width: 0; }
.panel::before { top: -1px; left: -1px; border-top-width: 2px; border-left-width: 2px; }
.panel::after { bottom: -1px; right: -1px; border-bottom-width: 2px; border-right-width: 2px; }
.panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: nowrap; margin-bottom: 1rem; }
/* Let the heading shrink and wrap its meta internally so the refresh button always
   stays beside the title (top-aligned) instead of being pushed onto its own line. */
.panel-head > h2, .panel-head > h3 { flex: 1 1 auto; min-width: 0; }
.panel-head > .btn { flex: none; }
.panel h2 { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin: 0; font-size: 0.95rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--amber); }
.panel h3 { display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem; margin: 1.8rem 0 0.7rem; font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
/* On phones let the long "· total · delayed" meta drop onto its own line. */
/* Sub-count next to a heading: stays inline when it's short (e.g. "On sale · 5"),
   and only a long one wraps — shrinking to the width and wrapping its own text rather
   than being forced onto an indented line or overflowing. */
.panel h2 .small, .panel h3 .small { min-width: 0; overflow-wrap: anywhere; }
.panel h3:first-child, .panel-head + h3 { margin-top: 0.4rem; }
.panel h3 .ico { color: var(--amber); }
/* My Stats + player/squad modal section headings — match other tabs' section titles. */
#view-mystats h3, #modal-body h3 { font-size: 0.95rem; color: var(--amber); margin-top: 1.6rem; }
.stamp { margin: 0.8rem 0 0; font-family: var(--mono); font-size: 0.7rem; color: var(--faint); letter-spacing: 0.06em; }

/* ── Overview tiles + jump cards ────────────────────────────────────────── */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.7rem; }
.tile { display: flex; align-items: center; gap: 0.7rem; padding: 0.85rem 1rem; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); }
.tile > .ico { color: var(--amber); font-size: 1.5rem; }
.tile-t { display: flex; flex-direction: column; line-height: 1.1; }
.tile-v { font-family: var(--mono); font-size: 1.45rem; }
.tile-k { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
.ov-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; margin-top: 1.25rem; align-items: start; }
.ov-card h2 { font-size: 0.85rem; }
.ov-top { display: grid; grid-template-columns: 2fr 1fr; gap: 1rem; margin-bottom: 1.75rem; align-items: stretch; }
@media (max-width: 720px) { .ov-top { grid-template-columns: 1fr; } }
.tiles { margin-bottom: 1.75rem; }
.srv-table { width: 100%; border-collapse: collapse; }
.srv-table th, .srv-table td { border-bottom: 1px solid var(--line); padding: 0.45rem 0; }
.srv-table th { text-align: left; font-weight: 400; color: var(--muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; padding-right: 0.9rem; white-space: nowrap; vertical-align: top; width: 1%; }
.srv-table td { font-family: var(--mono); color: var(--text); word-break: break-word; }
.srv-table tr:last-child th, .srv-table tr:last-child td { border-bottom: 0; }
.ov-restart { display: flex; flex-direction: column; gap: 0.2rem; padding: 0.4rem 0; }
.ov-big { font-family: var(--mono); font-size: 1.8rem; color: var(--amber-hi); line-height: 1; }
.srv-info { display: flex; flex-direction: column; gap: 0.4rem; }
.srv-name { font-size: 0.95rem; font-weight: 600; color: var(--text); line-height: 1.25; word-break: break-word; }
.srv-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding-top: 0.35rem; border-top: 1px solid var(--line); }
.srv-k { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
.srv-v { font-family: var(--mono); font-size: 0.9rem; color: var(--amber); }
.ov-squads { list-style: none; margin: 0; padding: 0; counter-reset: sq; }
.ov-squads li { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0; border-bottom: 1px solid var(--line); }
.ov-squads li::before { counter-increment: sq; content: counter(sq); font-family: var(--mono); color: var(--faint); min-width: 1.4rem; }
.ov-squads .sq-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-squads .sq-val { font-family: var(--mono); color: var(--amber); }

/* ── Leaderboards ───────────────────────────────────────────────────────── */
.lb-tools { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.search { display: flex; align-items: center; gap: 0.4rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.35rem 0.6rem; }
.search .ico { color: var(--muted); }
/* The WRAPPER is the field: it carries the border, the fill and the padding, so the input inside it
   carries none of the three. The third used to go without saying, because nothing on this site
   styled a bare input at all; once the control floor existed its 0.5rem landed here and every
   search box on the site grew. That is why the owner reported it as "it has some background and the
   size is odd" rather than as one obviously wrong thing — two of the three were answered here and
   the padding was not. A component that hands its own box to a wrapper states all three. */
.search input { background: transparent; border: 0; padding: 0; outline: none; color: var(--text); font-family: var(--ui); width: 140px; }
.search input::placeholder { color: var(--faint); }
/* The sub-category picker is the main control on this screen — it chooses which board you are
   looking at — but it was rendering smaller than the group chips above it. Sized to match its
   importance: readable text, a real hit target, and room for the longer category names. */
select#lb-category {
  /* `background-color`, not the shorthand — see `.items-toolbar select`. */
  background-color: var(--inset); color: var(--text);
  border: 1px solid var(--line-amber); border-radius: var(--radius);
  padding: 0.6rem 0.9rem; font-family: var(--ui); font-size: 1rem; font-weight: 600;
  min-height: 2.75rem; width: 100%; max-width: 420px; cursor: pointer;
}
select#lb-category:hover, select#lb-category:focus { border-color: var(--amber); outline: none; }
/* Two-level category filter: group pills, then the category select for that group */
.lb-filter { display: flex; flex-direction: column; gap: 0.6rem; margin: 0.2rem 0 1rem; }
.lb-groups { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.lb-gpill { padding: 0.4rem 0.85rem; border: 1px solid var(--line); border-radius: var(--pill); background: var(--inset); color: var(--muted); font-family: var(--ui); font-size: 0.78rem; cursor: pointer; text-transform: uppercase; letter-spacing: 0.06em; transition: border-color 0.12s, color 0.12s; }
.lb-gpill:hover { border-color: var(--line-amber); color: var(--text); }
.lb-gpill.active { background: var(--amber); color: var(--on-amber); border-color: var(--amber); font-weight: 700; }
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.seg-btn { background: var(--inset); color: var(--muted); border: 0; cursor: pointer; padding: 0.42rem 0.75rem; font-family: var(--ui); font-size: 0.82rem; }
.seg-btn:hover { color: var(--text); }
.seg-btn.active { background: var(--amber); color: var(--on-amber); font-weight: 700; }
.lb-list { list-style: none; margin: 0; padding: 0; }
/* Rank · name · value must ALL stay visible: the name keeps a minimum width and
   ellipsizes, and a long value (translated units) ellipsizes too instead of
   squeezing the name to nothing or overflowing the card. */
/* FOUR columns, and the first two are `.squad-row`'s first two EXACTLY — 2.4rem for the rank, 1.6rem
   for the face or the crest, and the same 0.7rem gap. That is what the owner's *"spravne zarovnany
   vse"* buys: a ranking of players and a ranking of squads put their names on the same vertical line,
   whichever of the two a reader is looking at. Adding an element to a fixed grid does NOT add a
   column — every cell shifts one along and the last drops out of the track list — which is why the
   squad leaderboard emits its badge cell too rather than skipping it (see `renderLeaderboard`). */
.lb-row { display: grid; grid-template-columns: 2.4rem 1.6rem minmax(7ch, 1fr) minmax(0, auto); align-items: center; gap: 0.7rem; padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--line); }
/* A phone gives the face back most of what it took: the row is NARROWER than it was before the face
   existed, so `#lb-list` cannot start scrolling sideways because of it.

   ⚠ **AND THE NAME STILL WENT TO SEVEN PIXELS, BECAUSE `minmax(0, 1fr)` BESIDE `max-content` GIVES
   THE WHOLE TRACK AWAY.** Measured at a true 310px on the FRENCH leaderboard: `Cveta_22` rendered in
   7px with 9% of it on screen, and every row under it the same. A `1fr` track has no floor once its
   minimum is 0, and `max-content` has no give at all, so the inflexible column takes what it likes
   and the flexible one pays — the name is what a reader is scanning FOR, and it was the only thing
   on the row that could lose.

   The value is not a number. `fmtLbVal` is a figure plus its TRANSLATED unit, so the same row that
   reads `826 headshots` in English reads `826 tiros a la cabeza` in Spanish and `826 coups à la
   tête` in French — three very different max-contents for one layout, and the English one fits.
   That is the seventeen-languages lesson on this surface: the rule was written against the language
   it was read in.

   So on a phone the row is TWO LINES and nothing is truncated at all: the rank and the face span
   both, the name takes the whole remaining width on the first, and the figure with its unit sits
   under it. It is the same answer `.squad-row` already gives its member count, for the same
   reason. */
@media (max-width: 560px) {
  .lb-row {
    grid-template-columns: 2.1rem 1.4rem minmax(0, 1fr);
    column-gap: 0.45rem; row-gap: 0.05rem; align-items: center;
  }
  .lb-row .lb-rank, .lb-row .pl-row-face, .lb-row .sq-row-face { grid-row: 1 / span 2; align-self: center; }
  .lb-name { grid-column: 3; grid-row: 1; }
  /* `overflow: visible` and `clip` together: the point of the second line is that the figure is
     whole, and an ellipsis left on it would quietly go on cutting the thing this rule exists for. */
  .lb-val {
    grid-column: 3; grid-row: 2; text-align: left; font-size: 0.86rem;
    overflow: visible; text-overflow: clip; white-space: normal;
  }
  .pl-row-face { width: 1.4rem; height: 1.4rem; }
}
.lb-row:hover { background: rgba(var(--accent-rgb),0.05); }
.lb-rank { font-family: var(--mono); font-size: 0.95rem; color: var(--faint); text-align: center; }
.lb-name { font-family: var(--ui); font-size: 1rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-val { font-family: var(--mono); color: var(--amber); font-size: 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
.lb-row.top .lb-rank { font-weight: 700; }
.lb-row.r1 { background: linear-gradient(90deg, rgba(217,178,74,0.12), transparent 70%); }
.lb-row.r1 .lb-rank, .lb-row.r1 .lb-val { color: var(--gold); }
.lb-row.r2 .lb-rank, .lb-row.r2 .lb-val { color: var(--silver); }
.lb-row.r3 .lb-rank, .lb-row.r3 .lb-val { color: var(--bronze); }
.lb-name .lb-sub { color: var(--faint); font-family: var(--mono); font-size: 0.82em; font-style: normal; }
/* Squads list — same medal treatment as the leaderboard */
.squad-row.top .sq-rank { font-weight: 700; }
.squad-row.r1 { background: linear-gradient(90deg, rgba(217,178,74,0.12), transparent 70%); }
.squad-row.r1 .sq-rank, .squad-row.r1 .sq-list-score { color: var(--gold); }
.squad-row.r2 .sq-rank, .squad-row.r2 .sq-list-score { color: var(--silver); }
.squad-row.r3 .sq-rank, .squad-row.r3 .sq-list-score { color: var(--bronze); }
.ov-motd { margin-bottom: 1rem; padding: 0.75rem 1rem; border: 1px solid var(--motd-accent); border-left: 3px solid var(--motd-accent); border-radius: var(--radius); background: rgba(var(--motd-rgb), 0.10); color: var(--text); font-size: 0.92rem; line-height: 1.45; }
.discord-invite { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.45rem 0.8rem; border: 1px solid var(--discord); border-radius: var(--radius-md, 6px); color: #8b93f7; font-size: 0.85rem; text-decoration: none; }
.discord-invite:hover { background: rgba(88,101,242,0.14); color: #a8adf9; }
.discord-invite .ico { width: 1em; height: 1em; }
.ov-more { display: block; width: 100%; margin-top: 0.55rem; padding: 0.4rem; background: none; border: none; border-top: 1px solid var(--line); color: var(--amber); font: inherit; font-size: 0.85rem; text-align: center; cursor: pointer; }
.ov-more:hover { color: var(--amber-hi); background: rgba(var(--accent-rgb),0.05); }
/* Events ranking — styled rows (rank + clickable name + per-player mini-stats) */
.ev-table-wrap { overflow-x: auto; }
.ev-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.ev-table thead th { text-align: right; font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 700; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--line-amber); white-space: nowrap; }
.ev-table thead th.ev-th-rank { text-align: center; width: 2.4rem; }
.ev-table thead th.ev-th-name { text-align: left; }
.ev-table tbody td { padding: 0.5rem 0.55rem; border-bottom: 1px solid var(--line); text-align: right; font-family: var(--mono); color: var(--text); white-space: nowrap; }
.ev-table tbody td:nth-child(3) { color: var(--amber); }
.ev-table .ev-rank { text-align: center; color: var(--faint); font-weight: 700; }
.ev-table .ev-name { text-align: left; font-family: var(--ui); font-size: 1rem; font-weight: 600; cursor: pointer; max-width: 12rem; overflow: hidden; text-overflow: ellipsis; }
.ev-table .ev-name:hover { color: var(--amber); }
.ev-row:hover td { background: rgba(var(--accent-rgb),0.05); }
.ev-row.r1 td { background: linear-gradient(90deg, rgba(217,178,74,0.12), transparent 70%); }
.ev-row.r1 .ev-rank { color: var(--gold); }
.ev-row.r2 .ev-rank { color: var(--silver); }
.ev-row.r3 .ev-rank { color: var(--bronze); }
.ev-table .lb-empty { text-align: center; color: var(--muted); font-family: var(--ui); padding: 1.2rem; }
/* ⚠ **BELOW 560px THIS STOPS BEING A TABLE, BECAUSE A NUMBER CUT IN HALF IS A WRONG NUMBER.**
   Nine statistics columns cannot fit a 310px phone, so the wrap above scrolls sideways — and where
   its edge falls depends on how long the HEADERS are in the reader's language. In German the kills
   column landed clear of it; in Russian and Hungarian it landed half past it, and 41, 40 and 39
   were published as 4, 4 and 3. No ellipsis, because a scroll edge is not a text overflow, and
   nothing on the screen said anything had been cut. See `eventRowHtml` for the whole finding.
   One block per player: the rank and the name as a heading, the figures as label-and-value pairs
   that WRAP. Nothing scrolls sideways, so nothing can be cut by a scroll edge. */
@media (max-width: 560px) {
  .ev-table-wrap { overflow-x: visible; }
  .ev-table, .ev-table tbody, .ev-table tr, .ev-table td { display: block; }
  .ev-table thead { display: none; }
  /* FLEX AND WRAP, not a grid. A grid item is block-level in its track, so the seven figures each
     took a row of their own and one player was eight lines tall. Flex lets them flow and wrap into
     a strip, and the name is given a full line by claiming the rest of the first one. */
  .ev-table tbody tr.ev-row {
    display: flex; flex-wrap: wrap; align-items: baseline;
    column-gap: 0.5rem; padding: 0.55rem 0 0.65rem; border-bottom: 1px solid var(--line);
  }
  .ev-table tbody tr.ev-row td { border-bottom: 0; padding: 0; background: none; }
  /* The highlight belongs to the ROW here. `.ev-row.r1 td` paints each cell, which in a table is
     one continuous band and in a stack is a row of grey chips behind the labels. */
  .ev-table tbody tr.ev-row.r1 { background: linear-gradient(90deg, rgba(217,178,74,0.12), transparent 70%); }
  .ev-table tbody td.ev-rank { flex: 0 0 2.2rem; }
  .ev-table tbody td.ev-name { flex: 1 1 auto; max-width: none; }
  .ev-table tbody td[data-label] {
    display: inline-flex; align-items: baseline; gap: 0.3rem;
    margin: 0.3rem 0.7rem 0 0; font-size: 0.86rem;
  }
  /* The strip starts under the name rather than under the rank number: the first figure takes the
     rank column's width as a left margin, and the rest follow it. */
  .ev-table tbody td[data-label]:first-of-type { margin-left: 2.7rem; }
  .ev-table tbody td[data-label]::before {
    content: attr(data-label) ':'; color: var(--muted); font-family: var(--ui);
    font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700;
  }
  /* The "nothing here" row is not an `.ev-row` and must not be laid out as one. */
  .ev-table tbody tr:not(.ev-row) { display: block; }
  .ev-table .lb-empty { display: block; }
}
.lb-row.me { box-shadow: inset 3px 0 0 var(--amber); }
.lb-empty { padding: 1.6rem 0.5rem; color: var(--muted); text-align: center; list-style: none; }

/* ── Dossier / My Stats ─────────────────────────────────────────────────── */
.dossier { padding-top: 1.5rem; }
.dossier-tab { position: absolute; top: -1px; right: 1.1rem; background: var(--amber); color: var(--on-amber); font-weight: 700; font-size: 0.7rem; letter-spacing: 0.28em; padding: 0.25rem 0.7rem 0.3rem; border-radius: 0 0 var(--radius) var(--radius); }
.dossier-body p { margin: 0 0 0.9rem; }
.record { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem; padding-bottom: 0.8rem; border-bottom: 1px dashed var(--line-amber); }
.record-head { display: flex; align-items: center; gap: 0.9rem; min-width: 0; }
.record-avatar { flex: none; display: inline-flex; }
.pl-avatar { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; border: 1px solid var(--line-amber); background: var(--bg-2); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.08); }
.pl-avatar-fb { display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.5rem; color: var(--amber); }

/* ── A PLAYER'S FACE, everywhere one is named (`playerFace.js`) ─────────────────────────────────
   ONE BOX, TWO CONTENTS. A linked account's picture is an <img> and an unlinked player is a <span>
   holding their initial, and they are deliberately the same element in the same place at the same
   size — the owner's ask ends *"a at je to takhle vsude a spravne zarovnany vse"*, and a list where
   some rows carry a 1.6rem picture and others carry nothing has two left edges.

   ⚠ **THE BASE IS `:where()`, WHICH IS NOT A STYLE CHOICE.** `.ssa-face` and `.pod-face` are both
   one class, so equal specificity is settled by ORDER — and this file already has the record of what
   that costs: the field console's control floor was written as `input[type="text"]`, tied with
   `.search input`, sat below it, and gave every search box on the site a second border. Inside
   `:where()` a selector weighs nothing, so every component below wins wherever it is written and a
   future reorder cannot change the page.

   The sizes are the SQUAD CREST's sizes, name for name, because that is what "aligned" means here:
   a list of players and a list of squads have to line up. 1.6rem = `.sq-row-face`, 2.2rem =
   `.sq-head-face`, 56px = `.sq-avatar` / `.pl-avatar`. A face is CLIPPED TO A CIRCLE and a crest is
   not — a badge's corners are part of the shape a squad chose, a photograph's are not. */
:where(.ssa-face) {
  width: 1.6rem; height: 1.6rem; flex: none; box-sizing: border-box;
  border-radius: 50%; overflow: hidden; object-fit: cover;
  display: inline-grid; place-items: center; vertical-align: middle;
  background: var(--bg-2); border: 1px solid var(--line);
  font-family: var(--mono); font-weight: 700; font-size: 0.78rem; line-height: 1;
  color: var(--amber); text-transform: uppercase; user-select: none;
}
/* The 56px one carries a letter the size of the old `.pl-avatar-fb`; it is stated on the pair rather
   than on the span alone so the two states cannot drift apart. */
.ssa-face.pl-avatar { font-size: 1.5rem; }
/* A row in a ranking, a squad member, a category leader — the squad list's own size. */
.pl-row-face, .sm-face, .cl-face, .vs-face { width: 1.6rem; height: 1.6rem; }
/* Inline, inside a sentence or a table cell: smaller, and with room after it. */
.kf-face { width: 1.15rem; height: 1.15rem; font-size: 0.6rem; margin-right: 0.3rem; }
.ev-face { width: 1.3rem; height: 1.3rem; font-size: 0.65rem; margin-right: 0.4rem; }
.op-face { width: 1.3rem; height: 1.3rem; font-size: 0.65rem; }
/* `.cl-line` aligns its two ends on the BASELINE so a name and a figure sit on one line; a round
   picture has no baseline worth honouring, so it centres itself against them. */
.cl-line .ssa-face { align-self: center; }
.vs-head .vs-n { display: inline-flex; align-items: center; gap: 0.4rem; min-width: 0; }
.vs-head .vs-b { justify-content: flex-end; }

/* ── Leaderboard podium (top 3) — sits above the existing table, same medal language,
   built entirely from theme tokens so the admin's colour customiser reskins it too. ── */
.podium { display: grid; grid-template-columns: 1fr 1.15fr 1fr; align-items: end; gap: 0.7rem; margin: 0.1rem 0 1.1rem; }
.pod { display: flex; flex-direction: column; align-items: center; text-align: center; min-width: 0; }
/* ── The podium ARRIVES, in place order ─────────────────────────────────────────────────────────
   In the site's own vocabulary (`viewIn`, `topIn`, `modalIn` further down) rather than a new one:
   a short rise with a soft ease, finite, and killed outright by the reduced-motion rule this sheet
   already carries for everything. The winner lands first and the other two follow, which is the
   order a reader looks at them in — a simultaneous fade says nothing about who won.

   TRANSFORM AND OPACITY ONLY. The render rig reads computed colours, and a colour caught mid-
   transition is a blend of two states that belongs to neither; this sheet has already failed a
   contrast rule that way once. Neither property here is a colour.

   It keeps its `opacity: 0` and its `both`, and the note beside `topIn` below says why that is a
   different question here: a podium place is a decoration inside a screen a reader has already
   reached, not the bar they reach it WITH. */
.podium .pod { animation: podIn 0.42s cubic-bezier(0.2, 0.7, 0.3, 1) both; }
.podium .p1 { animation-delay: 0s; }
.podium .p2 { animation-delay: 0.07s; }
.podium .p3 { animation-delay: 0.14s; }
@keyframes podIn { from { opacity: 0; transform: translateY(10px) scale(0.985); } to { opacity: 1; transform: none; } }

/* A row answers the pointer by MOVING, not by changing colour — the one hover change this sheet's
   own contrast rule cannot be tripped by, since it states neither half of a colour pair. */
.squad-row, .lb-row { transition: transform 0.14s ease; }
.squad-row:hover, .lb-row:hover { transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) { .squad-row:hover, .lb-row:hover { transform: none; } }
.pod-empty { visibility: hidden; }
.podium-title { grid-column: 1 / -1; text-align: center; font-family: var(--ui); font-weight: 700;
  font-size: 1.2rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--amber);
  margin: 2rem 0; }
.pod-av { position: relative; margin-bottom: 0.6rem; }
.pod-face { width: 62px; height: 62px; border-radius: 50%; display: grid; place-items: center; overflow: hidden;
  font-family: var(--mono); font-size: 1.45rem; color: var(--text); background: var(--bg-2); border: 2px solid var(--line); }
img.pod-face { object-fit: cover; }
.pod-face.pod-shield { color: var(--amber); }
/* A SQUAD'S OWN CREST, composited in the page from the game's own masks (see `squadEmblem.js` /
   app.js). It is a canvas until it has been painted and an <img> of the cached result afterwards,
   so both wear `.pod-face` and both are sized by it — what this adds is the two things a crest
   needs that a round avatar does not: it is a BADGE, not a face, so it is not clipped to a circle,
   and it must not inherit the plate behind an initial. */
.pod-face.sq-emblem { border-radius: var(--radius); background: transparent; object-fit: contain; }
/* The small one, on a row in the squad list. Sized against the row's text rather than the podium's
   face so a list of forty squads does not turn into a wall of badges. */
.sq-row-face { width: 1.6rem; height: 1.6rem; flex: none; }
/* Beside a squad's name in a card heading: bigger than a list row, smaller than the page's own. */
.sq-head-face { width: 2.2rem; height: 2.2rem; flex: none; }
/* On the squad's own page the crest stands exactly where a player's face stands: first in the
   record head, the label and the name beside it. Same size as `.pl-avatar`, and not clipped to a
   circle — a crest is a badge, and its corners are part of the shape the squad chose. */
.sq-avatar { width: 56px; height: 56px; flex: none; border-radius: var(--radius);
  border: 1px solid var(--line-amber); background: var(--bg-2);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.08); }
.pod-face.pod-shield svg { width: 28px; height: 28px; }
.pod.p1 .pod-face { width: 88px; height: 88px; font-size: 1.9rem; border-color: var(--gold);
  box-shadow: 0 0 0 4px rgba(217,178,74,0.12), 0 0 22px rgba(217,178,74,0.24); }
.pod.p1 .pod-face.pod-shield svg { width: 40px; height: 40px; }
.pod.p2 .pod-face { border-color: var(--silver); box-shadow: 0 0 0 3px rgba(184,188,194,0.10); }
.pod.p3 .pod-face { border-color: var(--bronze); box-shadow: 0 0 0 3px rgba(194,129,74,0.10); }
.pod-medal { position: absolute; bottom: -5px; right: -5px; width: 23px; height: 23px; border-radius: 50%;
  display: grid; place-items: center; font-family: var(--mono); font-weight: 700; font-size: 0.76rem;
  color: var(--on-amber); border: 2px solid var(--bg); }
.pod.p1 .pod-medal { background: var(--gold); } .pod.p2 .pod-medal { background: var(--silver); } .pod.p3 .pod-medal { background: var(--bronze); }
.pod-name { font-family: var(--ui); font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem;
  max-width: 15ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.pod-name:hover { color: var(--amber-hi); }
.pod-block { width: 100%; border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: linear-gradient(var(--panel-2), var(--panel)); border: 1px solid var(--line); border-bottom: 0;
  padding: 0.8rem 0.4rem 0.9rem; position: relative; }
.pod-block::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; border-radius: var(--radius-md) var(--radius-md) 0 0; }
.pod.p1 .pod-block { min-height: 148px; padding-top: 1.5rem; } .pod.p2 .pod-block { min-height: 108px; } .pod.p3 .pod-block { min-height: 82px; }
.pod.p1 .pod-block::before { background: var(--gold); box-shadow: 0 0 12px rgba(217,178,74,0.45); }
.pod.p2 .pod-block::before { background: var(--silver); } .pod.p3 .pod-block::before { background: var(--bronze); }
.pod-val { font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text); font-size: 1.5rem; line-height: 1; display: block; }
.pod.p1 .pod-val { font-size: 2rem; color: var(--amber-hi); }
/* ⚠ `overflow-wrap: anywhere` because ONE WORD CANNOT WRAP and this column is about 95px wide on a
   phone. The unit is the ranking's own word — "kills", "score", "deaths" — and in English every one
   of them is short enough that nothing here was ever under pressure. French says "éliminations",
   uppercased and letter-spaced to roughly 138px: a single token, wider than its column, with
   `white-space: normal` doing exactly what it is supposed to and nothing to break at. It was cut by
   18px on the podium of the leaderboards screen, and it went unseen because the four languages the
   dense screens were rendered in were picked by instinct and French was not one of them. */
.pod-unit { display: block; font-family: var(--mono); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-top: 0.4rem;
  overflow-wrap: anywhere; }
.pod-sub { display: block; font-family: var(--ui); font-size: 0.72rem; color: var(--faint); margin-top: 0.35rem; }
/* compact variant inside the Overview cards */
.podium.mini { gap: 0.45rem; margin-bottom: 0.7rem; }
.podium.mini .pod-face { width: 44px; height: 44px; font-size: 1rem; }
.podium.mini .pod.p1 .pod-face { width: 54px; height: 54px; font-size: 1.2rem; }
.podium.mini .pod-block { min-height: 0 !important; padding: 0.55rem 0.3rem 0.6rem; }
.podium.mini .pod-val { font-size: 1.1rem; } .podium.mini .pod.p1 .pod-val { font-size: 1.3rem; }
.podium.mini .pod-name { font-size: 0.8rem; margin-bottom: 0.35rem; }
@media (max-width: 560px) {
  .podium { gap: 0.3rem; }
  .pod-face { width: 44px; height: 44px; font-size: 1.05rem; }
  .pod.p1 .pod-face { width: 56px; height: 56px; font-size: 1.3rem; }
  .pod-face.pod-shield svg { width: 20px; height: 20px; }
  .pod.p1 .pod-face.pod-shield svg { width: 28px; height: 28px; }
  .pod-medal { width: 18px; height: 18px; font-size: 0.64rem; bottom: -4px; right: -4px; }
  .pod-av { margin-bottom: 0.4rem; }
  .pod-name { font-size: 0.74rem; max-width: 9ch; margin-bottom: 0.35rem; }
  .pod-block { padding: 0.55rem 0.2rem 0.6rem; }
  .pod.p1 .pod-block { min-height: 96px; padding-top: 0.95rem; }
  .pod.p2 .pod-block { min-height: 72px; }
  .pod.p3 .pod-block { min-height: 56px; }
  .pod-val { font-size: 1.1rem; }
  .pod.p1 .pod-val { font-size: 1.35rem; }
  .pod-unit { font-size: 0.72rem; letter-spacing: 0.04em; margin-top: 0.3rem; }
  .pod-sub { font-size: 0.66rem; }
  .podium-title { font-size: 1rem; margin: 1.4rem 0; }
}
.record-id { display: flex; flex-direction: column; min-width: 0; }
.record-label { font-family: var(--mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.2em; color: var(--faint); }
/* ⚠ **THIS ONE WAS CUT DOWNWARDS, WHICH NO WIDTH RULE CAN SEE.** It has no `nowrap`, so a long
   name already wrapped — and `overflow: hidden` then hid the second line, on the heading that
   names whose page this is. An ellipsis cannot even appear there; the words are simply gone. */
.record-name { font-size: 1.55rem; font-weight: 700; line-height: 1.15; }
.record-meta { font-size: 0.78rem; color: var(--muted); margin-top: 0.15rem; }
.dossier-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; margin-top: 0.4rem; }
@media (max-width: 760px) { .dossier-grid { grid-template-columns: 1fr; } }
.dstats { display: grid; grid-template-columns: 1fr 1fr; gap: 0.55rem; }
.ds { display: flex; align-items: center; gap: 0.65rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.65rem 0.8rem; }
.ds .ico { color: var(--amber); font-size: 1.35rem; }
.ds-txt { display: flex; flex-direction: column; line-height: 1.1; min-width: 0; }
/* A game code has no space and no hyphen in it, so it cannot wrap: at a 310px viewport
   `Cook_Dish_Cake_ApplePie` sets the row's min-content width and the whole record card scrolls
   sideways by 8px. Nothing declared is wrong — this is content pushing a box, which `check-mobile`
   reads declarations and cannot see. The recipe domain is where the codes got long enough to show
   it; the fix belongs to every value box, not to that one row. */
.ds-v { font-family: var(--mono); font-size: 1.15rem; overflow-wrap: anywhere; }
.ds-k { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.ranks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
/* Category group headers inside grouped ranking / category-leader lists */
.grp-h { list-style: none; font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--amber); font-weight: 700; padding: 0.45rem 0 0.1rem; margin-top: 0.35rem; border-top: 1px solid var(--line); }
.grp-h:first-child { border-top: 0; margin-top: 0; padding-top: 0; }
.cl-grp-h { grid-column: 1 / -1; }
.rank-chip { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.55rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.rank-pos { font-family: var(--mono); color: var(--amber-hi); font-size: 0.95rem; min-width: 2.4rem; }
.rank-pos.g { color: var(--gold); } .rank-pos.s { color: var(--silver); } .rank-pos.b { color: var(--bronze); }
.rank-overall { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0.65rem; margin-bottom: 0.5rem; border: 1px solid var(--amber); border-radius: var(--radius); background: rgba(var(--accent-rgb),0.08); }
.rank-overall .rank-label { flex: 1; font-weight: 600; }
.rank-overall .rank-val { font-family: var(--mono); color: var(--amber-hi); }
.rank-overall .rank-pos { font-weight: 700; min-width: auto; }
.record-overall { display: inline-flex; align-items: center; gap: 0.35rem; margin-top: 0.3rem; padding: 0.15rem 0.55rem; border: 1px solid var(--amber); border-radius: var(--pill); background: rgba(var(--accent-rgb),0.08); color: var(--muted); font-size: 0.8rem; width: fit-content; }
.record-overall svg { width: 0.9em; height: 0.9em; fill: currentColor; }
.record-overall .ro-rank { font-family: var(--mono); font-weight: 700; color: var(--amber-hi); }
.record-overall .ro-rank.g { color: var(--gold); } .record-overall .ro-rank.s { color: var(--silver); } .record-overall .ro-rank.b { color: var(--bronze); }
.rank-label { font-size: 0.84rem; flex: 1 1 auto; min-width: 6ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rank-val { font-family: var(--mono); color: var(--muted); font-size: 0.8rem; flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── Switches / form ────────────────────────────────────────────────────── */
.notif { display: flex; flex-direction: column; gap: 0.55rem; }
/* Raid sub-switches: indented under their master switch, with a rule that makes the nesting
   read as "part of the above" rather than as three more independent options. */
.notif-sub {
  display: flex; flex-direction: column; gap: 0.5rem;
  margin: -0.1rem 0 0.15rem 0.6rem; padding-left: 0.9rem;
  border-left: 2px solid rgba(255, 255, 255, 0.12);
}
.notif-sub.is-off { opacity: 0.45; }
.notif-sub.is-off .switch { cursor: default; }

.switch { display: flex; align-items: center; gap: 0.6rem; cursor: pointer; user-select: none; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch .track { position: relative; width: 38px; height: 20px; flex: none; background: var(--inset); border: 1px solid var(--line); border-radius: var(--pill); transition: background .15s, border-color .15s; }
.switch .track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--faint); transition: transform .15s, background .15s; }
.switch input:checked + .track { background: rgba(var(--accent-rgb),0.25); border-color: var(--line-amber); }
.switch input:checked + .track::after { transform: translateX(18px); background: var(--amber); }
.switch input:focus-visible + .track { outline: 2px solid var(--amber); outline-offset: 2px; }
.switch .lbl { font-size: 0.9rem; }
.scope { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; color: var(--muted); margin-top: 0.6rem; }
/* The option text was set well below the switch labels around it and read as an afterthought;
   it is a real choice, so it gets the same weight as the rest of the form. */
.scope select {
  /* `background-color`, not the shorthand — see `.items-toolbar select`. */
  background-color: var(--inset); color: var(--text); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.55rem 0.6rem; font-family: var(--ui); font-size: 0.95rem; line-height: 1.3;
}
.scope select option { font-size: 0.95rem; }
/* The one line under a control that says what it does. Below the select rather than beside it, so
   it wraps instead of squeezing the control on a phone — this is the narrow surface, and a hint
   that costs its own control half its width is worse than no hint. */
.notif-hint { font-size: 0.78rem; line-height: 1.45; color: var(--muted); }
.notif-actions { display: flex; align-items: center; gap: 0.7rem; margin-top: 0.3rem; }
.save-msg { font-size: 0.82rem; color: var(--online); }
/* A REFUSAL PAINTED IN THE SUCCESS COLOUR READS AS A SUCCESS. `.save-msg` is the one slot this
   form has and it is green, so "Save failed" and "nothing was saved" arrived looking exactly like
   "Saved ✓" — the sentence was right and the colour said the opposite. One modifier, set by
   whoever writes the sentence. */
.save-msg.bad { color: var(--alarm); }
/* The blanks note. `.notif-flagnote` is the shape; the rule it states is a warning rather than a
   fact about the server, so it is the alarm edge rather than the amber one. */
.notif-failnote { border-color: var(--alarm); }
.notif-failnote .ico { color: var(--alarm); }

/* ── Bunkers ────────────────────────────────────────────────────────────── */
/* Four bunker cards per row on desktop (2 on tablets, 1 on phones); the photo
   shows WHOLE at its natural aspect ratio instead of a fixed cropped strip. */
.bunker-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.9rem; }
@media (max-width: 1100px) { .bunker-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .bunker-grid { grid-template-columns: 1fr; } }
.bunker { display: flex; flex-direction: column; gap: 0.5rem; padding: 1.15rem 1.25rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); border-left: 3px solid var(--faint); overflow: hidden; }
/* Uniform image height via aspect-ratio (not a fixed px strip): every card
   matches its neighbours and the crop scales with the column width. */
.bunker-img { display: block; width: calc(100% + 2.5rem); margin: -1.15rem -1.25rem 0.5rem; height: auto; aspect-ratio: 16 / 10; object-fit: cover; border-bottom: 1px solid var(--line); }
.bunker.active { border-left-color: var(--online); }
.bunker.locked { border-left-color: var(--standby); }
.bunker-top { display: flex; align-items: center; justify-content: space-between; }
.bunker-sector { font-family: var(--mono); font-size: 1.5rem; font-weight: 700; }
.badge { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.1em; padding: 0.15rem 0.45rem; border-radius: var(--pill); border: 1px solid var(--line); }
.badge.active { color: var(--online); border-color: var(--online); }
.badge.locked { color: var(--standby); border-color: var(--standby); }
/* A bunker the server has not reported yet. It reads as neither open nor shut on purpose:
   the muted step is the one this sheet already uses for "nothing is known", and painting
   it in the locked colour would be the same lie the badge itself used to tell. */
.badge.unknown { color: var(--muted); border-color: var(--line-2); }
.bunker-eta { font-size: 0.9rem; color: var(--muted); }
.bunker-eta b { color: var(--text); font-family: var(--mono); }
.bunker-link { display: inline-flex; align-items: center; gap: 0.4rem; margin-top: 0.3rem; padding: 0.5rem 0.7rem; border: 1px solid var(--line-amber); border-radius: var(--radius); background: var(--panel-2); color: var(--amber); font-size: 0.82rem; font-weight: 600; align-self: flex-start; }
.bunker-link:hover { color: var(--amber-hi); border-color: var(--amber); }
.bunker-link .ico { font-size: 0.95rem; }
.bunker-link .ico:last-child { font-size: 0.8rem; color: var(--muted); }

/* ── Economy ────────────────────────────────────────────────────────────── */

/* ── Kill feed ──────────────────────────────────────────────────────────── */
.killfeed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.kf { display: flex; align-items: center; gap: 0.7rem; padding: 0.55rem 0.55rem; border-bottom: 1px solid var(--line); }
.kf:hover { background: rgba(var(--accent-rgb),0.04); }
.kf-wimg, .kf-wrap { width: 42px; height: 42px; flex: none; box-sizing: border-box; object-fit: contain; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 3px; display: flex; align-items: center; justify-content: center; }
.kf-wclick { cursor: pointer; transition: border-color 0.12s; }
.kf-wclick:hover { border-color: var(--line-amber); }
.kf-wrap .ico { color: var(--amber); font-size: 1.25rem; }
.kf-suicide .kf-wrap .ico { color: var(--muted); }
.kf-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.12rem; }
.kf-line { line-height: 1.25; }
.kf-killer { color: var(--text); font-weight: 600; }
.kf-arrow { color: var(--faint); margin: 0 0.15rem; }
.kf-victim { color: var(--alarm); font-weight: 600; }
.kf-sub { font-size: 0.74rem; color: var(--muted); font-family: var(--mono); }
.kf-weapon { color: var(--amber); cursor: pointer; }
.kf-weapon:hover { text-decoration: underline; }
.kf-time { font-size: 0.72rem; color: var(--faint); font-family: var(--mono); white-space: nowrap; align-self: center; }

/* ── Buttons ────────────────────────────────────────────────────────────── */
/* Filled buttons are often <a> elements — every hover must re-assert `color`,
   or the global `a:hover { color: amber-hi }` wins on the color property and the
   label melts into the amber fill. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; cursor: pointer; font-family: var(--ui); font-weight: 600; font-size: 0.9rem; padding: 0.55rem 0.95rem; border-radius: var(--radius); border: 1px solid var(--line-amber); background: var(--amber); color: var(--on-amber); }
.btn:hover { background: var(--amber-hi); color: var(--on-amber); }
.btn-ghost { background: transparent; color: var(--muted); border-color: var(--line); }
.btn-ghost:hover { color: var(--text); background: var(--panel-2); }
.btn-icon { padding: 0.45rem; }
.btn-discord { background: var(--discord); color: #fff; border-color: var(--discord); align-self: flex-start; }
.btn-discord:hover { background: var(--discord-dn); border-color: var(--discord-dn); color: #fff; }

/* ── Trader funds ───────────────────────────────────────────────────────── */
/* min(100%, 300px), never a bare 300px. `check-mobile` reads DECLARATIONS and its threshold is
   320px, so a 300px minimum passes it — and then overflows a 320px phone anyway, because the panel
   this grid sits in has 1.2rem of padding on each side that no declaration mentions. At a true
   310px viewport the page scrolled sideways by 19px and this card by 33. Only a rendered page can
   see that, and it took one. */
.econ-traders { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 0.8rem; margin-bottom: 0.4rem; }
.trader-card { padding: 1rem 1.15rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.trader-card.has-img { overflow: hidden; }
.outpost-img { display: block; width: calc(100% + 2.3rem); margin: -1rem -1.15rem 0.8rem; height: auto; border-bottom: 1px solid var(--line-amber); }
.trader-loc { font-size: 1.2rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--amber); margin-bottom: 0.7rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--line-amber); }
.tc-sub { display: block; font-size: 0.85rem; font-weight: 600; color: var(--amber-hi); font-family: var(--mono); text-transform: none; letter-spacing: 0; margin-top: 0.3rem; }
.trader-row { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; font-size: 1rem; padding: 0.32rem 0; }
/* ── The trader's own picture (`traderImg`, resolved by `items.traderTypeImage`) ─────────────────
   SCUM ships one 256px icon per trader kind and this product drew none of them. It is a BADGE
   rather than a face, so it keeps its corners — the same distinction `.sq-avatar` makes against
   `.pl-avatar` — and it is `object-fit: contain` because the art is a square drawing rather than a
   photograph to crop.
   `align-self: center` is not decoration: this row aligns on the BASELINE so the name and the
   figure sit on one line, and an image's baseline is its bottom edge — without it the picture would
   hang the whole row off its own foot, which is the note `.pd-crest` in the panel already carries.
   The NAME takes the free space so the figure stays hard right; `space-between` then has nothing
   left to distribute, which is what keeps a row with a picture and a row without it identical. */
.trader-face { width: 1.7rem; height: 1.7rem; flex: none; align-self: center; box-sizing: border-box;
  border-radius: var(--radius); object-fit: contain; background: var(--bg-2); border: 1px solid var(--line); }
.trader-face-fb { display: inline-grid; place-items: center; color: var(--amber); }
.trader-face-fb .ico { width: 0.95rem; height: 0.95rem; }
/* The picture, the trader's name, a sum of money and a count of trades on one line does not fit:
   measured at a true 310px, `Comerciante del búnker` rendered in 75px with 43% of it on screen,
   and on a DESKTOP `General Goods` was cut at 120px because the outpost cards sit two to a row.
   So the name wraps at every width — it is what the row identifies — and only the figures move
   to their own line on a phone. */
.trader-name { flex: 1 1 auto; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
@media (max-width: 560px) {
  .trader-row { flex-wrap: wrap; row-gap: 0.15rem; }
  /* The name stays BESIDE the badge and is allowed to run to a second line; only the figures move
     down. Sending the name to its own line instead would leave the picture sitting alone on the
     first one, which is a worse row than the one being fixed. */
  .trader-name {
    flex: 1 1 auto; min-width: 8ch;
    white-space: normal; overflow: visible; text-overflow: clip;
  }
  .trader-rv { flex: 1 1 100%; align-items: flex-start; }
}
.trader-row + .trader-row { border-top: 1px solid var(--line); }
.trader-row .v { font-family: var(--mono); font-size: 1.1rem; color: var(--amber-hi); }
.trader-sub { margin-top: 0.7rem; padding-top: 0.5rem; border-top: 1px solid var(--line-amber); font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--amber); }
.trader-sub + .trader-row { border-top: 0; }

/* ── Footer ─────────────────────────────────────────────────────────────── */
.site-footer { border-top: 1px solid var(--line-amber); margin-top: 2rem; background: linear-gradient(180deg, var(--bg-2), var(--bg)); }
.foot-cols { width: 100%; padding: 1.5rem 2.5rem 1rem; display: grid; grid-template-columns: 2fr 1.5fr 1fr; gap: 2rem; align-items: start; }
@media (max-width: 720px) { .foot-cols { grid-template-columns: 1fr; gap: 1.1rem; } }
.foot-col { display: flex; flex-direction: column; gap: 0.4rem; }
.foot-h { display: flex; align-items: center; gap: 0.45rem; font-weight: 700; font-size: 0.85rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--amber); margin-bottom: 0.2rem; }
.foot-logo { height: 40px; }
.foot-col a { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--muted); font-size: 0.88rem; }
.foot-col a:hover { color: var(--amber-hi); }
.foot-nav { display: grid; grid-template-columns: repeat(3, max-content); gap: 0.35rem 1.4rem; }
.foot-nav a[hidden] { display: none; }
@media (max-width: 460px) { .foot-nav { grid-template-columns: repeat(2, max-content); } }
.foot-bottom { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; padding: 0.8rem 2.5rem; border-top: 1px solid var(--line); }
.foot-social { display: flex; gap: 1.2rem; }
.foot-social a { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--muted); font-size: 0.85rem; }
.foot-social a:hover { color: var(--amber-hi); }
.foot-link { background: none; border: 0; padding: 0; color: var(--amber); cursor: pointer; font: inherit; }
.foot-link:hover { color: var(--amber-hi); text-decoration: underline; }
/* App status modal */
.appstat { display: flex; flex-direction: column; margin-top: 0.6rem; }
.appstat-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.2rem; border-bottom: 1px solid var(--line); }
.appstat-row:last-child { border-bottom: 0; }
.appstat-row .ico { color: var(--amber); flex: none; }
.appstat-k { color: var(--muted); font-size: 0.85rem; }
.appstat-v { color: var(--text); font-family: var(--mono); margin-left: auto; text-align: right; }
/* Members-only gate (player profiles / squad details for logged-out visitors) */
.gate { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.7rem; padding: 1.6rem 1rem 0.8rem; }
/* One rule, and it says what the browser was already computing: the later of the two copies
   this replaces set the size and the colour, the earlier set the badge, and the element got a
   hybrid neither block described. 3rem is the size that was winning; 58px never applied. */
.gate-ico { display: inline-flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem; border-radius: 50%; background: var(--inset);
  border: 1px solid var(--line-amber); color: var(--discord); }
.gate-ico .ico { width: 1.6rem; height: 1.6rem; color: var(--amber); }
.gate-h { margin: 0; font-size: 1.1rem; color: var(--text); text-transform: none; letter-spacing: 0.02em; }
.gate-p { margin: 0; color: var(--muted); font-size: 0.92rem; max-width: 360px; line-height: 1.5; }
.gate-btn { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 0.6rem; padding: 0.65rem 1.2rem; border: 1px solid var(--line-amber); border-radius: var(--radius); background: var(--panel-2); color: var(--amber); font-weight: 600; cursor: pointer; font-family: var(--ui); font-size: 0.92rem; }
.gate-btn:hover { color: var(--amber-hi); border-color: var(--amber); }
.gate-btn .ico { flex: none; }
.gate-discord { background: var(--discord); border-color: var(--discord); color: #fff; }
.gate-discord:hover { background: var(--discord-dn); border-color: var(--discord-dn); color: #fff; }
:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }

/* Phone: raise small controls to a comfortable ~40px touch target.
   ⚠ THE REST OF THIS FLOOR IS AT THE FOOT OF THIS FILE AND HAS TO BE, and the two entries that
   used to sit here are why. `.map-zoom-b { width: 40px }` was written here and LOST to the
   component's own `width: 36px` 460 lines below: a media query does not raise specificity, so
   between two rules of weight (0,1,0) the later one wins and the zoom buttons were 36px on every
   phone this has ever run on. `.mp-close` was worse — it set 34px inside a block whose own comment
   promised 40. A floor states `min-*` and goes LAST; see `TOUCH FLOOR` at the end. */
@media (max-width: 640px) {
  .seg-btn, .lb-gpill, .etr-pg, .tab-btn { min-height: 40px; display: inline-flex; align-items: center; }
}
/* The drawer's header row (title + ×) exists only on phones. */
.drawer-head { display: none; }
.ham-backdrop[hidden] { display: none; }
body.ham-open { overflow: hidden; } /* scroll lock while the drawer is open */

/* Phone header: smaller brand on the left so the player chip (Discord login) +
   hamburger fit on one row; the language / Discord / Admin actions live in an
   off-canvas drawer sliding in from the RIGHT edge. The status readout drops to
   its own full-width row below so the server status stays visible. */
@media (max-width: 640px) {
  .mainnav { padding: 0 0.75rem; }
  .nav-btn { padding: 0.75rem 0.7rem; font-size: 0.85rem; }
  .hud-top { gap: 0.55rem 0.6rem; padding: 0.6rem 0.75rem; }
  .brand { min-width: 0; flex-shrink: 1; gap: 0.5rem; }
  .brand img { height: 32px; }
  .brand-name { font-size: 0.92rem; }
  .brand-sub { font-size: 0.55rem; letter-spacing: 0.2em; }
  .header-right { margin-left: auto; }
  .ham-btn { display: inline-flex; }
  /* Off-canvas drawer: full-height panel from the right, over a dimmed backdrop.
     Hidden via clip-path (a wipe from the right), NOT translateX: the drawer sits
     inside the sticky .topbar, and when the topbar auto-hides its transform makes
     it the drawer's containing block — a translated-off-screen drawer then counted
     as horizontal overflow and let the whole page pan to the right. A clipped
     drawer never leaves the viewport bounds, so no overflow. The open state's
     negative left inset leaves a window for the box-shadow. */
  .ham-backdrop { position: fixed; inset: 0; z-index: 99; background: rgba(0, 0, 0, 0.55); }
  .header-actions {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 100;
    flex-direction: column; align-items: stretch; gap: 0.55rem;
    width: min(78vw, 320px); padding: 0.8rem 0.9rem;
    background: var(--panel); border-left: 1px solid var(--line-amber);
    box-shadow: -14px 0 36px rgba(0, 0, 0, 0.6);
    clip-path: inset(0 0 0 100%); visibility: hidden;
    transition: clip-path 0.22s ease, visibility 0.22s;
  }
  .header-actions.open { clip-path: inset(0 0 0 -56px); visibility: visible; }
  /* The off-canvas drawer is position:fixed and must be sized to the VIEWPORT.
     Any transform on an ancestor (the topbar's scroll auto-hide, or the header's
     one-time `topIn` intro animation on .hud-top > *) would instead become the
     drawer's containing block and trap it at the header's height — collapsing it
     to just the "MENU ×" title bar and clip-path-cutting everything below. While
     the drawer is open, neutralise those ancestor transforms so it fills the
     screen and every action (language, Discord, Admin, profile) is visible. */
  body.ham-open .topbar,
  body.ham-open .hud-top > * { transform: none !important; animation: none !important; }
  .drawer-head {
    display: flex; align-items: center; justify-content: space-between;
    padding-bottom: 0.55rem; margin-bottom: 0.2rem; border-bottom: 1px solid var(--line);
    color: var(--amber); font-weight: 700; font-size: 0.85rem; letter-spacing: 0.14em; text-transform: uppercase;
  }
  .drawer-close {
    background: none; border: 0; color: var(--muted); font-size: 1.5rem; line-height: 1;
    padding: 0 0.2rem; cursor: pointer;
  }
  .drawer-close:hover { color: var(--amber-hi); }
  /* The player chip (Discord login / profile) lives in the drawer on phones —
     the header row keeps only brand + hamburger so it always fits. */
  .header-actions .player-chip { max-width: none; }
  .header-actions .lang-select-wrap { display: flex; }
  .header-actions .lang-select { flex: 1; }
  /* Inside the drawer the language widget stacks VERTICALLY (the base .lang-select
     is a flex ROW — without this the expanded list rendered beside the button,
     squeezed to the right). The list opens in flow, full width, and pushes the
     Discord / Admin rows down instead of overflowing. */
  .header-actions .lang-dd { width: 100%; flex-direction: column; align-items: stretch; }
  .hud-top .header-actions .lang-dd-list:not([hidden]) { display: flex; }
  .header-actions .lang-dd-list { position: static; min-width: 0; width: 100%; max-height: 45vh; margin-top: 0; border: 0; border-top: 1px solid var(--line); border-radius: 0; box-shadow: none; }
  .header-actions .discord-invite, .header-actions .admin-link { justify-content: flex-start; }
  .readout { order: 3; flex: 1 0 100%; margin-left: 0; justify-content: flex-start; min-width: 0; padding: 0.4rem 0.8rem; }
  .readout-pop { min-width: 0; }
}
/* Narrowest phones: drop the brand subtitle to guarantee the login chip fits. */
@media (max-width: 400px) {
  .brand-sub { display: none; }
}
/* On the narrowest screens drop the small captions but keep the count + server state. */
@media (max-width: 640px) {
  .readout-cap, .readout-sub { display: none; }
  .readout-num { font-size: 1.15rem; }
  .readout-state-wrap { padding-left: 0.75rem; }
  .readout-state { font-size: 0.78rem; }
}
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; scroll-behavior: auto; } }

/* ── Economy (Discord-aligned) ────────────────────────────────────────────── */
.econ-rotation { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.econ-rotation li { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: var(--text); }
.econ-rotation li .ico { color: var(--amber); flex: none; }
.econ-rotation b { color: var(--amber); font-family: var(--mono); }


/* ── My Stats: combat|survival grid, squad, notifications ─────────────────── */
.record-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; justify-content: flex-end; margin-top: 0.5rem; }
.ms-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.6rem; margin-top: 0.4rem; }
@media (max-width: 760px) { .ms-grid { grid-template-columns: 1fr; } }
/* Grouped stat sheet (Combat / Survival / Hunting / Crafting / Exploration / Body) */
.ms-statgroup { margin-top: 0.7rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); overflow: hidden; }
.ms-statgroup:first-child { margin-top: 0.4rem; }
.statgroup-sum { display: flex; align-items: center; gap: 0.5rem; padding: 0.7rem 0.85rem; cursor: pointer; list-style: none; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.82rem; color: var(--amber); user-select: none; }
.statgroup-sum::-webkit-details-marker { display: none; }
.statgroup-sum:hover { background: var(--hover-bg); }
.statgroup-sum .ico { color: var(--amber); width: 1.05em; height: 1.05em; }
.statgroup-title { flex: 1; }
.statgroup-chev { color: var(--muted); font-size: 0.9rem; transition: transform 0.15s; }
.ms-statgroup[open] .statgroup-chev { transform: rotate(180deg); }
.ms-statgroup .dstats { padding: 0 0.85rem 0.85rem; }
.ms-statgroup[open] .statgroup-sum { border-bottom: 1px solid var(--line); }
.ms-statgroup[open] .dstats { padding-top: 0.7rem; }
/* auto-fill (not auto-fit) keeps every card the same width — empty trailing tracks
   instead of stretching the last row, so all groups line up identically. */
.ms-statgroups .dstats { grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 0.7rem; align-items: stretch; }
/* Survival stat cards mirror the Overview category-leader cards (icon | label/value),
   plus the weekly delta on the value line. */
.ds-card { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 0.75rem; padding: 0.75rem 0.9rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.ds-card > .ico { color: var(--amber); font-size: 1.7rem; }
.ds-card .cl-cat { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-card .cl-line { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; }
.ds-card .cl-name { font-family: var(--ui); font-size: 1.15rem; font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-card .ds-delta { flex: none; margin-left: auto; }
.ds-card.ds-link { cursor: pointer; transition: border-color 0.12s, transform 0.12s; }
.ds-card.ds-link:hover { border-color: var(--line-amber); transform: translateY(-1px); }
.ds-card.ds-link .cl-cat::after { content: ' ›'; color: var(--amber); }
@media (prefers-reduced-motion: reduce) { .ds-card.ds-link:hover { transform: none; } }

/* Tabbed sections (My Stats + player modal) */
.tabbed-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 0.2rem; margin: 1rem 0 1.3rem; border-bottom: 1px solid var(--line-amber); }
.tab-btn { display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer; background: transparent; border: 0; border-bottom: 2px solid transparent; color: var(--muted); font-family: var(--ui); font-weight: 600; font-size: 0.85rem; padding: 0.6rem 0.85rem; margin-bottom: -1px; white-space: nowrap; }
.tab-btn .ico { font-size: 1.05rem; }
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--amber); border-bottom-color: var(--amber); }
/* Account actions (Unlink · Logout) sit to the RIGHT of the profile record —
   ghost buttons, out of the tab row entirely so tabs never wrap around them. */
.ms-acts { display: flex; gap: 0.45rem; flex: none; align-self: center; }
.ms-act { font-size: 0.82rem; padding: 0.45rem 0.8rem; }
.ms-act .ico { font-size: 1rem; }
#unlink-btn:hover { color: var(--alarm); border-color: var(--alarm); background: transparent; }
/* Phones: the actions drop BELOW the profile as their own full-width row —
   sitting beside it squeezed the avatar + name. Labels stay visible. */
@media (max-width: 640px) {
  .record { flex-wrap: wrap; }
  .ms-acts { flex-basis: 100%; margin-top: 0.6rem; }
  .ms-act { flex: 1; justify-content: center; }
}
.tab-pane[hidden] { display: none; }
@media (max-width: 640px) { .tabbed-nav { flex-wrap: nowrap; overflow-x: auto; } }

#me-squad { margin-top: 0.2rem; }
.squad-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem 1rem; flex-wrap: wrap; margin-bottom: 0.7rem; padding-bottom: 0.6rem; border-bottom: 1px dashed var(--line-amber); }
/* The crest and the name are one thing and belong together; the badge goes to the far end.
   `.squad-head` is space-between, and with a third child that would have put the name in the
   middle with air on both sides. */
.squad-name { font-size: 1.1rem; font-weight: 700; margin-right: auto; }
.squad-meta { display: inline-flex; align-items: center; gap: 0.9rem; }
.squad-note { margin: 0 0 0.6rem; padding: 0.65rem 0.85rem; background: var(--inset); border: 1px solid var(--line); border-left: 2px solid var(--line-amber); border-radius: var(--radius); }
.squad-note-h { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--amber); font-weight: 700; }
.squad-note-h .ico { color: var(--amber); }
.squad-private { margin-left: auto; display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.62rem; font-style: normal; text-transform: none; letter-spacing: 0; color: var(--faint); }
.squad-note-txt { margin: 0.4rem 0 0; font-size: 0.9rem; line-height: 1.45; color: var(--text); white-space: pre-wrap; word-break: break-word; }
.raid-status { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.7rem; padding: 0.6rem 0.85rem; background: var(--inset); border: 1px solid var(--line); border-left: 3px solid var(--faint); border-radius: var(--radius); font-size: 0.9rem; font-weight: 600; }
.raid-status .ico { flex: none; }
.raid-status.rs-on { border-left-color: var(--online); color: var(--online); }
.raid-status.rs-pending { border-left-color: var(--standby); color: var(--standby); }
.raid-status.rs-off { border-left-color: var(--alarm); color: var(--alarm); }

/* ── My Vehicles: full live map (ported from the admin Live Map) ──────────── */
.mv-locked { display: flex; align-items: center; gap: 0.9rem; padding: 1.1rem 1.3rem; background: rgba(var(--accent-rgb),0.08); border: 1px solid var(--line-amber); border-radius: var(--radius); color: var(--text); line-height: 1.5; }
.mv-locked .ico { width: 1.8rem; height: 1.8rem; color: var(--amber); flex: none; }

.map-wrap {
  position: relative; display: flex; margin: 0.2rem 0 0.6rem;
  border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden;
}

/* Side panel — layer toggles, options + searchable entity lists. */
.map-side {
  width: 280px; flex: 0 0 280px; display: flex; flex-direction: column;
  height: 72vh; min-height: 380px; background: var(--panel); border-right: 1px solid var(--line);
  overflow-y: auto;   /* whole panel (filters + lists) scrolls as one, like the admin map */
}
/* The lists, on the other side of the map from the controls. Stacked under the filters they pushed
   the layer toggles off screen the moment a layer had more than a handful of rows. */
.map-side-r { border-right: 0; border-left: 1px solid var(--line); }

/* Layers holding nothing, together, behind one disclosure. They keep their switches: a reader needs
   to see that Mines EXIST as a layer and hold nothing, which is a different fact from this server
   not having them. Ghost hover — no fill, the colour change is the whole affordance. */
.ml-empties { margin-top: 0.45rem; border-top: 1px solid var(--line); padding-top: 0.35rem; }
.ml-empties > summary {
  cursor: pointer; font-family: var(--ui); font-size: 0.68rem; color: var(--muted);
  padding: 0.2rem 0.4rem;
  --hov-bg: transparent; --hov-fg: var(--text);
}
.ml-empties > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }

/* Each side says what it is. With one panel there was nothing to confuse; with two, an unlabelled
   column of controls beside an unlabelled column of rows makes the reader work out which is which. */
.map-side-h {
  display: flex; align-items: center; gap: 0.45rem;
  padding: 0.55rem 0.75rem; border-bottom: 1px solid var(--line);
  background: var(--panel-2); color: var(--muted);
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  position: sticky; top: 0; z-index: 2;
}
.map-side-h .ico { width: 14px; height: 14px; color: var(--amber); flex: none; }
.map-side-h > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-side-reset { margin-left: auto; }

.map-search-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--line); }
.map-search-row .ico { width: 15px; height: 15px; color: var(--muted); flex: none; }
.map-search-row input {
  flex: 1; min-width: 0; background: none; border: none; color: var(--text);
  font-family: var(--ui); font-size: 0.85rem; padding: 0.15rem 0;
}
.map-search-row input:focus { outline: none; }

/* One player filter for the whole map — same control and shape as the admin map's, so the two
   panels read as one product. Type-to-search, because a squad can be large. */
.map-owner-row { position: relative; display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--line); }
.map-owner-row[hidden] { display: none; }
.map-owner-row .ico { width: 15px; height: 15px; color: var(--muted); flex: none; }
.map-owner-row input {
  flex: 1; min-width: 0; padding: 0.3rem 0.4rem; font-family: var(--ui); font-size: 0.8rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); color: var(--text);
}
.map-owner-row input:focus { outline: none; border-color: var(--amber); }
.map-owner-clear[hidden] { display: none; }
.map-owner-sug {
  position: absolute; z-index: 20; left: 0.75rem; right: 0.75rem; top: 100%;
  max-height: 260px; overflow-y: auto; display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line-amber, var(--line)); border-radius: var(--radius);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
}
.map-owner-sug[hidden] { display: none; }
/* Beats the global button skin, which would paint every suggestion solid amber. */
.map-owner-sug .mos-item, .map-owner-sug .mos-item:active {
  display: block; width: 100%; text-align: left; padding: 0.35rem 0.6rem; border: 0; border-radius: 0;
  background: none; color: var(--text); font: inherit; font-size: 0.8rem; letter-spacing: 0; transform: none;
}
.map-owner-sug .mos-item:hover, .map-owner-sug .mos-item.hi { background: var(--amber); color: var(--on-amber); }

.map-filters { border-bottom: 1px solid var(--line); }
.map-filters-head {
  display: flex; align-items: center; gap: 0.5rem; width: 100%;
  padding: 0.6rem 0.75rem; background: none; border: none; cursor: pointer;
  color: var(--muted); font-family: var(--ui); font-weight: 700; font-size: 0.72rem;
  text-transform: uppercase; letter-spacing: 0.08em;
}
.map-filters-head:hover { color: var(--text); background: var(--hover-bg); border-color: transparent; }
.map-filters-head .ico { width: 15px; height: 15px; flex: none; color: var(--amber); }
.mf-chev { margin-left: auto; font-size: 0.85rem; transition: transform 0.15s; }
.map-filters.collapsed .mf-chev { transform: rotate(-90deg); }
.map-filters.collapsed .map-layers { display: none; }

.map-layers { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.6rem; }
.mlyr { border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; background: var(--panel-2); }
.map-layer {
  display: flex; align-items: center; gap: 0.55rem; width: 100%; padding: 0.5rem 0.6rem;
  background: none; border: none; color: var(--text);
  font-family: var(--ui); font-weight: 600; font-size: 0.88rem; cursor: pointer; text-align: left;
}
.ml-badge {
  display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
  border-radius: var(--radius-md); flex: none; border: 1px solid rgba(0, 0, 0, 0.35);
}
.ml-badge .ico { width: 14px; height: 14px; color: var(--bg); }
/* ONE FACT, FOUR RENDERERS — and it used to be four LISTS, which is how the fourth came to be six
   entries short. A kind's colour is now written once as `--badge-bg` (see the block beside the
   marker dots further down) and read here, on the list dot, on the marker dot and on the CARD
   plate. That last one is why this matters rather than being tidiness: `.mp-ico .ico` is painted
   `var(--bg)`, so a kind missing from the card's list drew a near-black glyph on a near-black
   panel at 1.06:1 — invisible, laying out perfectly, with a sprite id that resolves and no error
   anywhere. Six kinds were in that state on this page, two of them unconditionally, and the admin
   panel had already been through the identical failure nineteen icons at a time. Adding a kind is
   now one line that cannot be half-done. */
.ml-badge { background: var(--badge-bg, transparent); }
.map-layer .ml-name { flex: 1; }
.map-layer:hover { background: var(--hover-bg); color: var(--text); }
.map-count {
  min-width: 1.6em; padding: 0.05rem 0.45em; text-align: center; border-radius: var(--pill);
  background: rgba(0, 0, 0, 0.3); color: var(--muted); font-size: 0.75rem; font-variant-numeric: tabular-nums;
}
.map-layer.active .map-count { color: var(--text); }
.mlyr:has(.map-layer:not(.active)) { opacity: 0.55; }
.map-layer:not(.active) .ml-badge { filter: grayscale(1); }
.map-layer:not(.active) .ml-name { text-decoration: line-through; text-decoration-color: var(--muted); }

/* Three toggles plus the reset button did not fit on one line and ran into each other. Wrap,
   with the reset button pushed to its own end, so nothing is ever clipped at narrow widths. */
.map-opts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.85rem;
  padding: 0.45rem 0.75rem 0.6rem; border-bottom: 1px solid var(--line);
}
.map-opts .map-reset { margin-left: auto; }
/* ⚠ THESE ARE BUTTONS AND THE CHECKBOX IS STILL INSIDE EACH ONE. The owner rejected the bare
   checkbox this shipped as — *"nejaky prepinaci tlacitko"* — and the admin map already had the
   answer: hide the native tick and make the whole cell the tick. The input keeps the click, the
   keyboard focus, the checked state and the screen reader's word for it.

   They lay out as a wrapping row rather than a fixed grid, because a grid breaks in German the
   moment "Sectors" becomes "Sektoren" — a layout that only works in the language it was designed
   in, on a surface that renders in seventeen. Each takes what its label needs and the slack is
   shared. */
.map-opts-top {
  display: flex; align-items: stretch; flex-wrap: wrap;
  gap: 0.4rem; padding: 0.5rem 0.75rem;
}
.map-opts-top .map-toggle {
  flex: 1 0 auto;   /* grow into the slack, never shrink below the word */
  justify-content: center; gap: 0; padding: 0.34rem 0.45rem; min-width: 0;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius);
  --hov-bg: var(--hover-bg); --hov-fg: var(--text);
}
/* Only its 14 px of box is reclaimed — with the tick in the cell, three of these in a 280 px panel
   left about 55 px of text each and every label rendered as "Na…", "Sec…", "In vi…". */
.map-opts-top .map-toggle input {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.map-opts-top .map-toggle > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-opts-top .map-toggle:has(:checked) {
  border-color: var(--amber); background: rgba(var(--accent-rgb), 0.12); color: var(--amber-hi);
}
.map-opts-top .map-toggle:hover { background: var(--hov-bg); color: var(--hov-fg); }
.map-opts-top .map-toggle:has(:checked):hover { border-color: var(--amber-hi); }
/* Keyboard users get the focus ring the hidden input would have shown. */
.map-opts-top .map-toggle:focus-within { outline: 2px solid var(--amber); outline-offset: 1px; }
.map-toggle { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); font-size: 0.8rem; cursor: pointer; white-space: nowrap; }
.map-toggle input { accent-color: var(--amber); }
.map-zoom-btn {
  display: inline-flex; margin-left: auto; padding: 0.3rem; background: var(--panel-2); border: 1px solid var(--line);
  color: var(--muted); border-radius: var(--radius-md); cursor: pointer;
}
.map-zoom-btn:hover { color: var(--amber-hi); border-color: var(--line-amber); background: var(--panel-2); }
.map-zoom-btn .ico { width: 15px; height: 15px; }

/* WHY PART OF THIS MAP IS NOT HERE. Not an error colour: nothing has gone wrong, part of the map
   simply comes from a server that is not answering. An alarm here would have a player reporting a
   fault every time an owner restarts. */
.map-note {
  margin: 0.45rem 0.75rem 0; padding: 0.4rem 0.55rem;
  border: 1px solid var(--line); border-left: 2px solid var(--amber); border-radius: var(--radius);
  background: var(--panel-2); color: var(--muted);
  font-size: 0.72rem; line-height: 1.45;
}
.map-lists { flex: 0 0 auto; padding: 0 0 0.6rem; }
.ml-sec { margin-bottom: 0.3rem; }
/* A <button> now (it collapses its section), so every state has to be stated explicitly or the
   global button skin paints it solid amber. Same affordance and look as the admin map. */
.ml-sec-h, .ml-sec-h:hover, .ml-sec-h:active {
  display: flex; align-items: center; gap: 0.4rem; width: 100%; padding: 0.5rem 0.6rem;
  position: sticky; top: 0; z-index: 2; cursor: pointer; user-select: none; text-align: left;
  background: var(--panel); border: 0; border-bottom: 1px solid var(--line); border-radius: 0; transform: none;
  color: var(--text); font-family: inherit; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
}
.ml-sec-h::after {
  content: '▾'; display: inline-block; flex: none; margin-left: 0.5rem;
  color: var(--muted); font-size: 0.7rem; transition: transform .15s ease, color .12s ease;
}
.ml-sec-h:hover { color: var(--amber-hi); }
.ml-sec-h:hover::after, .ml-sec-h:hover .ico { color: var(--amber-hi); }
.ml-sec.collapsed .ml-sec-h::after { transform: rotate(-90deg); }
.ml-sec.collapsed .ml-row { display: none; }
.ml-sec-h .ico { width: 13px; height: 13px; color: var(--amber-hi); }
.ml-sec-n {
  margin-left: auto; min-width: 1.6em; padding: 0.05rem 0.45em; text-align: center;
  border-radius: var(--pill); background: rgba(0, 0, 0, 0.3); color: var(--muted);
  font-size: 0.75rem; font-variant-numeric: tabular-nums;
}
/* What "In view" took out of this section. It is a sentence rather than a row: nothing here can be
   clicked, and it must not read as a squadmate whose name failed to load. */
.ml-outside {
  padding: 0.3rem 0.6rem 0.45rem; color: var(--muted); font-size: 0.75rem; line-height: 1.35;
}
.ml-row {
  display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.4rem 0.6rem;
  background: none; border: none; cursor: pointer; text-align: left; color: var(--text);
}
.ml-row:hover { background: var(--hover-bg); color: var(--text); }
.ml-row .ml-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; border: 1px solid rgba(0, 0, 0, 0.5); }
.mlr-main { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
/* The map's own side list. Both lines wrap: a vehicle carries its squad in brackets after its
   name and the state line is a sentence with separators in it, so in a long language they were
   cut to three quarters and a half of themselves. The list scrolls vertically already. */
.mlr-name { font-size: 0.82rem; white-space: normal; overflow-wrap: anywhere; }
.mlr-meta { font-size: 0.7rem; color: var(--muted); white-space: normal; overflow-wrap: anywhere; }
.mlr-sec { font-size: 0.72rem; color: var(--muted); font-variant-numeric: tabular-nums; padding: 0 0.35em; border: 1px solid var(--line); border-radius: var(--radius); align-self: center; }
.ml-empty { padding: 1rem; text-align: center; color: var(--muted); font-size: 0.85rem; }
.ml-dot { background: var(--badge-bg, transparent); }

.map-subf { padding: 0.4rem 0.5rem 0.5rem; border-top: 1px solid var(--line); }
.sf-select {
  width: 100%; padding: 0.4rem 1.6rem 0.4rem 0.55rem; background: var(--panel-2); border: 1px solid var(--line);
  color: var(--text); font-family: var(--ui); font-weight: 600; font-size: 0.78rem; border-radius: var(--radius-md); cursor: pointer;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ff9a2e' stroke-width='2.5'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 0.8rem center;
}
/* The chevron itself now comes from the control floor at the top of this file, which covers every
   dropdown on the site rather than the three somebody remembered. What is left here is the ROOM for
   it: both of these state their own `padding`, and the floor's `padding-right` loses to that. */
select#lb-category, .scope select {
  padding-right: 2.3rem;
}
.sf-select:hover, .sf-select:focus { border-color: var(--line-amber); outline: none; }
.sf-select option { background: var(--panel); color: var(--text); }
.map-mk.mk-player .mk-dot { box-shadow: 0 0 0 2px rgba(53, 208, 127, 0.4), 0 1px 3px rgba(0, 0, 0, 0.6); }

/* Sector grid — inside the canvas so it pans/zooms with the map. */
.map-sectors { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.map-sectors.hidden { display: none; }
.sec-line { position: absolute; background: rgba(255, 200, 120, 0.14); }
/* ⚠ **THE LINE IS COUNTER-SCALED AND THE LABEL BESIDE IT ALWAYS WAS.** The canvas is painted under
   `transform: scale()` that reaches 8 on the panel and 26 on a phone, so a bare `1px` here is an
   8-to-26px amber band straddling exactly the sector boundary — and what a reader sees at that zoom
   is a fat smear over the join between two map tiles, which is how it was reported: *"when I zoom
   into a place and it is between sectors it is badly joined"*. `--mz` is `1 / scale`, set on the
   canvas by the same code that sets the transform, and `.sec-lab` right below has used it since it
   was written. A grid line is a screen-space hairline, never a world-space object. */
.sec-v { top: 0; bottom: 0; width: max(0.5px, calc(1px * var(--mz, 1))); }
.sec-h { left: 0; right: 0; height: max(0.5px, calc(1px * var(--mz, 1))); }
/* Smaller than the admin map's 22px: the frontend map viewport is much narrower, so
   fixed screen-size labels would otherwise sprawl across the whole island. */
.sec-lab {
  position: absolute; transform: scale(var(--mz, 1)); transform-origin: top left;
  padding: 2px 4px; color: rgba(255, 255, 255, 0.82); font-family: var(--ui); font-weight: 700; font-size: 12px;
  letter-spacing: 0.04em; white-space: nowrap;
  text-shadow: 0 0 3px #000, 1px 1px 2px #000, -1px 1px 2px #000;
}

.map-viewport {
  position: relative; flex: 1; min-width: 0; height: 72vh; min-height: 380px; overflow: hidden; cursor: grab;
  background: #070a10; touch-action: none; user-select: none;
}
@media (max-width: 760px) {
  .map-wrap { flex-direction: column; margin: 0.4rem 0; }
  /* No height cap on the side panel: with max-height it couldn't scroll, so an
     expanded "Layers & filters" overflowed it and the map canvas below (its
     transform creates a stacking context) painted OVER the filters. The panel
     now grows to fit; only the entity lists scroll. */
  .map-side { width: auto; flex: none; height: auto; min-height: 0; max-height: none; border-right: none; border-bottom: 1px solid var(--line); }
  /* Two panels now, and the ORDER is the point: filters, then the island, then the lists. Without
     it the flex source order puts both panels above the map and a reader scrolls past twenty layer
     rows to reach the island they came for. */
  .map-side-l { order: 1; }
  .map-viewport { order: 2; }
  .map-side-r { order: 3; border-left: none; border-bottom: none; border-top: 1px solid var(--line); }
  .map-lists { max-height: 32vh; }
  /* Square viewport so the (square) island map fills it, instead of floating in a
     too-tall 58vh box that left an empty band and pushed against the filters above. */
  .map-viewport { flex: none; height: auto; min-height: 0; aspect-ratio: 1 / 1; }
  /* Let the info popover scroll inside the map rather than being clipped by the
     viewport's overflow when a container's contents list is long. */
  .map-popover { max-height: calc(100% - 16px); overflow-y: auto; touch-action: pan-y; overscroll-behavior: contain; }
  .mp-contents { max-height: none; }
}
.map-viewport.dragging { cursor: grabbing; }
/* On-screen zoom buttons (touch has no wheel). */
.map-zoom { position: absolute; right: 10px; bottom: 10px; z-index: 15; display: flex; flex-direction: column; gap: 5px; }
.map-zoom-b {
  width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem; line-height: 1; background: var(--panel); border: 1px solid var(--line-amber);
  color: var(--amber-hi); border-radius: var(--radius-md); cursor: pointer; user-select: none; padding: 0;
}
.map-zoom-b:hover { background: var(--panel-2); }
.map-zoom-b:active { background: var(--amber); color: var(--on-amber); }
/* THE CANVAS IS SQUARE BEFORE THE PICTURE ARRIVES, NOT AFTER.
   It is `width: 100%` with no height of its own, so its height came entirely from the island
   picture inside it — and every marker is placed as `top: N%` OF THIS BOX. Until that picture
   decoded the box was zero pixels tall, so all of them resolved to the same line and then flew
   apart when the image landed. This is the page a stranger opens on a phone, where that wait is
   longest. `aspect-ratio` is not a guess about the artwork: the world span is identical on both
   axes, the sector grid cuts the box into 20% cells in BOTH directions, and the pyramid's z0 is
   one square tile — every placement here already assumes a square, and this is that assumption
   written where the browser can act on it before the first byte arrives. */
.map-canvas { position: absolute; top: 0; left: 0; width: 100%; aspect-ratio: 1 / 1; transform-origin: 0 0; }
.map-img { display: block; width: 100%; height: auto; user-select: none; -webkit-user-drag: none; }
.map-canvas.map-img-missing .map-img {
  min-height: 640px;
  background:
    repeating-linear-gradient(0deg, rgba(var(--accent-rgb),0.05) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(90deg, rgba(var(--accent-rgb),0.05) 0 1px, transparent 1px 64px), #0a1018;
}
/* ── Live Map tile pyramid ────────────────────────────────────────────────────
   Drawn by public-site/mapTiles.js, which is shared by this panel and the field
   console. It replaces nothing in the geometry: it fills exactly the square box
   the <img> filled, and every tile is placed as a PERCENTAGE of that same box —
   the one markers are placed against — so the background's pixel count is not an
   input to where anything lands.

   z-index: 0 (not auto) is load-bearing. It makes .map-tiles a stacking context,
   which keeps the per-level z-indexes inside it; without it a level-5 layer would
   compete with .map-mk (z-index 2) and paint over every marker on the island. */
.map-tiles { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.map-tiles-l { position: absolute; inset: 0; }
/* A tile is hidden until its image has actually decoded, so a level that is still
   arriving shows the coarser one underneath rather than anything half-painted.

   NO TRANSITION on this pair, deliberately. A crossfade here would be a nicety worth
   about 120ms, and it costs the ability to CHECK the rule: `check-panel-render` asks
   every loaded tile for its computed opacity, and under Chrome's virtual clock a
   transition does not advance, so twenty perfectly good tiles report 0 and the one
   failure that genuinely looks like a slow network becomes unmeasurable. A tile that
   pops in over a correct coarse image is not a defect; a blank square is. */
/* THE CELL is the box a tile occupies; the tile is the picture inside it.
   Two elements because an `img` is a REPLACED element: `left` + `right` on one of those does not
   stretch it, the width stays intrinsic and `right` is dropped. On a plain span the two edges
   resolve the box exactly, and because a cell's right edge and its neighbour's left edge are the
   SAME percentage of the SAME containing block, the pair meets on one number. That is what the
   1-screen-pixel bleed used to paper over: the bleed stopped hairline gaps by making every tile a
   pixel too big, which -- an img being `object-fit: fill` -- STRETCHED the 320 source columns
   across cell+1, so content drifted up to a full screen pixel by each tile's far edge and snapped
   back at the join. A road did not continue; the owner saw it as the map being "bitten". */
.map-cell { position: absolute; overflow: hidden; }
.map-tile {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0;
  user-select: none; -webkit-user-drag: none;
}
.map-tile.is-on { opacity: 1; }

.map-markers { position: absolute; inset: 0; }

/* Claimed base area rectangle.
   The rectangle takes the SAME `--badge-bg` its marker does, so a squadmate's claimed area reads as
   theirs rather than as yours. It used to have exactly one rule — the owner's amber — and
   `squadBases` reuses the same markup, so a squad base drew a pink dot inside an amber rectangle
   and the rectangle was the bigger of the two. `color-mix` keeps the fill and the edge derived from
   that one colour rather than adding a second list of translucent variants to drift from it. */
.map-area { position: absolute; border: 1px solid; border-radius: 3px; }
.map-area.area-player {
  border-color: color-mix(in srgb, var(--badge-bg, #ff9a2e) 55%, transparent);
  background: color-mix(in srgb, var(--badge-bg, #ff9a2e) 10%, transparent);
}

/* ── THE ZONES ─────────────────────────────────────────────────────────────────────────────────
 *
 * The admin panel's rules, letter for letter, because the two maps have to draw one thing one way.
 *
 * `pointer-events: none` on the shape and `auto` on its name is load-bearing rather than tidy: a
 * zone is up to 500 m across, which at map scale is a large part of a sector, and a clickable disc
 * would swallow every click on every marker standing inside it. The name takes the pointer, which
 * is also what makes picking one of two overlapping circles precise rather than a guess. */
.map-zone {
  position: absolute; z-index: 0; pointer-events: none;
  border: 1px solid currentColor;
}
/* The fill is a PSEUDO-ELEMENT at a tenth opacity rather than a translucent background, and that is
   not a style preference. A zone's colour arrives as `currentColor` — the layer's, or the
   configuration's own, set inline — and the only way to give `currentColor` an alpha in CSS is
   `color-mix()`. A browser without it drops that declaration and falls back to whatever came
   before, which was `background: currentColor`: a FULLY OPAQUE disc 500 m across, painted over the
   island. `opacity` on a child needs no such support and cannot fail that way. It is a child rather
   than `opacity` on the element itself because the name label is also a child, and fading a zone
   must not fade the word that says which zone it is. */
.map-zone::before {
  content: ''; position: absolute; inset: 0; background: currentColor; opacity: 0.1;
  border-radius: inherit;
}
.map-zone.zn-round, .map-zone.zn-round::before { border-radius: 50%; }
/* The border gets a RING and no fill. It is 18 km on a 15.24 km island, so a tenth-opacity fill is
   a tint over the entire viewport rather than a shape anybody can see the edges of. A boundary is
   its edge; that is the whole of what it is. */
.map-zone.zn-border::before { display: none; }
.map-zone.zn-border { border-style: dashed; }
.map-zone.zn-custom, .map-zone.zn-outpost,
.map-zone.zn-hazard, .map-zone.zn-border { color: var(--zn-c); }
/* ⚠ A DASHED OUTLINE MEANS "THE RIGHT SIZE IN THE WRONG SHAPE", and it is drawn for exactly one
   thing: a radiation ellipse the game has turned, whose angle is in no payload. It is painted
   beside the inscribed circle that is right at any angle, because neither is honest alone — the
   ellipse is wrong at the corners and the circle under-claims a hazard. The fill goes with it: a
   tenth-opacity wash over an area that is not quite the real area reads as a measurement. */
.map-zone.zn-uncertain { border-style: dashed; }
.map-zone.zn-uncertain::before { display: none; }
.zn-name {
  position: absolute; left: 50%; top: 0; transform: translate(-50%, -50%);
  /* ⚠ 12rem FLAT, never `min(100%, 12rem)`. The percentage resolves against the ZONE, not against
     the map — and a zone is as small as its own geometry — so the radiation sphere's plate came out
     about fifty pixels wide and its name read "Ra…", with the custom rectangle beside it reading
     "A…". Nothing measured it: the element was there, its contrast was fine, `hasnt` had nothing to
     match and the ellipsis is exactly what a correctly-clipped label does. It was found by opening
     the rendered page. 12rem is 192px, comfortably inside a 320px phone, so there is nothing for
     the width rules to guard against here. */
  max-width: 12rem; padding: 0.1rem 0.45rem;
  background: var(--panel); border: 1px solid currentColor; border-radius: var(--pill, 999px);
  color: var(--text); font-size: 0.66rem; line-height: 1.5;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: auto; cursor: pointer;
  /* Counter-scaled with the markers, so a name stays readable at every zoom instead of growing into
     a banner across the island. `--mz` is set on the canvas by `applyTransform`. */
  scale: var(--mz, 1);
}
/* Both halves together on the ELEMENT, never one of them in the hover rule: a rule that changes only
   the background keeps the old text colour and a rule that changes only the colour keeps the old
   fill, and either way somebody cannot read it. */
.zn-name:hover { border-color: var(--amber-hi); color: var(--amber-hi); background: var(--panel-2); }

.map-mk {
  position: absolute; width: 0; height: 0; padding: 0; background: none; border: none; cursor: pointer; z-index: 2;
}
.map-mk .mk-dot {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%) scale(var(--mz, 1));
  box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  border: 1.5px solid rgba(0, 0, 0, 0.55); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}
.map-mk .mk-dot::before { content: ''; position: absolute; inset: -8px; border-radius: 50%; }
.map-mk .mk-dot .ico { width: 12px; height: 12px; color: var(--bg); }
/* The label plate, its edge nudge and its decluttered state — the same three rules as the admin
   panel's `.mk-label`, and they have to stay the same three: the two maps are separate bundles
   sharing one design, and `declutterLabels()` here is the twin of `mapDeclutterLabels()` there.
   `--mk-dx` is a NUMBER OF SCREEN PIXELS, multiplied by --mz because everything inside the canvas
   is scaled by it. The plate is OPAQUE on purpose: at 0.85 an overlapped name bled through the one
   on top of it and the two read as a third word that is in neither. */
.map-mk .mk-label {
  display: none; position: absolute; left: 0; top: 0;
  transform: translate(-50%, calc(13px * var(--mz, 1)))
             translateX(calc(var(--mk-dx, 0) * var(--mz, 1) * 1px))
             translateY(calc(var(--mk-dy, 0) * var(--mz, 1) * 1px))
             scale(var(--mz, 1));
  transform-origin: top center;
  max-width: 130px; padding: 1px 5px; border-radius: var(--radius);
  background: #0a0d12; color: var(--text); font-size: 11px; font-weight: 600;
  line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none;
}
.map-markers.labels-on .mk-label, .map-mk:hover .mk-label { display: block; }
/* A name that could not be read where it landed. It keeps its box so the next pass can still
   measure where it wants to be, and the hover rule outranks it so the name is one point away. */
.mk-label.mk-lab-off { visibility: hidden; }
.map-mk:hover .mk-label { visibility: visible; }
.map-mk:hover { z-index: 5; }
.map-mk:hover .mk-dot { outline: 2px solid var(--amber-hi); }

.map-mk .mk-dot { background: var(--badge-bg, transparent); }
/* Squad layers (My Stuff map) — one distinct colour so squad stuff/positions read apart from your own. */
:root { --c-squad: #e05aa8; }
/* The island's own events — a third group beside your things and your squad's, and about nobody in
   particular. Warm for the things that are happening (a crate in the sky), cool for the places that
   have a state (a bunker, an arena), so the three read apart at a glance the way mine/squad do. */
:root { --c-cargo: #ffd23f; --c-bunker: #b9c2cf; --c-gevent: #d34fd9; }
/* Your own mines — red, because that is what the marker means. */
:root { --c-trap: #e5443f; }
/* A garden is the one thing on this map that GROWS, so it gets the one green nothing else uses.
   Points of interest are a PLACE rather than a thing, which is why they have a hue of their own;
   the turret yellow is borrowed by the bank for the same reason the admin map borrows it. Both
   values are the admin panel's, letter for letter — one product, one palette. */
:root { --c-garden: #7ad14f; --c-poi: #4fb9c9; --c-turret: #e8c33a; }
/* The living world: hostile red, static-but-dangerous orange, people gold, wildlife green,
   machines violet. Copied from the admin map so a wolf is the same green on both. */
:root {
  --lk-puppet: #e5443f;
  --lk-boss: #ff2d55;
  --lk-sentry: #ff7a45;
  --lk-npc: #c9a24b;
  --lk-animal: #6cbf3f;
  --lk-shark: #29a3c9;
  --lk-drone: #a06cff;
  --lk-dropship: #00d3c2;
}
/* ══ EVERY KIND'S COLOUR, ONCE ═════════════════════════════════════════════════════════════════
 *
 * Four separate lists used to carry this fact — the marker dot, the layer badge, the list dot and
 * the card plate — and they had drifted: the card's list was SIX ENTRIES SHORT. `.mp-ico .ico` is
 * painted `var(--bg)` deliberately (the glyph sits on the kind's colour), so a kind the card's list
 * had never heard of drew a near-black glyph on a near-black panel at 1.06:1. Invisible, laying out
 * at full size, sprite id resolving, no error anywhere, and no rule able to see it — a missing
 * colour has perfectly good geometry and a missing glyph has a perfectly good colour.
 *
 * Two of the six were unconditional: `mk-squad-base` and `mk-squad-player` carried no picture at all,
 * so the plate was the only thing on the card and it was blank every single time. The admin panel
 * reached exactly this conclusion after the render check found nineteen at once; this is its fix,
 * applied here, and it is why the four consumers below read a token instead of naming a colour.
 *
 * Adding a kind is now ONE LINE, and it cannot be half-done. */
.mk-player { --badge-bg: var(--c-player); }
.mk-base { --badge-bg: var(--c-base); }
.mk-vehicle { --badge-bg: var(--c-vehicle); }
.mk-chest { --badge-bg: var(--c-chest); }
.mk-storage { --badge-bg: var(--c-storage); }
.mk-trap { --badge-bg: var(--c-trap); }
.mk-garden { --badge-bg: var(--c-garden); }
/* A player's own note. Deliberately not one of the game's colours: everything else on this map
   is a thing the game put there, and a marker is not — it reads as somebody's handwriting. */
:root { --c-marker: #c98fe0; }
.mk-marker { --badge-bg: var(--c-marker); }
/* ⚠ **THIS IS WHERE THE "PINK" CAME FROM.** Every squad marker shared `--c-cargo` — the CARGO DROP
   colour — so a squadmate's chest was drawn as neither a chest nor a squad thing but as a supply
   crate, which is the *"nechci videt squad veci ruzovi"* the owner reported. The property classes
   are gone with the parallel layers: a chest is a chest, in the chest colour, and whose it is is on
   the row. What is left is the squadmate themselves, and a person is drawn in the PERSON colour —
   they are told apart from you by their name, their face and the fact that yours says "You". */
.mk-squad-player { --badge-bg: var(--c-player); }
.mk-cargo { --badge-bg: var(--c-cargo); }
.mk-bunker { --badge-bg: var(--c-bunker); }
.mk-gevent { --badge-bg: var(--c-gevent); }
/* The fixed places, drawn by their own KIND. The layer's colour is the fallback for a kind a newer
   reference file adds; every one that exists today has its own, because a bank, a trader counter
   and a petrol pump under one glyph is a field of identical dots. The colours are the admin map's.
   `.mk-poi` was here and is gone with the one layer that used it — see PLACE_LAYERS in app.js. */
.mk-resource { --badge-bg: var(--c-poi); }
.poi-trader { --badge-bg: var(--c-chest); }
.poi-bank { --badge-bg: var(--c-turret); }
.poi-fuel { --badge-bg: var(--c-poi); }
.poi-sentry { --badge-bg: var(--lk-sentry); }
.poi-bunker { --badge-bg: var(--lk-boss); }
/* The living world. The LAYER's colour, for a list badge and for a card whose kind is unknown… */
.mk-puppet { --badge-bg: var(--lk-puppet); }
.mk-animal { --badge-bg: var(--lk-animal); }
.mk-sentry { --badge-bg: var(--lk-sentry); }
.mk-npc { --badge-bg: var(--lk-npc); }
.mk-drone { --badge-bg: var(--lk-drone); }
.mk-dropship { --badge-bg: var(--lk-dropship); }
.mk-boss { --badge-bg: var(--lk-boss); }
/* …and the KIND's, which is what a marker, a list dot and a card plate actually use — a shark among
   the animals and a deployed sentry among the sentries are the whole reason the layers are worth
   opening. A kind a newer bridge reports has no rule here and falls back to its layer's colour,
   which is a real colour rather than the transparent plate a missing entry would leave. */
.lk-puppet { --badge-bg: var(--lk-puppet); }
.lk-animal { --badge-bg: var(--lk-animal); }
.lk-shark { --badge-bg: var(--lk-shark); }
.lk-sentry, .lk-sentryd { --badge-bg: var(--lk-sentry); }
.lk-npc { --badge-bg: var(--lk-npc); }
.lk-drone { --badge-bg: var(--lk-drone); }
.lk-dropship { --badge-bg: var(--lk-dropship); }
.lk-boss { --badge-bg: var(--lk-boss); }
/* The five zone layers. `--badge-bg` here too, so the layer list's icon square is filled — a class
   that sets a shape colour and forgets the badge leaves that square TRANSPARENT and the glyph is
   near-black at 1.14:1, which is the defect the admin panel's render check found nineteen of at
   once and the reason every colour on this map is one token feeding every consumer. */
.zn-custom { --zn-c: #6ee7c8; }
.zn-outpost { --zn-c: #e6b96a; }
.zn-hazard { --zn-c: #b6e34a; }
.zn-border { --zn-c: #8ea2c0; }
.zn-custom, .zn-outpost, .zn-hazard, .zn-border { --badge-bg: var(--zn-c); }
/* Both dropdowns under a layer sit on one line and share the width. */
/* Layer groups: "mine" and "squad" are different questions, so they get their own headings
   instead of one long undifferentiated list. */
.mlyr-group { border-top: 1px solid var(--line); margin-top: 0.35rem; padding-top: 0.35rem; }
.mlyr-group:first-child { border-top: 0; margin-top: 0; padding-top: 0; }
.mlyr-group-h {
  padding: 0.2rem 0.75rem 0.3rem; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted);
}
/* Styled dropdown, ported from the admin panel so both maps read as one product. The native
   <select> underneath stays as the source of truth (and the accessible control); only its
   presentation is replaced. */
.cdd { position: relative; display: inline-flex; flex: 1; min-width: 0; }
.cdd-native { position: absolute; width: 1px; height: 1px; padding: 0; margin: 0; opacity: 0; pointer-events: none; }
.cdd-btn, .cdd-btn:hover, .cdd-btn:active {
  display: inline-flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.32rem 0.5rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.78rem; font-weight: 600; letter-spacing: 0;
  text-align: left; cursor: pointer; transform: none;
}
.cdd-btn:hover { border-color: var(--line-amber, var(--amber)); color: var(--amber-hi); }
.cdd-lbl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cdd-btn .dd-caret { margin-left: auto; color: var(--muted); font-size: 0.7rem; }
.cdd-btn:hover .dd-caret { color: var(--amber-hi); }
.cdd-menu {
  position: absolute; z-index: 3000; min-width: 8rem; max-width: calc(100vw - 16px); overflow-y: auto;
  background: var(--panel); border: 1px solid var(--line-amber, var(--line)); border-radius: var(--radius);
  padding: 0.3rem; box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
}
.cdd-menu[hidden] { display: none; }
.cdd-item, .cdd-item:active {
  display: flex; align-items: center; justify-content: flex-start; gap: 0.5rem; width: 100%;
  white-space: nowrap; padding: 0.4rem 0.55rem; background: none; border: none; border-radius: var(--radius);
  color: var(--text); cursor: pointer; font: inherit; font-size: 0.85rem; text-align: left; transform: none;
}
.cdd-item:hover { background: var(--amber); color: var(--on-amber); }
/* Derived from --accent-rgb, not a literal: this was the one accent tint in the sheet written as a
   hard-coded triplet, so it kept the shipped amber on a server whose owner had set a custom accent
   — the selected row of a dropdown was the only thing on the page that did not reskin. */
.cdd-item.on { background: rgba(var(--accent-rgb), 0.18); color: var(--amber-hi); }
.cdd-item.on:hover { background: var(--amber); color: var(--on-amber); }
/* Checkable items: a multi-select that still looks like every other dropdown. */
.cdd-item.cdd-check { gap: 0.55rem; }
.cdd-item.cdd-check .cdd-box {
  flex: none; width: 14px; height: 14px; border-radius: 3px;
  border: 1px solid var(--line-amber, var(--line)); background: var(--inset);
}
.cdd-item.cdd-check.on { background: none; color: var(--text); }
.cdd-item.cdd-check.on .cdd-box {
  background: var(--amber); border-color: var(--amber);
  /* The tick is ink on the amber fill, so its colour is --on-amber — but a data: URI cannot read a
     custom property, so this one literal has to be kept in step with :root by hand. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230b0c0e' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12.5l4.5 4.5L19 7.5'/></svg>");
  background-size: 11px 11px; background-repeat: no-repeat; background-position: center;
}
.cdd-item.cdd-check .cdd-t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.map-subf { display: flex; flex-direction: column; gap: 0.35rem; }

/* Info card — anchored next to the clicked marker (position set in JS). */
.map-popover {
  position: absolute; z-index: 20; width: 330px; max-width: calc(100% - 24px); padding: 0.85rem 1rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius-lg);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
}
/* Never taller than the map, with the BODY scrolling inside it. A garden bed with two dozen crops
   ran past the bottom of the page and could not be scrolled at all. */
.map-popover { display: flex; flex-direction: column; max-height: calc(100% - 16px); }
/* ⚠ **AND ON A PHONE IT WAS 165px WIDE, WHICH IS WHY EVERY NAME INSIDE IT WAS CUT.** `max-width:
   calc(100% - 24px)` is a percentage of the card's own offset parent rather than of the screen, and
   on a phone that parent is about half the page — so a card listing what is inside a container had
   about 130px to put an item name in, and `7.62x39mm Round` came back as 28% of itself across 274
   measured rows. Letting the name wrap does nothing at that width; the card is the defect.
   `min()` against the VIEWPORT is what makes the card as wide as the screen allows, and the anchor
   code already clamps its position back inside the map, so a wider card slides rather than spills. */
@media (max-width: 560px) {
  .map-popover { width: min(330px, calc(100vw - 20px)); max-width: none; }
}
/* `min-height: 0` is the whole reason this scrolls: a flex item defaults to `min-height: auto`,
   which refuses to shrink below its content, so the card grows past its own max-height and the
   overflow rule never comes into play. The card looks capped and simply is not. */
.map-popover .mp-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain; margin: 0 -0.35rem; padding: 0 0.35rem;
}
.map-popover.hidden { display: none; }
.mp-close {
  position: absolute; top: 5px; right: 7px; width: 22px; height: 22px; padding: 0; line-height: 1;
  background: none; border: none; color: var(--muted); font-size: 20px; cursor: pointer;
}
.mp-close:hover { color: var(--amber-hi); background: none; border: none; }
.mp-head { display: flex; align-items: center; gap: 0.55rem; margin: 0 1.2rem 0.6rem 0; }
.mp-ico { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: var(--radius-md); flex: none; border: 1px solid rgba(0, 0, 0, 0.4); }
.mp-ico .ico { width: 15px; height: 15px; color: var(--bg); }
/* The fourth reader of `--badge-bg`, and the one the missing entries were in. No colour on the
   bare wrapper: `.mp-ico.mp-img` below carries the artwork and stays on the panel. */
.mp-ico { background: var(--badge-bg, transparent); }
/* A state word that means something has to LOOK like it. Both of these are used on this page — the
   bunker card's "● Open"/"● Closed" and the mine card's "● Armed"/"● Unarmed" — and neither was
   defined in this stylesheet at all, only in the admin panel's. So the dot and the word rendered as
   plain body text and the whole point of the "●" was gone. A third state exists on purpose: a
   bunker or a mine the server has not reported on is neither, and it gets the muted wording rather
   than being guessed into one of the two. */
.mp-on { color: var(--c-player); font-weight: 600; }
.mp-off { color: var(--muted); font-weight: 600; }
.mp-headtxt { min-width: 0; }
/* A card's own heading, so it wraps: `Kinglet Duster (squad runabout)` showed 73% of itself in
   185px, which is a card that cannot say what it is about. The header is two lines then, and the
   body below it scrolls, so nothing else on the card moves. */
.mp-title { color: var(--text); font-weight: 700; font-size: 0.95rem; white-space: normal; overflow-wrap: anywhere; }
.mp-sub { font-size: 0.75rem; color: var(--muted); }
.map-popover .mp-row { display: flex; justify-content: space-between; gap: 0.75rem; font-size: 0.82rem; padding: 0.2rem 0; border-top: 1px solid var(--line-2); }
.map-popover .mp-rows .mp-row:first-child { border-top: none; }
.map-popover .mp-row span { color: var(--muted); }
.map-popover .mp-row b { color: var(--text); font-variant-numeric: tabular-nums; text-align: right; }
/* ⚠ A SENTENCE IS NOT A VALUE. `.mp-row` is a label on the left and a figure on the right, which is
   right for "Floors 3" and wrong for a whole sentence: "Where it is comes from the save. What it is
   doing right now comes from the running game." came out as a four-line right-aligned block beside
   a label that had itself wrapped onto two lines. It was found by looking at the rendered card,
   because every rule on this page was perfectly happy with it. Full width, left-aligned, its own
   line, no label — the same shape and the same class name the admin map's bunker card uses. */
.map-popover .mp-note {
  font-size: 0.75rem; line-height: 1.45; color: var(--muted);
  padding: 0.35rem 0 0.1rem; border-top: 1px solid var(--line-2);
}
.map-popover .dim { color: var(--muted); }
.mp-contents-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.4rem; width: 100%; margin-top: 0.5rem;
  padding: 0.4rem; background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  font-family: var(--ui); font-weight: 600; font-size: 0.8rem; border-radius: var(--radius-md); cursor: pointer;
}
.mp-contents-btn:hover { border-color: var(--line-amber); color: var(--amber-hi); background: var(--panel-2); }
.mp-contents-btn.open { color: var(--amber-hi); border-color: var(--line-amber); }
.mp-contents-btn .ico { width: 14px; height: 14px; }
/* `padding-right: 1px` is the third fix of the same class in this block, and it is the one that
   belongs on the SCROLLER rather than on something inside it. Measured at a true 320px viewport:
   the popover is `calc(100% - 24px)` of a map whose own width is fractional, so this box lays out
   at 175.15625px and its content box at 165.15625 once the vertical scrollbar is taken. Every
   block child then fills 165.15625 — `clientWidth` FLOORS that to 165 and `scrollWidth` CEILS it to
   166, and the one pixel that falls out is a real horizontal scrollbar inside a card on a phone.
   Nothing inside it is too wide; the box simply cannot be divided evenly. A pixel of right padding
   is included in `clientWidth` and excluded from the content, so the two round to the same number
   whatever fraction the map hands down — and it also keeps the last character of a row off the
   scrollbar, which is worth the pixel on its own. */
.mp-contents { margin-top: 0.5rem; border-top: 1px solid var(--line); max-height: 240px; padding-right: 1px; overflow-y: auto; overscroll-behavior: contain; }
.mp-contents[hidden] { display: none; }
.mp-c-head {
  display: flex; align-items: center; gap: 0.4rem; padding: 0.45rem 0.4rem; position: sticky; top: 0; z-index: 1;
  background: var(--panel); border-bottom: 1px solid var(--line);
  color: var(--text); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
}
.mp-c-head .ico { width: 13px; height: 13px; color: var(--amber-hi); }
.mp-c-count { margin-left: auto; color: var(--muted); font-weight: 600; text-transform: none; letter-spacing: 0; }
.mp-c-msg { padding: 0.7rem; text-align: center; color: var(--muted); font-size: 0.82rem; }
.mp-c-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.18rem 0.1rem; }
.mp-c-img { width: 22px; height: 22px; flex: none; display: inline-flex; align-items: center; justify-content: center; background: var(--panel-2); border-radius: var(--radius); overflow: hidden; }
.mp-c-img img { width: 100%; height: 100%; object-fit: contain; }
/* The panel carries this same rule and the same reasoning: the name of what is inside a container
   is the row, so it wraps rather than being cut to a third of itself. */
.mp-c-name { flex: 1; min-width: 0; font-size: 0.8rem; white-space: normal; overflow-wrap: anywhere; }
.mp-c-n { font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* ── A container found inside another one ──────────────────────────────────────────────────────
   The same row shape as an item, because it IS one — a backpack in a car is a thing in the car —
   except that it opens. It sits among the loose rows in one list rather than in a group of its own:
   what a player is looking at is a car with a backpack in it. */
/* NO `width: 100%`, and that is not a tidy-up. `display: flex` (from `.mp-c-row`) already makes this
   a block-level box that fills its parent, and `100%` of a FRACTIONAL parent rounds UP where the
   parent's own client width rounds down — one pixel of sideways scroll inside the card, measured at
   a true 310px viewport where the popover is 165px wide and every level of nesting narrows it
   again. An auto width cannot do that. */
.mp-c-open {
  text-align: left; font: inherit; letter-spacing: 0; cursor: pointer;
  padding: 0.22rem 0.25rem; border-radius: var(--radius);
  border: 1px solid var(--line-2); background: var(--inset); color: var(--text);
  margin-bottom: 0.15rem;
}
/* Both halves in one rule. A hover that changes only the fill or only the ink is how a control ends
   up unreadable in one state, and this surface has no global button hover to inherit from. */
.mp-c-open:hover, .mp-c-open:focus-visible {
  background: var(--hover-bg); color: var(--text); border-color: var(--line-amber); outline: none;
}
.mp-c-open .mp-c-chev { width: 11px; height: 11px; color: var(--muted); flex: none; margin-left: 0.15rem; }
.mp-c-open.open .mp-c-chev { color: var(--amber-hi); }
/* Opens straight underneath the row that was clicked, with a rule down the left saying "these belong
   to that". Indenting the whole block instead reads as the panel jumping sideways.
   `.mp-c-sub` also carries `.mp-contents`, whose 240px cap and scrollbar are right for the outer box
   and wrong here — nested scrollers inside a scroller are a trap on a phone. The card scrolls; this
   does not. */
.mp-c-sub {
  /* The horizontal inset is in WHOLE PIXELS on purpose, and it is the difference between this box
     scrolling sideways and not. `0.55rem` is 7.7px here; Chrome lays the content out against 7.7
     and paints the border against 8, and the one pixel that falls out of that is a real horizontal
     scrollbar inside the card — measured at a true 310px viewport, where the popover is 165px wide
     and each level of nesting takes another 10 off it. A whole number cannot round apart. */
  margin: 0.15rem 0 0.35rem; padding: 0.2rem 0 0.2rem 8px;
  max-height: none; overflow: visible; border-top: 0;
  border-left: 2px solid var(--line-amber);
  background: rgba(0, 0, 0, 0.14); border-radius: 0 var(--radius) var(--radius) 0;
}
/* A list that was cut says so, with the number that was really there. */
.mp-c-note { padding: 0.35rem 0.4rem 0.15rem; color: var(--standby); font-size: 0.74rem; line-height: 1.4; }
/* `width: auto` on a block, never `calc(100% - margins)` — same rounding trap as `.mp-c-open`. */
.mp-c-more {
  display: block; width: auto; margin: 0.35rem 0.4rem 0.2rem; padding: 0.36rem;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.76rem; font-weight: 600; cursor: pointer;
}
.mp-c-more:hover, .mp-c-more:focus-visible { background: var(--hover-bg); color: var(--amber-hi); border-color: var(--line-amber); outline: none; }

/* ── Condition as a dot ────────────────────────────────────────────────────────────────────────
   How the game itself shows it, and the same three tokens the readouts use — invented names would
   silently fall back to literals and drift into a second green. */
.hpd { flex: none; width: 0.5rem; height: 0.5rem; border-radius: 50%; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
.hpd-ok { background: var(--online); }
.hpd-warn { background: var(--standby); }
.hpd-bad { background: var(--alarm); }
/* Dot AND figure. The dot is what the eye reads down a column of rows; the number is what a player
   acts on — a colour alone means hovering every row to learn whether "warn" is 58% or 27%. */
.hpd-w {
  flex: none; display: inline-flex; align-items: center; gap: 0.3rem; min-width: 3.1rem;
  font-family: var(--mono); font-size: 0.7rem; font-variant-numeric: tabular-nums;
}
.hpd-n { color: var(--muted); }
.hpd-ok-t { color: var(--online); opacity: 0.85; }
.hpd-warn-t { color: var(--standby); }
.hpd-bad-t { color: var(--alarm); }

.map-side, .mp-contents { scrollbar-width: thin; scrollbar-color: rgba(var(--accent-rgb), 0.4) transparent; }
.map-side::-webkit-scrollbar, .mp-contents::-webkit-scrollbar { width: 9px; }
.map-side::-webkit-scrollbar-track, .mp-contents::-webkit-scrollbar-track { background: transparent; }
.map-side::-webkit-scrollbar-thumb, .mp-contents::-webkit-scrollbar-thumb {
  background: rgba(var(--accent-rgb), 0.35); border-radius: var(--radius-md); border: 2px solid transparent; background-clip: content-box;
}
.map-side::-webkit-scrollbar-thumb:hover, .mp-contents::-webkit-scrollbar-thumb:hover {
  background: rgba(var(--accent-rgb), 0.6); background-clip: content-box;
}
.mp-ico.mp-img { position: relative; overflow: hidden; background: var(--panel-2); }
.mp-ico.mp-img img { width: 100%; height: 100%; object-fit: contain; display: block; }
.mp-ico.mp-img .mp-ico-fb { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; }
.mp-ico.mp-img .mp-ico-fb .ico { width: 15px; height: 15px; color: var(--amber-hi); }
.mp-ico.mp-img.mp-imgfail img { display: none; }
.mp-ico.mp-img.mp-imgfail .mp-ico-fb { display: flex; }
.ml-thumb { width: 22px; height: 22px; flex: none; border-radius: var(--radius); overflow: hidden; background: var(--panel-2); display: inline-flex; align-items: center; justify-content: center; }
.ml-thumb img { width: 100%; height: 100%; object-fit: contain; }

.map-empty {
  position: absolute; top: 1rem; left: 50%; transform: translateX(-50%);
  padding: 0.4rem 0.9rem; background: rgba(8, 10, 14, 0.85); border: 1px solid var(--line);
  border-radius: var(--pill); color: var(--muted); font-size: 0.85rem;
}
.map-empty.hidden { display: none; }
.squad-members { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 0.4rem; }
/* FOUR columns since the member carries a face. Every cell is placed EXPLICITLY, which is what lets
   the online dot be absent on a squad that is not the reader's own without the rest of the row
   sliding one track along — the failure `.squad-row`'s own note records. The face is 1.6rem, the
   same as the crest on the heading above this list. */
.sm-row { display: grid; grid-template-columns: auto auto 1fr auto; align-items: center; gap: 0.1rem 0.55rem; padding: 0.5rem 0.65rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.sm-dot { grid-column: 1; grid-row: 1 / span 2; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
.sm-dot.on { background: var(--online); box-shadow: 0 0 7px var(--online); }
.sm-face { grid-column: 2; grid-row: 1 / span 2; }
.sm-name { grid-column: 3; grid-row: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sm-rank { grid-column: 4; grid-row: 1; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--amber); white-space: nowrap; }
.sm-seen { grid-column: 3 / -1; grid-row: 2; font-family: var(--mono); font-size: 0.74rem; color: var(--muted); }

#notif-panel { margin-top: 1.25rem; }
.notif-history { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.nh-row { display: flex; align-items: center; gap: 0.7rem; padding: 0.55rem 0.7rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.nh-ico { color: var(--amber); flex: none; display: inline-flex; }
.nh-ico .ico { font-size: 1.2rem; }
.nh-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.nh-title { font-weight: 600; font-size: 0.9rem; }
/* ⚠ **A NOTIFICATION IS A SENTENCE, AND A SENTENCE WITH AN ELLIPSIS ON IT IS NOT ONE.** Measured on
   a phone: `Something happened to your property at A1 il y a 35d` came back with 29% of it on
   screen — a player told that something happened and not what, where or when, on the one screen
   that exists to tell them. A label can be elided because the reader can go and look; this IS the
   looking. It wraps, and the row grows to hold it. */
.nh-body { font-size: 0.82rem; color: var(--muted); white-space: normal; overflow-wrap: anywhere; }
.nh-time { font-family: var(--mono); font-size: 0.74rem; color: var(--faint); white-space: nowrap; }
.nh-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 1.6rem 0 0.6rem; }
.nh-head h3 { margin: 0; }
.nh-head .etr-pager { margin: 0; }
/* "alerts only inside your flag area" note, mirrors the Discord /notify panel */
.notif-flagnote { display: flex; align-items: flex-start; gap: 0.5rem; margin: 0 0 1rem; padding: 0.6rem 0.75rem; background: var(--inset); border: 1px solid var(--line-amber); border-radius: var(--radius); color: var(--muted); font-size: 0.84rem; line-height: 1.5; }
.notif-flagnote .ico { color: var(--amber); flex: none; margin-top: 0.1rem; }
.notif-flagnote strong { color: var(--text); }

/* ── Modal (player profile / squad detail) ────────────────────────────────── */
.modal { position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,0.72); display: flex; align-items: flex-start; justify-content: center; padding: 5vh 1rem; overflow: auto; }
.modal[hidden] { display: none; }
.modal-card { --modal-pad: 1.6rem; position: relative; width: 100%; max-width: 1320px; background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 1.5rem var(--modal-pad); }
.modal-close { position: absolute; top: 0.5rem; right: 0.8rem; background: transparent; border: 0; color: var(--muted); font-size: 1.7rem; line-height: 1; cursor: pointer; }
.modal-close:hover { color: var(--amber-hi); }
#modal-body h3 { display: flex; align-items: center; gap: 0.45rem; margin: 1.2rem 0 0.55rem; font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
#modal-body h3 .ico { color: var(--amber); }

/* The page's own "are you sure", in place of the browser's. Same shell as the card above — it is
   the same kind of thing — with a width a question deserves rather than a dossier's. Centred
   vertically because there is nothing to scroll. `min(100%, …)` rather than a bare width, which is
   the rule this project keeps relearning: a fixed one collapses a 320px phone into sideways scroll. */
.fc-confirm { align-items: center; z-index: 120; }
.fc-confirm-card { max-width: min(100%, 26rem); }
.fc-confirm-q { margin: 0 0 1.1rem; line-height: 1.55; }
.fc-confirm-btns { display: flex; flex-wrap: wrap; gap: 0.6rem; justify-content: flex-end; }
/* 40px under a thumb, which is the floor everything a finger lands on holds to here. */
.fc-confirm-btns .btn { min-height: 40px; }

/* ── Squads list ──────────────────────────────────────────────────────────── */
.lb-click { cursor: pointer; }
.squad-list { list-style: none; margin: 0; padding: 0; }
/* Same guarantee as .lb-row: the squad name keeps a floor width; meta + score
   ellipsize instead of pushing the name out of the card. */
/* FIVE columns, because the row now carries the squad's badge between its rank and its name.
   It was four, and adding an element to a fixed grid does not add a column -- every cell shifts
   one along and the last one drops out of the track list entirely. That is what a reader sees
   as the table being thrown about, and no rule here reads it: the markup is valid, the text is
   all present and every box is inside the page. The badge column is a fixed 1.6rem so a list of
   forty squads does not become a wall of crests, and the gap shrinks to keep the name's room. */
.squad-row { display: grid; grid-template-columns: 2.4rem 1.6rem minmax(7ch, 1fr) minmax(0, auto) minmax(0, auto); align-items: center; gap: 0.7rem; padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--line); cursor: pointer; }
.squad-row:hover { background: rgba(var(--accent-rgb),0.05); }
.squad-row .sq-rank { font-family: var(--mono); color: var(--faint); text-align: center; }
.sq-list-name { font-family: var(--ui); font-size: 1rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sq-list-meta { font-size: 0.82rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sq-list-score { font-family: var(--mono); color: var(--amber); font-size: 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ⚠ **THE SAME DEFECT AS THE EVENTS TABLE, BY A DIFFERENT MECHANISM: the RANKED VALUE was the thing
   that got truncated.** At 310px every row read `Squad A  6 m…  4 …` — "4 210 score" cut to a "4"
   and "6 members" to "6 m…" — all the way down a ranking whose whole content is those numbers. This
   one does have an ellipsis, and it still reads as a number: "4 …" beside "3 …" beside "2 …" is a
   plausible, wrong table.
   The cause is the track list: `minmax(7ch, 1fr)` on the NAME takes the free space and leaves the
   two `minmax(0, auto)` tracks to be squeezed to nothing, so the least important thing on the row
   was protected and the most important was not. A truncated squad name with an ellipsis is honest;
   a truncated score is a lie.
   So on a phone the row is two lines and nothing is truncated at all: rank, name and score on the
   first, the member count on the second. `max-content` on the score is what makes that a promise
   rather than a hope. */
/* ⚠ **AND THIS TEMPLATE WAS WRITTEN BEFORE THE ROW HAD A CREST, SO IT NAMED THREE COLUMNS FOR FIVE
   CHILDREN.** Adding an element to a fixed grid does not add a column — the note on `.lb-row` says
   so in as many words — and here the crest took column 2, pushed the NAME into column 3, and the
   score is explicitly placed in column 3 row 1 as well. Two items in one `max-content` cell, one of
   them with an ellipsis on it: measured at a true 310px, `Squad M` came out **41px wide with 59% of
   it on screen**, which is the owner's *"vedle toho squad emblemu by mel byt videt nazev toho squadu
   a ne to tam byt tak zmrskane"*.

   Four columns for the four things that sit on the first line, and the member count keeps its own
   row under the name. The crest spans both rows so it stays beside the whole row rather than
   floating against the name alone. */
@media (max-width: 560px) {
  .squad-row {
    grid-template-columns: 2.4rem 1.4rem minmax(0, 1fr) max-content;
    column-gap: 0.45rem; row-gap: 0.1rem;
  }
  .squad-row .sq-rank, .squad-row .sq-row-face { grid-row: 1 / span 2; align-self: center; }
  .sq-row-face { width: 1.4rem; height: 1.4rem; }
  /* ⚠ **THE NAME SPANS BOTH REMAINING TRACKS, and putting it in the first one alone made this
     WORSE rather than better** — measured, 41px before and 27px after. The score's track is
     `max-content` and the name's is `minmax(0, 1fr)`, which is the same trap `.lb-row` above
     records: the inflexible column takes what it likes and the flexible one has no floor to stop
     at. Spanning is what settles it — the name is then sized by the pair rather than by what the
     score left over, and the score keeps its own column on the second line where nothing competes
     with it. */
  .sq-list-name { grid-column: 3 / -1; grid-row: 1; }
  /* ⚠ **AND SHARING THE SECOND LINE WITH THE SCORE ONLY MOVED THE CUT ONTO THE MEMBER COUNT.**
     Wrapping it was not enough either: with the score's `max-content` track beside it the cell is
     about 46px, and `members` on its own is wider than that, so it went on being cut — by 9px
     instead of 21px, which is a smaller lie and still a lie.
     Each of the three gets its own line, in the order a reader wants them: which squad, what it
     scored, how many are in it. One line taller than a design that fitted nothing. */
  .sq-list-meta {
    grid-column: 3 / -1; grid-row: 3;
    white-space: normal; overflow: visible; text-overflow: clip;
  }
  /* ⚠ **AND THE SCORE GIVES BACK THE FEW PIXELS THAT KEEP THE NAME WHOLE.** Protecting the number
     cost the name at first — the top four rows all read "Squad…", which is four squads a reader
     cannot tell apart, and two controls that say the same thing is the `duptext` failure one rule
     along. A tenth of a rem on a mono figure buys the letter back, and the name still ellipsises
     before the number does, which is the order that matters. */
  .sq-list-score { grid-column: 3 / -1; grid-row: 2; overflow: visible; text-overflow: clip; font-size: 0.88rem; }
}
.squad-head .record-overall { margin-top: 0; }

/* ── Login gate (non-public tabs when logged out) ─────────────────────────── */
.gate-view[hidden] { display: none; }
.gate-card { max-width: 540px; margin: 6vh auto; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 1rem; padding: 2.6rem 2rem; }
.gate-card h2 { margin: 0; color: var(--amber); letter-spacing: 0.1em; text-transform: uppercase; font-size: 1.1rem; }
.gate-card p { margin: 0; }
.gate-card .btn-discord { align-self: center; }

/* Clickable player name (squad members → profile) */
.pl-click { cursor: pointer; }
.pl-click:hover { color: var(--amber-hi); }

/* Account-linking code (My Stats unlinked state) */
.link-code { font-family: var(--mono); font-size: 1.25rem; color: var(--amber-hi); background: var(--inset); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 0.5rem 0.85rem; letter-spacing: 0.06em; user-select: all; }

/* Account-linking stepper (My Stats, signed-in-but-unlinked state) */
.link-intro { margin: 0 0 1.2rem; color: var(--muted); line-height: 1.55; }
.link-steps { list-style: none; margin: 0 0 1.1rem; padding: 0; display: flex; flex-direction: column; gap: 1.1rem; }
.link-step { display: flex; gap: 0.85rem; align-items: flex-start; }
.link-step-no { flex: none; width: 27px; height: 27px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: var(--inset); border: 1px solid var(--line-amber); color: var(--amber); font-family: var(--mono); font-size: 0.85rem; font-weight: 700; }
.link-step-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.55rem; padding-top: 0.15rem; }
.link-step-t { font-weight: 600; color: var(--text); font-size: 0.95rem; }
.link-step-d { color: var(--muted); font-size: 0.86rem; line-height: 1.55; }
.link-step-btn { align-self: flex-start; }
.link-code-row { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; margin: 0; }
.link-copy { font-size: 0.78rem; padding: 0.4rem 0.75rem; }
.link-waiting { display: inline-flex; align-items: center; gap: 0.55rem; color: var(--amber); font-size: 0.85rem; }
.link-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--amber); animation: linkpulse 1.3s ease-in-out infinite; }
/* ── The step the reader is ON ──────────────────────────────────────────────────────────────────
   The whole complaint about this screen was that generating a code changed nothing where it
   mattered: the code, the exact form and the clock all appear inside step 2 now, and the step marks
   itself so a reader coming back to the tab can see where they are. A step that is DONE keeps its
   number and loses its emphasis — it is still part of the story, and hiding it would make the list
   renumber itself under somebody's eyes. */
.link-live { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.15rem;
  padding: 0.85rem 0.95rem; background: var(--inset); border: 1px solid var(--line-amber);
  border-left: 3px solid var(--amber); border-radius: var(--radius); }
.link-live[hidden] { display: none; }
.link-say { font-weight: 600; color: var(--text); font-size: 0.9rem; }
/* The rule the whole flow turns on. It is a WARNING rather than a note because the failure it
   describes is completely silent: the line is neither redeemed nor shown. */
.link-warn { display: flex; align-items: flex-start; gap: 0.5rem; margin: 0;
  color: var(--text); font-size: 0.86rem; line-height: 1.55; }
.link-warn .ico { flex: none; margin-top: 0.15rem; color: var(--amber); }
.link-why { margin: 0; line-height: 1.55; }
/* Empty until the clock is running, and empty is a state: a payload with no expiry prints nothing
   rather than a deadline this page invented. */
.link-expiry { margin: 0; font-family: var(--mono); font-size: 0.85rem; color: var(--amber); }
.link-expiry:empty { display: none; }
.link-expiry.is-expired { color: var(--alarm); }
.link-step.is-done .link-step-no { background: var(--amber); border-color: var(--amber); color: var(--on-amber); }
.link-step.is-done .link-step-t { color: var(--muted); }
.link-step.is-live .link-step-no { box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.16); }
/* ⚠ **THE CODE IS FOURTEEN MONO CHARACTERS AT 1.25rem WITH LETTER-SPACING, AND IT DOES NOT FIT A
   PHONE.** Measured rather than guessed: `check-field-console` reported `#me-unlinked` scrolling
   sideways by 20px at a true 310px viewport the first time this block was drawn, which is the
   letter-spacing alone (14 × 0.06em ≈ 17px) plus the box this now sits in. Nothing that reads
   declarations could have seen it — `check-mobile`'s threshold is a width somebody wrote down, and
   nobody wrote this one down; it is the sum of four paddings and a font.
   `overflow-wrap: anywhere` is the floor under all of it: a code somebody has to read and copy may
   wrap onto two lines, and must never be a box a reader has to scroll to the end of. */
@media (max-width: 560px) {
  .link-live { padding: 0.7rem 0.65rem; }
  .link-code { font-size: 1.05rem; padding: 0.4rem 0.6rem; letter-spacing: 0.03em; }
}
.link-code { overflow-wrap: anywhere; }
@keyframes linkpulse { 0%, 100% { opacity: 0.25; transform: scale(0.75); } 50% { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .link-dot { animation: none; } }

/* ── Overview: category leaders (#1 per category) ─────────────────────────── */
.cat-leaders { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
/* Collapsible category-leader groups (Combat, Survival…) on the overview. */
.cl-group { border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); overflow: hidden; }
.cl-grp-h { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.8rem; cursor: pointer; list-style: none; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--amber); font-weight: 700; user-select: none; }
.cl-grp-h::-webkit-details-marker { display: none; }
.cl-grp-h .ico { color: var(--amber); width: 1.1em; height: 1.1em; flex: none; }
.cl-grp-h:hover { background: var(--hover-bg); }
.cl-grp-h .statgroup-chev { margin-left: auto; color: var(--muted); font-size: 0.9rem; transition: transform 0.15s; }
.cl-group[open] .cl-grp-h { border-bottom: 1px solid var(--line); }
.cl-group[open] .cl-grp-h .statgroup-chev { transform: rotate(180deg); }
.cl-items { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 0.6rem; padding: 0.7rem 0.8rem; }
.cl-row { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 0.75rem; padding: 0.7rem 0.85rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.cl-row > .ico { color: var(--amber); font-size: 1.6rem; }
.cl-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.cl-cat { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.cl-line { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; min-width: 0; }
/* Name keeps a floor width; a long translated value ellipsizes inside the card
   instead of overflowing it or crushing the name. */
.cl-name { font-family: var(--ui); font-weight: 600; font-size: 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 7ch; }
.cl-val { font-family: var(--mono); color: var(--amber); white-space: normal; flex: 0 1 auto; min-width: 0; }
/* ⚠ **AND THE HALF THAT WAS PROTECTED IS THE HALF THAT LIES WHEN IT IS CUT.** The comment above is
   right that the name must keep a floor, and the consequence went unmeasured: `flex: 0 1 auto` on
   the value means the VALUE is what gives way, and at a true 310px `826 tiros a la cabeza` rendered
   in 27px with 14% of it on screen — `826 t…`, which reads as a number and a word and is neither.
   `.squad-row` already states the rule this breaks: a truncated name with an ellipsis is honest, a
   truncated figure is not.

   So on a phone the pair wraps instead of competing. The name keeps the first line, the figure and
   its unit take the second whole, and neither is elided at any width. */
/* ⚠ The wrap is at EVERY width, not only on a phone — the overview puts these lines in a column
   that is narrow on a desktop too, so `826 tiros a la cabeza` was still cut to 71% of itself at
   127px well above the phone breakpoint. A rule aimed at a breakpoint rather than at the text is
   right on one side of it and wrong on the other. The pair only wraps when it has to, so a short
   figure still sits beside the name. */
.cl-line { flex-wrap: wrap; row-gap: 0.1rem; }
@media (max-width: 560px) {
  .cl-val { flex: 1 0 100%; font-size: 0.86rem; }
}

.ov-squad-row.sq-click { cursor: pointer; }
.ov-squad-row.sq-click:hover .sq-name { color: var(--amber-hi); }
#view-overview > .panel + .panel { margin-top: 1.75rem; }

/* Clickable category (overview leaders / my-stats rankings) → leaderboard category */
.cat-click { cursor: pointer; }
.cl-row.cat-click:hover, .rank-chip.cat-click:hover { border-color: var(--line-amber); }
.rank-chip > .ico { color: var(--amber); flex: none; }

/* ── My Stats: prisoner skills ────────────────────────────────────────────── */
.skills { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.9rem; }
.skill-group { display: flex; flex-direction: column; gap: 0.55rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.85rem 0.9rem; }
.sg-head { display: flex; flex-direction: column; align-items: center; gap: 0.45rem; border-bottom: 1px solid var(--line-amber); padding-bottom: 0.55rem; margin-bottom: 0.35rem; }
.sg-titlewrap { display: flex; align-items: center; gap: 0.45rem; }
.sg-titlewrap > .ico { color: var(--amber); font-size: 1.15rem; }
.sg-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--amber); font-weight: 700; }
.attr-ring { width: 84px; height: 84px; }
.ar-bg { fill: none; stroke: var(--line); stroke-width: 4.5; }
.ar-fg { fill: none; stroke: var(--amber); stroke-width: 4.5; stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; transition: stroke-dashoffset 0.5s ease; }
.ar-num { fill: var(--text); font-family: var(--mono); font-size: 21px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
.skill-row { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 0.2rem 0.6rem; }
.sk-name { font-weight: 600; }
.sk-tier { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.sk-bar { grid-column: 1 / -1; height: 5px; background: var(--inset); border: 1px solid var(--line); border-radius: 3px; overflow: hidden; }
.sk-fill { display: block; height: 100%; background: var(--amber); }
.sk-l0 .sk-fill { background: var(--faint); }
.sk-l1 .sk-fill { background: var(--standby); }
.sk-l2 .sk-fill { background: var(--silver); }
.sk-l3 .sk-fill { background: var(--amber); }
.sk-l4 .sk-fill { background: var(--gold); }
.sk-l3 .sk-tier { color: var(--amber); }
.sk-l4 .sk-tier { color: var(--gold); }

/* ── Economy: market activity (recent trades) ─────────────────────────────── */
.econ-market-stats { margin-bottom: 0.9rem; }
/* Section sub-labels (Activity by outpost / Most bought / Recent trades …) */
.econ-hot-h { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); margin: 1.4rem 0 0.6rem; }
.econ-hot-h.buy { color: var(--alarm); }
.econ-hot-h.sell { color: var(--online); }
/* Hot items, split into bought / sold columns */
.hot-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; }
.econ-hot { display: flex; flex-direction: column; gap: 0.5rem; }
.hot-item { display: flex; align-items: center; gap: 0.7rem; cursor: pointer; text-align: left; padding: 0.6rem 0.7rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); color: var(--text); font-family: var(--ui); }
.hot-item.nolink { cursor: default; }
.hot-item:not(.nolink):hover { border-color: var(--line-amber); }
.hot-img { width: 46px; height: 46px; object-fit: contain; flex: none; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 2px; }
.hot-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
/* The item, on a card about that item. `Medical Treatment Service` showed 84% in 168px — a fifth
   of a name is still a name nobody can read back. */
.hot-name { font-weight: 600; font-size: 0.95rem; white-space: normal; overflow-wrap: anywhere; }
.hot-val { font-family: var(--mono); font-size: 0.85rem; color: var(--amber-hi); }
.hot-val em { color: var(--muted); font-style: normal; }
.hot-cols > * { min-width: 0; } /* let grid columns shrink instead of overflowing the page */
@media (max-width: 560px) { .hot-cols { grid-template-columns: 1fr; } }
.econ-trades { list-style: none; margin: 0 0 1.1rem; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.etr { display: grid; grid-template-columns: auto auto 1fr auto; align-items: baseline; gap: 0.1rem 0.6rem; padding: 0.6rem 0.7rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.etr-img { grid-column: 1; grid-row: 1 / 3; align-self: center; width: 40px; height: 40px; object-fit: contain; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 2px; }
.etr-img-x { display: flex; align-items: center; justify-content: center; color: var(--faint); }
.etr-act { grid-column: 2; grid-row: 1; font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
.etr.etr-sell .etr-act { color: var(--online); }
.etr.etr-buy .etr-act { color: var(--alarm); }
.etr-item { grid-column: 3; grid-row: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.etr-price { grid-column: 4; grid-row: 1; font-family: var(--mono); color: var(--amber-hi); white-space: nowrap; }
.etr-sub { grid-column: 2 / -1; grid-row: 2; font-size: 0.74rem; color: var(--muted); font-family: var(--mono); }
/* ⚠ **THE WORST CUT ON THIS WHOLE SURFACE, AND IT WAS THE ONE THING THE ROW IS ABOUT.** Four tracks
   on one line — a 40px thumbnail, the word SOLD, the item's NAME and the price — and only the name
   is the `1fr`. At a true 310px the three inflexible ones take everything: `5.56x45mm Tracer Ammo`
   rendered in **5px with 2% of it on screen**, a trade log in which no trade names its item.

   Reading order decides the fix rather than arithmetic: what was bought is the sentence, and how
   much it went for is the footnote. So on a phone the NAME takes the whole width on its own line
   and wraps rather than eliding, with the direction and the price under it and the who/where/when
   below that. Nothing on the row is truncated at any width. */
@media (max-width: 560px) {
  .etr { grid-template-columns: auto minmax(0, 1fr) auto; }
  .etr-img { grid-row: 1 / 4; }
  .etr-item {
    grid-column: 2 / -1; grid-row: 1;
    white-space: normal; overflow: visible; text-overflow: clip;
  }
  /* The direction is two words in several languages — `a acheté`, `ha comprato`, `αγόρασε` — and
     it sits in the flexible track beside a price that sits in an inflexible one, so it was the half
     that gave way. It wraps here rather than taking a column floor: a floor wide enough for the
     longest of the seventeen would be dead space in the other sixteen. */
  .etr-act { grid-column: 2; grid-row: 2; white-space: normal; }
  .etr-price { grid-column: 3; grid-row: 2; }
  .etr-sub { grid-column: 2 / -1; grid-row: 3; }
}
.etr-pager { display: flex; align-items: center; justify-content: center; gap: 0.8rem; margin: 0.7rem 0 0.2rem; flex-wrap: wrap; }
.etr-pager:empty { display: none; }
.etr-pg { cursor: pointer; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); color: var(--text); font-family: var(--ui); font-weight: 600; font-size: 0.82rem; padding: 0.4rem 0.8rem; }
.etr-pg:hover:not(:disabled) { border-color: var(--line-amber); color: var(--amber); }
.etr-pg:disabled { opacity: 0.4; cursor: default; }
.etr-pg-info { font-family: var(--mono); font-size: 0.85rem; color: var(--muted); }
.etr-qty { color: var(--amber); font-style: normal; font-size: 0.82rem; }
.etr-ea { color: var(--muted); font-style: normal; font-size: 0.7rem; }
.item-history .ith-act em { color: var(--amber); font-style: normal; font-size: 0.82rem; }

/* ── Overview: online players ─────────────────────────────────────────────── */
.online-players { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* The left padding is smaller than it was because the chip now leads with a face, which is a round
   box that brings its own optical margin — the same reason a pill with an icon in it is padded
   asymmetrically everywhere else on this page. */
.op-chip { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.75rem 0.3rem 0.45rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--pill); font-size: 0.86rem; font-weight: 600; color: var(--text); }
.op-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--online); box-shadow: 0 0 6px var(--online); }
.op-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 12rem; }
.op-chip.pl-click { cursor: pointer; transition: border-color 0.12s, color 0.12s, background 0.12s; }
.op-chip.pl-click:hover { border-color: var(--line-amber); color: var(--amber-hi); background: var(--panel-2); }
.op-empty { padding: 0.3rem 0; }

/* ── My Stats: bank cards ─────────────────────────────────────────────────── */
.bank-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 0.7rem; margin-top: 0.9rem; }
.bank-card { display: flex; gap: 0.85rem; padding: 0.75rem 0.85rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.bank-card-img { width: 88px; height: 56px; flex: none; object-fit: contain; align-self: center; }
.bank-card-body { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }
.bank-card-type { font-weight: 700; color: var(--amber); text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.85rem; }
.bank-card-rows { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.82rem; color: var(--muted); }
.bank-card-rows b { color: var(--text); font-family: var(--mono); }
.pin-btn { cursor: pointer; background: var(--panel-2); border: 1px solid var(--line-amber); border-radius: var(--radius); color: var(--text); font-family: var(--mono); font-size: 0.82rem; letter-spacing: 0.08em; padding: 0.08rem 0.5rem; }
.pin-btn em { font-style: normal; color: var(--muted); font-size: 0.68rem; letter-spacing: 0; margin-left: 0.2rem; }
.pin-btn.revealed { color: var(--amber-hi); border-color: var(--amber); }

.fin-acc { display: flex; align-items: center; gap: 0.7rem; margin-top: 0.7rem; padding: 0.6rem 0.8rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.fin-acc > .ico { color: var(--amber); font-size: 1.35rem; flex: none; }
.fin-acc-t { display: flex; flex-direction: column; min-width: 0; }
.fin-acc-k { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.fin-acc-v { font-family: var(--mono); font-size: 1.05rem; overflow-wrap: anywhere; }

/* ── Mobile polish: reclaim width + collapse wide grids so nothing overflows ─ */
@media (max-width: 640px) {
  main { padding: 1rem 0.8rem; }
  .panel { padding: 0.95rem 0.85rem; }
  .bank-card-img { width: 70px; height: 46px; }
  .modal { padding: 3vh 0.55rem; }
  .modal-card { --modal-pad: 1rem; padding: 1.15rem var(--modal-pad); }
  .record-name { font-size: 1.3rem; }
  .foot-cols { padding: 1.2rem 0.9rem 1rem; gap: 1.1rem; }
  .foot-bottom { padding-left: 0.9rem; padding-right: 0.9rem; }
  /* Every panel header (Leaderboards, Squads, Economy, Events, Kill Feed, Items…):
     stack the toolbar under the title on its own full-width row and let the search
     grow, instead of cramming title + search + toggles + refresh into one line. */
  .panel-head { flex-wrap: wrap; row-gap: 0.6rem; }
  .lb-tools { width: 100%; gap: 0.5rem; }
  .lb-tools .search { flex: 1 1 auto; }
  .lb-tools .search input { width: 100%; }
  .lb-filter select#lb-category { max-width: none; width: 100%; }
}
@media (max-width: 480px) {
  .econ-deals, .cl-items, .squad-members, .bank-cards, .skills { grid-template-columns: 1fr; }
}
@media (max-width: 400px) {
  .dstats { grid-template-columns: 1fr; }
}

/* ── Item database tab ────────────────────────────────────────────────────── */
.items-toolbar { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 1.1rem; }
/* `background-color`, NOT `background`. The shorthand resets `background-image` to `none`, which
   would take the control floor's chevron off every dropdown in this toolbar — the exact defect this
   rule was the last one carrying. It is the one thing a component here must not say. */
.items-toolbar input, .items-toolbar select {
  background-color: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.95rem; padding: 0.55rem 0.75rem;
}
/* …and the room the arrow needs, because this rule states its own padding and the floor's
   `padding-right` loses to it. */
.items-toolbar select { padding-right: 2.3rem; }
.items-toolbar input { flex: 1; min-width: 200px; }
.items-toolbar input:focus, .items-toolbar select:focus { outline: none; border-color: var(--line-amber); }
/* `min()` even at 150px: `../CLAUDE.md` records a 300px minimum overflowing a 310px viewport by
   19px, because the panel this grid sits in carries 1.2rem of padding a side that no declaration
   mentions. A bare length here is one card-size change away from the same defect. */
.items-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr)); gap: 0.6rem; }
.item-card {
  display: flex; flex-direction: column; align-items: center; gap: 0.4rem; cursor: pointer;
  padding: 0.75rem 0.5rem; background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius); color: var(--text); font-family: var(--ui); text-align: center;
}
.item-card:hover { border-color: var(--line-amber); }
.item-img { width: 64px; height: 64px; object-fit: contain; flex: none; }
.item-img-x { display: flex; align-items: center; justify-content: center; color: var(--faint); }
.item-img-x .ico { width: 2rem; height: 2rem; }
.item-name { font-size: 0.82rem; font-weight: 600; line-height: 1.2; }
/* The card's second line — the family it belongs to. `min-width: 0` because the card is a flex
   COLUMN and a long subcategory ("Modular building elements · Walls") would otherwise push the
   tile wider than its grid track rather than wrapping inside it. */
.item-meta {
  min-width: 0; font-size: 0.66rem; line-height: 1.25; color: var(--muted);
  letter-spacing: 0.02em; overflow-wrap: anywhere;
}
.item-rar { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.item-rar.r-uncommon { color: var(--online); }
.item-rar.r-rare { color: #5aa9e6; }
.item-rar.r-veryrare { color: #b07cff; }
.item-rar.r-extremelyrare { color: var(--amber-hi); }
.items-foot { margin-top: 1.1rem; text-align: center; }
.item-detail { display: flex; gap: 1.3rem; flex-wrap: wrap; margin-top: 0.5rem; }
.item-detail-img { width: 150px; height: 150px; object-fit: contain; flex: none; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 8px; }
.item-detail-body { flex: 1; min-width: 240px; }
/* `white-space: pre-line` is gone from here on purpose: it was this page's only answer to the
   game's own paragraph breaks and it could not tell a paragraph from a wrapped line. `gameText.js`
   emits real `<p>` and `<br>` now, so the spacing is typography rather than preserved whitespace. */
.item-desc { color: var(--text); margin: 0 0 1.1rem; line-height: 1.5; }
/* The game ships no description for 208 of the published records, and a gap where the text belongs
   reads as a page that failed to load it. Muted and italic so the sentence is plainly OURS and is
   never mistaken for the record's own words the way `.item-desc` above would be. */
.item-desc-none { font-style: italic; }
/* ── THE GAME'S OWN STYLING, honoured rather than thrown away ─────────────────────────────────
   SCUM writes its manual and its descriptions with nine styling spans of its own, and `<gold>` /
   `<Important>` are the game telling a reader WHICH SENTENCE MATTERS — `<gold>IMPORTANT - </>`.
   Stripping them to plain text would have been the easy fix and would have lost that.
   The game's own colour names are NOT reproduced literally. `<blue>` against this near-black
   ground is the one thing `check-contrast` exists to catch, and a page that suddenly paints in
   SCUM's palette stops looking like this product. The two roles survive instead, in our own
   tokens: `--amber` is what this site highlights a named thing with, `--standby` is its gold.
   `.gt-colour` is the exception and keeps the game's literal `#RRGGBB`, because there the game
   chose a specific colour and it appears 15 times in the entire game (0 of them published). */
.gt-p { margin: 0 0 0.75rem; }
.gt-p:last-child { margin-bottom: 0; }
.gt-blue { color: var(--amber); }
.gt-gold, .gt-important, .gt-default { color: var(--standby); font-weight: 600; }
.gt-gray { color: var(--muted); }
.gt-key { color: var(--amber); font-weight: 600; letter-spacing: 0.02em; }
.gt-sub { font-size: 0.85em; vertical-align: sub; }
/* Why every card in the grid is a box outline: the free tier is served names without art. One line
   above the grid rather than a badge on each of forty-eight cards. */
.items-note {
  margin: 0 0 0.9rem; padding: 0.6rem 0.8rem;
  border-left: 2px solid var(--line-amber); border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--bg-2); line-height: 1.5;
}
/* The second sentence of an empty state — the one that says what FILLS it. A block of its own so it
   wraps under the short sentence instead of running on from it, at every width; `check-mobile` and
   the render rig are why it states no width of its own. */
.empty-hint { display: block; margin-top: 0.25rem; opacity: 0.85; line-height: 1.5; }
/* A sentence the GAME wrote, sitting inside the record rather than above it: what a medicine does,
   what the codex says about a mechanic, the repair cap. Set apart from our own labels by an amber
   rule down the left so a reader can tell the game's voice from the page's. `pre-line`, because the
   codex ships hard line breaks inside one block. */
.rec-note {
  margin: 0.35rem 0 1rem; padding: 0.5rem 0 0.5rem 0.8rem;
  border-left: 2px solid var(--line-amber); color: var(--muted);
  font-size: 0.9rem; line-height: 1.55; white-space: pre-line;
}
/* A heading INSIDE a codex article. The entry's own `<h3>` is the group heading ("Codex"); these are
   the game's section titles under it, so they read as body copy with weight rather than as another
   uppercase amber rule — a second `h3` would read as a new section of the record. */
.rec-sub {
  margin: 1.1rem 0 0.2rem; font-size: 0.95rem; font-weight: 700; line-height: 1.3; color: var(--text);
}
.rec-sub:first-child { margin-top: 0.3rem; }
/* …and its illustration. The height cap is here because a codex plate would otherwise push every
   paragraph after it off the screen on a phone.

   ⚠ `width: 100%` WITH A HEIGHT CAP DRAWS A FRAME ROUND EMPTY SPACE, and every plate in the game
   hits it: all 98 are 1586×892 or 1586×934, so at the card's ~1080px the picture wants 607px of
   height, the 22rem cap letterboxes it to 352px — and `object-fit: contain` then paints a 626px
   picture inside a 1080px BOX, left-aligned, with 450px of bordered `--inset` beside it. The
   element was the full width; the image was not.

   `width: auto` lets the replaced element size itself to the picture under both caps, so the
   border hugs what it frames. `object-fit` is gone with it: there is nothing to letterbox any
   more, and a declaration that no longer does anything is one somebody restores a reason for. */
.rec-fig {
  display: block; width: auto; max-width: 100%; max-height: 22rem;
  margin: 0.3rem 0 1rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset);
}
@media (max-width: 480px) { .items-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); } }

/* ── Item detail extras ───────────────────────────────────────────────────── */
.item-detail-side { flex: none; }
.repair-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.2rem; }
.repair-chip {
  display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer;
  padding: 0.4rem 0.7rem 0.4rem 0.45rem; background: var(--inset);
  border: 1px solid var(--line); border-radius: var(--radius); color: var(--text);
  font-family: var(--ui); font-size: 0.85rem;
}
.repair-chip:hover { border-color: var(--line-amber); color: var(--amber-hi); }
.repair-chip img { width: 28px; height: 28px; object-fit: contain; flex: none; }
.price-buy .ds-v { color: var(--alarm); }
.price-sell .ds-v { color: var(--online); }
.item-detail-body .item-rar { font-size: 0.7rem; vertical-align: middle; margin-left: 0.4rem; }
.record-name .item-rar { font-size: 0.7rem; margin-left: 0.5rem; }
/* Clickable economy rows → item detail */
.etr-click { cursor: pointer; }
.etr-click:hover .etr-item { color: var(--amber-hi); }

/* ── Item price history + back button ─────────────────────────────────────── */
.item-back {
  display: inline-flex; align-items: center; cursor: pointer; margin-right: 1.1rem;
  background: var(--inset); border: 1px solid var(--line-amber); border-radius: var(--radius);
  color: var(--amber); font-family: var(--ui); font-size: 0.72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em; padding: 0.2rem 0.55rem;
}
.item-back:hover { color: var(--amber-hi); background: var(--panel-2); }
.item-history { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; max-height: 280px; overflow-y: auto; }
.ith { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; padding: 0.4rem 0.6rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); font-size: 0.85rem; }
.ith-act { font-family: var(--mono); font-size: 0.72rem; min-width: 4.5rem; }
.ith-sell .ith-act { color: var(--online); }
.ith-buy .ith-act { color: var(--alarm); }
.ith-price { font-family: var(--mono); color: var(--amber-hi); }
.ith-sub { color: var(--muted); font-size: 0.76rem; margin-left: auto; }

.repair-chip .chip-def { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--bg); background: var(--amber); border-radius: 2px; padding: 0.05rem 0.3rem; margin-left: 0.3rem; }

/* Compatibility matrix chips */
.chip-tag { font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); background: var(--bg-2); border: 1px solid var(--line); border-radius: 2px; padding: 0.05rem 0.3rem; margin-left: 0.3rem; }
.repair-chip .chip-tag:first-of-type { margin-left: 0.4rem; }
/* clickable code chip (compatibility / variants / recipes) → opens that item */
button.chip-link { font-family: inherit; text-transform: none; letter-spacing: 0; cursor: pointer; margin: 0.15rem 0.15rem 0 0; }
button.chip-link:hover { color: var(--text); border-color: var(--accent, var(--amber)); }
/* related-item chips with thumbnail + name (compatibility / variants / recipes / fits vehicles) */
.rel-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
/* The rest of a long chip group, behind one line. "Used to craft" on a common tool is a hundred
   things, and rendered flat it buried every other section on the page. Ghost hover — no fill, the
   colour change is the whole affordance. */
.rel-more { margin-top: 0.35rem; }
.rel-more > summary {
  cursor: pointer; font-size: 0.75rem; color: var(--muted); padding: 0.2rem 0.1rem;
  --hov-bg: transparent; --hov-fg: var(--text);
}
.rel-more > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }
.rel-more[open] > summary { margin-bottom: 0.35rem; }
/* max-width: 100% on the CHIP, min-width: 0 on the name. Without both, a chip is as wide as its
   text: `.rel-chip-n` had `max-width: 16rem` and an ellipsis, but a flex item's floor is its content
   and 16rem is 256px, so a real published name — "5.56x45mm Armor Piercing Ammunition Box" — pushed
   the modal 51px past a 310px phone. It was invisible until the fixtures carried real names: every
   canned chip in the rig was short enough to fit. Same shape as the bare `minmax(N, 1fr)` rule —
   a length that cannot shrink, inside a box that has to. */
.rel-chip { display: inline-flex; align-items: center; gap: 0.4rem; cursor: pointer; font-family: inherit; max-width: 100%;
  color: var(--text); background: var(--bg-2); border: 1px solid var(--line); border-radius: 6px; padding: 0.25rem 0.5rem 0.25rem 0.3rem; font-size: 0.72rem; }
.rel-chip:hover { border-color: var(--accent, var(--amber)); }
.rel-chip img, .rel-chip .rel-chip-x { width: 26px; height: 26px; object-fit: contain; flex: none; }
.rel-chip .rel-chip-x { display: inline-flex; align-items: center; justify-content: center; color: var(--muted); opacity: 0.6; }
.rel-chip-n { white-space: nowrap; max-width: 16rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* attachments grouped by type */
.att-sub { margin: 0.35rem 0 0.5rem; }
.att-sub-h { display: block; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 0.25rem; }
/* crafting ingredients (amount × alternative options) */
.craft-list { margin-top: 0.4rem; }
.ing-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.35rem; }
.ing-amt { font-family: var(--mono); color: var(--accent, var(--amber)); font-size: 0.8rem; flex: none; min-width: 2rem; }
.ing-opts { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.ing-or { color: var(--muted); font-size: 0.7rem; margin: 0 0.1rem; }
/*
 * A quest OBJECTIVE, which is a sentence rather than a chip. Every other thing in an `.ing-opts`
 * row is an item chip with its own picture and its own box; this is the game's own translated
 * line, so it takes the page's ordinary text colour and is allowed to WRAP — a caption like
 * "Find a Deer Head at 80%+ durability" in a nowrap flex row is what scrolls a phone sideways.
 */
.q-obj { color: var(--text); font-size: 0.85rem; min-width: 0; overflow-wrap: anywhere; }
.craft-meta { margin: 0.2rem 0 0.7rem; display: flex; flex-wrap: wrap; gap: 0.15rem 0.55rem;
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
/* loot / where-to-find chips with rarity colour */
.loot-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.4rem; }
.loot-chip { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; color: var(--text);
  background: var(--bg-2); border: 1px solid var(--line); border-left-width: 3px; border-radius: 5px; padding: 0.2rem 0.5rem; }
.loot-w { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.loot-fixed { border-left-color: #7dd3fc; } .loot-abundant { border-left-color: #86efac; }
.loot-common { border-left-color: #a3e635; } .loot-uncommon { border-left-color: #fbbf24; }
.loot-rare { border-left-color: #fb923c; } .loot-veryrare { border-left-color: #f87171; }
.loot-extremelyrare { border-left-color: #c084fc; }
.att-grp { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--amber); margin: 0.7rem 0 0.4rem; }

/* Trader funds — show when each balance was last updated */
.trader-rv { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.15; }
.trader-upd { font-size: 0.62rem; color: var(--faint); white-space: nowrap; }

/* Weekly delta badge on My Stats values + small period toggles */
.ds-delta { font-size: 0.7rem; color: var(--online); font-family: var(--mono); margin-left: 0.3rem; white-space: nowrap; }
.seg-sm .seg-btn { padding: 0.2rem 0.5rem; font-size: 0.72rem; }
.ms-h3row { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap; }
.ms-h3row h3 { margin-top: 1.6rem; }

/* Gold capacity — inside the Trader funds section, slightly bigger */

/* Premium-locked item detail (non-subscribers) */
.item-locked{text-align:center;padding:2rem 1rem 2.4rem;max-width:440px;margin:0 auto;display:flex;flex-direction:column;align-items:center}
.item-locked-ic{display:inline-flex;width:52px;height:52px;align-items:center;justify-content:center;border-radius:50%;border:1px solid var(--line-amber,rgba(var(--accent-rgb),.35));color:var(--amber,var(--amber));margin-bottom:.7rem}
.item-locked-ic svg{width:26px;height:26px}
/* Beat the generic #modal-body h3 (flex, left-aligned, uppercase) so the heading centers. */
#modal-body .item-locked h3{display:block;margin:.2rem 0 .5rem;font-size:1.05rem;text-transform:none;letter-spacing:normal;color:var(--amber-hi)}
.item-locked p{max-width:38ch;margin:0}
/* WHICH RECORD THIS IS. The free tier answers a name and the card's own sentence promises it stays
   free, so it is the first thing on the panel and reads as the subject rather than as body copy. */
.item-locked-name{max-width:38ch;margin:0 0 .15rem;font-weight:700;font-size:1.05rem;color:var(--text)}

/* ── Premium upsell tab (Field Console) ───────────────────────────────────── */
.nav-btn.nav-premium.active,.nav-btn.nav-premium{color:var(--amber-hi)}
.prem-fc{max-width:920px;margin:0 auto;text-align:center}
.prem-fc-hero{padding:.8rem 0 1.4rem;border-bottom:1px solid var(--line);margin-bottom:1.4rem}
.prem-fc-ic{width:48px;height:48px;color:var(--amber);margin-bottom:.5rem}
.prem-fc-hero h2{display:block;margin:.3rem 0 .55rem;font-size:1.6rem;color:var(--amber-hi);text-transform:none;letter-spacing:normal}
.prem-fc-lead{margin:0 auto;max-width:60ch;color:var(--text);line-height:1.55}
.prem-fc-price{display:inline-flex;flex-direction:column;align-items:center;gap:.1rem;margin-top:1.1rem;padding:.5rem 1.1rem;border:1px solid var(--line-amber);border-radius:var(--radius)}
.prem-fc-price-row{display:flex;align-items:baseline;gap:.3rem}
.prem-fc-amt{font-family:var(--mono);font-size:2rem;font-weight:800;color:var(--amber-hi)}
.prem-fc-per{color:var(--muted);font-size:.85rem}
.prem-fc-extra{font-size:.72rem;color:var(--muted);white-space:nowrap}
.prem-fc-list{list-style:none;margin:0;padding:0;display:inline-flex;flex-direction:column;gap:.6rem;text-align:left}
.prem-fc-list li{position:relative;padding-left:1.7rem;line-height:1.45}
.prem-fc-list li::before{content:'✓';position:absolute;left:0;top:0;color:var(--online);font-weight:700}
.prem-fc-note{margin:1.4rem auto 0;max-width:60ch;line-height:1.5}

/* Floating "back to top" button (bottom-right) — fades in once the page scrolls. */
.back-top {
  position: fixed; right: 1.1rem; bottom: 1.1rem; z-index: 60;
  width: 46px; height: 46px; padding: 0; border-radius: 50%; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-2); border: 1px solid var(--line-amber); color: var(--amber-hi);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
  opacity: 0; visibility: hidden; transform: translateY(10px);
  transition: opacity 0.18s, transform 0.18s, visibility 0.18s, border-color 0.14s, color 0.14s;
}
.back-top.show { opacity: 1; visibility: visible; transform: none; }
.back-top:hover { background: var(--panel); border-color: var(--amber); color: var(--amber); }
.back-top svg { width: 20px; height: 20px; }

/* Global chat feed — reuses the kill-feed row layout; message flows after the name. */
.chatfeed .ch-msg { color: var(--text); word-break: break-word; }
/* A line the server sent (an admin broadcast through the bridge), not a player. It carries the
   accent rather than a name colour, and it is deliberately not clickable — there is no profile
   behind it. Same distinction the Discord relay has always made by writing "Server:". */
.chatfeed .ch-system { color: var(--amber-hi); font-weight: 700; cursor: default; }
.chatfeed .ch-sys-row { background: rgba(var(--accent-rgb), 0.06); }
.chatfeed .kf-line { flex-wrap: wrap; }
/* Chat avatar — same footprint as the kill-feed icon box, but round and covering. */
.chatfeed .ch-avatar { border-radius: 50%; object-fit: cover; padding: 0; }

/* ══ Motion design ═══════════════════════════════════════════════════════════
   Page transitions, reveal animations and microinteractions. Everything here is
   CSS-only, restarts naturally when an element is re-shown (display toggles) and
   is fully disabled by the prefers-reduced-motion rule above. */

/* Screen transition: each tab fades in and rises slightly when it becomes
   visible (display:none → block restarts the animation on every switch). */
@keyframes viewIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.view { animation: viewIn 0.28s ease both; }

/* Intro: the header content settles in once on page load.

   ⚠ **THIS ONE MOVES AND DOES NOT FADE, AND THE REASON IS A MEASUREMENT: AN ENTRY ANIMATION MUST
   NEVER BE THE THING THAT MAKES THE NAVIGATION VISIBLE.** It was
   `from { opacity: 0; transform: translateY(-8px) }` with `both`, and the `opacity: 0` is the whole
   defect: a CSS animation does not begin when it is created, it begins on the browser's next
   RENDERING UPDATE, and until that frame arrives it is **play-pending** — `startTime: null` with a
   hold time of `currentTime: 0`. Zero is INSIDE the active interval, so the first keyframe applies.
   The header controls and every tab in the bar are therefore `opacity: 0` for exactly as long as
   the browser has not painted, and `animation-fill-mode` CANNOT change that: fill governs the
   before and after phases, never time zero. Dropping `both` was tried first and the buttons stayed
   invisible, which is what proved it.

   Measured on 2026-09-18 inside `check-field-console`'s tab-bar view, which had been failing about
   one run in three and had already produced two wrong diagnoses (machine load, then smooth
   scrolling). The active tab read `topIn running pending=true startTime=null currentTime=0` and
   computed `opacity: 0` at 100, 300, 600, 1200 and 2500 ms, with `document.timeline` frozen at 89ms
   and **one** rAF callback in the whole run: that render produced a single frame and stopped. The
   button was 117×46 at 190,98 inside a 320px viewport — plainly on screen, and not reachable by any
   rule.

   ⚠ **AND NOTHING REBUILDS THE NAV, which is what a reading of "an animation at 0 ms" invites you
   to conclude.** A MutationObserver over the bar for the whole run recorded the premium tab's
   `hidden` flip and eleven text swaps from the i18n pass, and nothing else; the `<nav>` and the
   button were the same NODES from boot to measurement. An animation sitting at 0 ms is one that
   never started, not one that started twice.

   A renderer that owes you a frame and has not produced it is not only a test rig: a phone
   decoding a webfont, a 10k-wide island picture and fifty item cards is in exactly that state on
   the first paint, which is the one paint where the tab bar has to be there. So the intro keeps its
   settle and gives up its fade, and the from-state of the whole header is now simply the header,
   eight pixels high. The podium above keeps its fade because it is a decoration inside a screen a
   reader has already reached, not the bar they reach it with.

   `both` goes with it, on the same rules and for a smaller reason: the `forwards` half buys nothing
   here (the to-state IS the natural state) and the `backwards` half is dead without an
   `animation-delay`, while filling for ever keeps every one of these elements in its own stacking
   context — which the note beside `.hud-top` records as having painted the language menu
   underneath the nav bar. */
@keyframes topIn { from { transform: translateY(-8px); } to { transform: none; } }
.hud-top > * { animation: topIn 0.35s ease; }
.mainnav .nav-btn { animation: topIn 0.35s ease; }

/* Modal reveal: the card scales up from 98% with a fade. */
@keyframes modalIn { from { opacity: 0; transform: translateY(14px) scale(0.985); } to { opacity: 1; transform: none; } }
.modal-card { animation: modalIn 0.22s ease; }

/* Dropdown reveal (language picker). */
@keyframes ddIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.lang-dd-list { animation: ddIn 0.16s ease; }

/* Microinteractions: smooth state changes on every interactive control… */
.nav-btn, .tab-btn, .btn, .seg-btn, .lb-gpill, .etr-pg, .hot-item, .item-card,
.discord-invite, .admin-link, .player-chip, .lang-dd-btn, .lang-dd-item, .ham-btn,
.bunker-link, .ed, .drawer-close {
  transition: color 0.15s, border-color 0.15s, background-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
/* …a light press feedback on real buttons… */
.btn:active, .nav-btn:active, .tab-btn:active, .seg-btn:active, .etr-pg:active,
.item-card:active, .hot-item:active, .bunker-link:active { transform: translateY(1px); }

/* …and cards that lift towards the cursor. */
.bunker, .deal-card, .trader-card, .ds-card, .skill-group {
  transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s;
}
.bunker:hover, .deal-card:hover, .trader-card:hover {
  border-color: var(--line-amber);
  transform: translateY(-3px);
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.35);
}
.item-card:hover, .hot-item:hover { border-color: var(--line-amber); transform: translateY(-2px); }

/* Bunker/outpost photos zoom gently inside their card on hover. */
.bunker-img, .outpost-img { transition: transform 0.4s ease; }
.bunker:hover .bunker-img, .trader-card:hover .outpost-img { transform: scale(1.045); }

/* Themed scrollbars for the whole site (the map panel already had them). */
html { scrollbar-width: thin; scrollbar-color: rgba(var(--accent-rgb), 0.4) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(var(--accent-rgb), 0.3); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: rgba(var(--accent-rgb), 0.5); }

/* Footer "new version available" indicator (informational). */
.foot-update { margin-left: .5rem; color: var(--amber); text-decoration: none; }
.foot-update:hover { color: var(--amber-hi); text-decoration: underline; }
.foot-update.hidden { display: none; }

/* Numbered pages — Prev/Next alone meant twenty clicks to reach page twenty. */
.etr-pg-nums { display: inline-flex; align-items: center; gap: 0.25rem; flex-wrap: wrap; justify-content: center; }
.etr-pg-n { padding: 0.4rem 0.6rem; min-width: 2.1rem; font-family: var(--mono); }
.etr-pg-n.cur { border-color: var(--line-amber); color: var(--amber); background: var(--panel); cursor: default; }
/* The current page is `disabled` because pressing it does nothing — but it must not be FADED like
   the Prev/Next ends: this is the one number in the row that is meant to stand out, and the 0.4
   above would turn the highlight into the dimmest thing on the bar. */
.etr-pg-n.cur:disabled { opacity: 1; }
.etr-pg-gap { color: var(--muted); font-family: var(--mono); padding: 0 0.15rem; user-select: none; }

/* Pagination on a phone: keep Prev/Next and the pages nearest the current one, drop the outer
   neighbours (first and last always stay, so you can still jump to either end). Marked in the
   markup rather than computed here, so rotating the device reflows without a re-render. */
@media (max-width: 560px) {
  .etr-pager { gap: 0.45rem; }
  .etr-pg { padding: 0.4rem 0.6rem; font-size: 0.78rem; }
  .etr-pg-far { display: none; }
  .etr-pg-n { min-width: 1.9rem; padding: 0.4rem 0.45rem; }
}
@media (max-width: 380px) {
  .etr-pg-nums { gap: 0.15rem; }
  .etr-pg-n { min-width: 1.7rem; padding: 0.35rem 0.35rem; font-size: 0.75rem; }
}

/* Fullscreen: the whole widget, side panel included. `position: fixed` covers the browser
   window when the Fullscreen API is unavailable; under the real API the element already fills
   the screen and these values are harmless.

   The panel and the viewport are sized `height: 72vh` for the normal in-page layout — which in
   fullscreen left the bottom ~28% of the screen empty black. Both have to be released to 100%
   of the (now full-screen) wrapper, and `height`, not `max-height`, is what actually pins them. */
.map-wrap.map-fs {
  position: fixed; inset: 0; z-index: 4000; width: 100vw; height: 100vh; max-height: none;
  border-radius: 0; margin: 0;
}
.map-wrap:fullscreen { width: 100vw; height: 100vh; max-height: none; border-radius: 0; margin: 0; }
.map-wrap.map-fs .map-side, .map-wrap:fullscreen .map-side,
.map-wrap.map-fs .map-viewport, .map-wrap:fullscreen .map-viewport {
  height: 100%; max-height: none; min-height: 0;
}
.map-fs-b .ico { width: 14px; height: 14px; }

/* ── two players side by side (player profile → "Vs. you") ─────────────────────────────
   A ROW PER STAT, never two columns of cards. `.hot-cols` collapses to one column under 560px,
   which for a comparison destroys the only thing it is for — the two numbers have to stay beside
   each other or there is nothing to compare. So the row is a three-part grid whose middle column
   carries the label and shrinks first: it survives 320px, the width this page is measured at. */
:root { --vs-a: #5b9dff; --vs-b: #ff9a3c; }
/* ⚠ A STICKY BAR HAS TO COVER THE WHOLE WIDTH IT IS STICKING OVER, and this one covered its own
   box only. The card pads 1.6rem each side, the bar did not span that padding, and the rows scrolled
   past IN that gap -- so the two names stayed put while the comparison slid visibly beside and
   behind them. The owner's words: *"it is a strip that holds its position and then what I have
   scrolled past passes over it, which looks odd"*. The bar is right and it was see-through.

   The card's padding is a token now, so the bar spans it by NAME rather than by repeating 1.6rem
   here and 1rem again in the phone block -- the shape that drifts the first time one of them moves.
   The bottom rule is the other half: a bar with nothing under it reads as part of the row beneath. */
.vs-head {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: .4rem;
  margin: .2rem calc(-1 * var(--modal-pad, 1.6rem)) .1rem;
  padding: .45rem var(--modal-pad, 1.6rem);
  position: sticky; top: 0; z-index: 5; background: var(--panel);
  border-bottom: 1px solid var(--line);
}
.vs-head .vs-n { font-weight: 700; overflow-wrap: anywhere; }
.vs-head .vs-a { color: var(--vs-a); }
.vs-head .vs-b { color: var(--vs-b); text-align: right; }
.vs-mid { opacity: .6; display: inline-flex; }
.vs-note { margin: 0 0 .6rem; }
.vs-grid { display: flex; flex-direction: column; gap: .15rem; }
.vs-row { display: grid; grid-template-columns: minmax(3.2rem, auto) minmax(0, 1fr) minmax(3.2rem, auto);
  align-items: center; gap: .5rem; padding: .3rem .45rem; border-radius: .4rem; background: var(--panel-2, transparent); }
.vs-row .vs-k { display: inline-flex; flex-direction: column; align-items: center; gap: .15rem;
  min-width: 0; color: var(--muted); font-size: .86em; text-align: center; }
.vs-row .vs-k > span { overflow-wrap: anywhere; }
.vs-row .vs-v { font-variant-numeric: tabular-nums; white-space: nowrap; }
.vs-row .vs-v:last-child { text-align: right; }
/* The side that is ahead, by COLOUR — bold against not-bold is a difference a reader has to look
   for. Each column keeps its own colour, so the page reads as "this side / that side". */
.vs-win-a { color: var(--vs-a); font-weight: 700; }
.vs-win-b { color: var(--vs-b); font-weight: 700; }
/* …and the same fact without numbers: the smaller figure as a share of the larger. */
.vs-bar { display: flex; gap: 2px; width: 100%; max-width: 9rem; height: 3px; }
.vs-bar i { display: block; height: 100%; border-radius: 2px; min-width: 2px; }
.vs-bar-a { background: var(--vs-a); margin-left: auto; }
.vs-bar-b { background: var(--vs-b); margin-right: auto; }
.vs-un { opacity: .55; }
.vs-grp { margin: .6rem 0 .2rem; font-size: .8em; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.vs-rank { cursor: pointer; }
.vs-overall { background: var(--panel-2, transparent); }

/* ── a player's own map markers ─────────────────────────────────────────────────────────
   Everything about a marker is INSIDE the map now: the button that makes one, the card that
   manages one, and the message when a write is refused. The list that used to sit under the map
   was a second copy of the map's own list and vanished in fullscreen, which is exactly when
   somebody is working with it. */

/* The eight colours a marker can be. One place, read by the marker on the island, by the picker
   and by the swatch row — three surfaces that must agree or a green pin is a red dot in the list. */
:root {
  --mkc-amber: #ff9a3c; --mkc-red: #ff5c5c; --mkc-green: #54c98a; --mkc-teal: #3fc7c0;
  --mkc-blue: #5b9dff; --mkc-violet: #b58cff; --mkc-pink: #ff7ac2; --mkc-grey: #b8bcc2;
}
.mk-c-amber { --badge-bg: var(--mkc-amber); }
.mk-c-red { --badge-bg: var(--mkc-red); }
.mk-c-green { --badge-bg: var(--mkc-green); }
.mk-c-teal { --badge-bg: var(--mkc-teal); }
.mk-c-blue { --badge-bg: var(--mkc-blue); }
.mk-c-violet { --badge-bg: var(--mkc-violet); }
.mk-c-pink { --badge-bg: var(--mkc-pink); }
.mk-c-grey { --badge-bg: var(--mkc-grey); }

/* Tools: the caller's own controls, top-left of the viewport, opposite the zoom stack. */
/* THE MAP'S CONTROLS ARE ONE SET, so they look like one set. These were their own shape -- a
   translucent pill with a blur -- beside the +/- and fullscreen stack, which is a square amber
   button on the panel colour. Two vocabularies for the four things you can do to a map. They are
   the zoom stack's shape now, sized the same, with room for a word beside the picture. */
.map-tools { position: absolute; left: 10px; top: 10px; z-index: 15; display: flex; gap: 5px; }
.map-tool {
  height: 36px; display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem;
  padding: 0 0.6rem; background: var(--panel); border: 1px solid var(--line-amber);
  color: var(--amber-hi); border-radius: var(--radius-md); cursor: pointer; user-select: none;
  font-size: 0.8rem; line-height: 1;
}
.map-tool:hover { background: var(--panel-2); }
.map-tool .ico { width: 15px; height: 15px; }
/* A refusal where the gesture was, over the map so fullscreen keeps it. */
.map-flash {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 0.6rem; z-index: 6;
  max-width: calc(100% - 1.2rem); padding: 0.4rem 0.7rem;
  border: 1px solid var(--line-amber); border-radius: var(--radius);
  background: var(--panel); color: var(--text); font-size: 0.82rem;
}

/* The form. Two pickers, two fields, one choice — in that order, because the picture is what a
   reader is looking for and the words are what they fill in afterwards. */
.mk-form { display: flex; flex-direction: column; gap: 0.6rem; }
.mk-count { margin-left: auto; font-size: 0.8rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.mk-kinds {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 2.6rem), 1fr));
  gap: 0.3rem; max-height: 11rem; overflow-y: auto;
  padding: 0.4rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset);
}
.mk-kind {
  display: inline-flex; align-items: center; justify-content: center;
  aspect-ratio: 1 / 1; border: 1px solid transparent; border-radius: var(--radius);
  background: rgba(var(--fg-rgb), 0.04); color: var(--muted);
  --hov-bg: rgba(var(--fg-rgb), 0.12); --hov-fg: var(--text); --hov-line: transparent;
}
.mk-kind .ico { width: 18px; height: 18px; }
/* The chosen one wears the chosen COLOUR, so the picture in front of the reader is the marker they
   are about to get rather than a swatch they have to imagine applied. */
.mk-kind.on { color: var(--pick, var(--mkc-amber)); border-color: var(--pick, var(--mkc-amber)); background: rgba(var(--fg-rgb), 0.1); }
.mk-p-amber { --pick: var(--mkc-amber); } .mk-p-red { --pick: var(--mkc-red); }
.mk-p-green { --pick: var(--mkc-green); } .mk-p-teal { --pick: var(--mkc-teal); }
.mk-p-blue { --pick: var(--mkc-blue); } .mk-p-violet { --pick: var(--mkc-violet); }
.mk-p-pink { --pick: var(--mkc-pink); } .mk-p-grey { --pick: var(--mkc-grey); }
.mk-cols { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.mk-col {
  width: 1.5rem; height: 1.5rem; border-radius: 50%; border: 2px solid transparent;
  background: var(--badge-bg, var(--mkc-amber));
  --hov-bg: var(--badge-bg, var(--mkc-amber)); --hov-fg: var(--text); --hov-line: var(--text);
}
.mk-col.on { border-color: var(--text); }
/* Three states, one control: private, the squad sees it, the squad may change it. */
.mk-share3 { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.mk-s3 {
  display: inline-flex; align-items: center; gap: 0.3rem; flex: 1 1 auto; justify-content: center;
  padding: 0.4rem 0.5rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--inset); color: var(--muted); font-size: 0.82rem;
  --hov-bg: rgba(var(--fg-rgb), 0.08); --hov-fg: var(--text); --hov-line: var(--line);
}
.mk-s3.on { color: var(--amber); border-color: var(--line-amber); background: rgba(var(--accent-rgb), 0.12); }
.mk-err { color: var(--alarm); }
.mk-actions { display: flex; gap: 0.5rem; }
/* The card's own actions, inside the map popover. */
.mp-acts { display: flex; gap: 0.4rem; margin-top: 0.4rem; }
.mp-act {
  display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.5rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset);
  color: var(--text); font-size: 0.8rem;
  --hov-bg: rgba(var(--fg-rgb), 0.08); --hov-fg: var(--text); --hov-line: var(--line);
}
.mp-act-danger { color: var(--alarm); --hov-fg: var(--alarm); --hov-line: var(--alarm); }
.mp-act.armed { color: var(--alarm); font-weight: 700; border-color: var(--alarm); }

/* The second copy of this whole section used to sit here, and being second it WON.
   Everything it said was already said above, differently: the sticky bar's negative margins
   were reset to zero, and `.vs-row .vs-k` was overridden from a column back to a row. So the
   comparison rendered with its geometry from the stale copy and its colours from the live one,
   and a fix written into the block you can see changed nothing on the page -- which is exactly
   how it was found. `.vs-more` went with it: nothing has emitted that class since the winner
   became a colour instead of a weight. */

/* The old marker UI's stylesheet used to sit here: a bar, a list and a row per marker UNDER
   the map. That markup is gone -- a marker is managed on its own card inside the map, so it
   survives fullscreen -- and every class in it was dead, drawn by nothing. Two of them were
   worse than dead: `.mk-form` and `.mk-actions` are still emitted, so this stale copy went on
   overriding the live block above it on `gap`, which is the same trap the comparison section
   had. `duplicateRules` in the render rig now reports a selector declared twice whose two
   copies disagree. */

/* Placing a marker: the map says what it is waiting for and how to leave. A crosshair alone is a
   mode with no way out on a touch screen, where there is no cursor and no Escape key. */
.map-picking .map-viewport { cursor: crosshair; }
/* ⚠ CENTRED BY MARGINS, NOT BY `left: 50%`. An absolutely positioned box is shrink-to-fit, and its
   available width is the containing block MINUS its own `left` — so `left: 50%` gave this bar half
   the map to lay out in, whatever `max-width` said underneath. On a desktop half a map is plenty and
   nothing showed. At 310px it is about 155px, the sentence shrank to its longest word, and "Tap the
   map where the marker goes" rendered as SEVEN LINES, one word each, with Cancel wedged beside them.
   `left: 0; right: 0` gives it the whole width, `width: max-content` keeps it only as wide as it
   needs, and `margin-inline: auto` is what centres it. */
.map-pickbar { position: absolute; left: 0; right: 0; margin-inline: auto; top: .5rem; z-index: 6;
  width: max-content; display: flex; align-items: center; gap: .6rem; max-width: calc(100% - 1.2rem);
  padding: .35rem .6rem; border-radius: .4rem; background: var(--panel); border: 1px solid var(--line-amber);
  font-size: .85em; color: var(--text); }
.map-pickbar .map-pickx { --hov-bg: transparent; --hov-fg: var(--amber); --hov-line: currentColor;
  color: var(--amber); font-weight: 600; }

/* ── TOUCH FLOOR: nothing a finger has to find is smaller than a fingertip ─────────────────────
   THIS IS THE PHONE SURFACE. Most of the people who open the Field Console open it on a phone,
   and every control below was measured in a real browser at a true 310px viewport, across all
   seventeen languages, and came back between 18px and 39px. The hamburger — the ONLY way to the
   language picker, the Discord login and the admin link on a phone — was 37×34. The map's zoom and
   reset buttons were 36 and 27. The map card's ✕ was 22. Every notification switch was 23 tall.
   Every footer link was 18 to 22.

   None of that is visible to anything that reads a declaration, and check-mobile is right to stay
   quiet: a control's size here is padding plus a font plus what the flex parent had left, so there
   is no length in this file to judge. `cfg.tap` in the render rig is what measures it now, and
   `phone · nothing is smaller than a fingertip` in check-field-console.js is what keeps it measured.

   ⚠ IT GOES LAST, AND THAT IS THE WHOLE MECHANISM. A media query does not raise specificity, so a
   floor written next to the other phone rules near the top of this file loses to any component rule
   below it that names the same class — which is exactly what happened to `.map-zoom-b`, written as
   `width: 40px` in a block headed "raise small controls to ~40px" and beaten by the component's own
   `width: 36px` 460 lines further down. It was dead from the day it was written and no rendered
   check could see it, because 36px is not a defect any existing rule is about.

   ⚠ AND IT STATES `min-*`, NEVER `width`/`height`. A floor must not SHRINK anything: a control
   that is already comfortable keeps its size, and the glyph inside every one of these keeps its
   own — only the hit area grows. That is also what makes it safe to apply to a class this file has
   never heard of yet.

   40px rather than the 44 the admin panel uses: 44 is the accessibility figure and it is right for
   a screen an owner works on, but this surface is denser, it is read one-handed, and 40 is what the
   rest of this stylesheet's phone rules already promise. Raising the number is a separate decision
   with a layout cost; making the promise true is not.

   ⚠ AND THE HEIGHT IS THE SAFE AXIS. `min-width: <length>` on a FLEX ITEM replaces its automatic
   content floor (`min-width: auto`, which is what stops a flex item shrinking below its own text)
   with a number the browser is then free to shrink it to. Putting 40px on `.map-pickx` — the way
   out of the map's placing mode, a 60px button in a flex bar — let it shrink to 40 and cut "Cancel"
   off mid-word. So a control that CARRIES TEXT gets `min-height` only; `min-width` is for the
   square icon-only ones, where there is no text to lose and the narrow axis is the whole problem. */
@media (max-width: 640px) {
  /* Square, icon-only: both axes, and the glyph centres inside the bigger box. */
  .ham-btn, button.btn-icon, .modal-close, .drawer-close, .map-zoom-b, .map-zoom-btn, .mk-col {
    min-width: 40px; min-height: 40px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .mk-col { border-radius: 50%; }

  /* Carries text: height only, so nothing can shrink below its own word. */
  .lang-dd-btn, .ov-more, .btn.ms-act, .rel-chip, .mp-contents-btn, .link-copy,
  .map-tool, .map-pickbar .map-pickx, .mk-s3, .map-opts-top .map-toggle,
  .map-filters-head, .ml-sec-h, .ml-row, .ml-empties > summary,
  .switch, .search {
    min-height: 40px;
  }

  /* A ZONE'S NAME IS A LABEL ON THE ISLAND, NOT CHROME, and it is the one control here that must
     NOT grow: 40px of plate drawn over the map is a worse answer than 22px of plate. It gets the
     HIT AREA instead, which is what the markers beside it already do — `.mk-dot::before` puts
     `inset: -8px` of transparent slop around a 20px dot. 9px above and below reaches 40 without
     changing a pixel of what is drawn, and NONE sideways: a zone name sits among markers, and slop
     on that axis starts taking taps meant for them. 10px, not 9: the plate's own height is 20.2px
     (0.66rem of type on a 1.5 line box plus its padding and border), so 9 each side reached 38.2
     and the measurement said so. */
  .zn-name::before { content: ''; position: absolute; inset: -10px 0; }

  /* Paging is the one text control that needs the WIDTH too: the height was already 40 through
     `.etr-pg`, and a page number was 29 wide, which is the axis a thumb misses on. It is safe here
     for the same reason it is dangerous above — the content is one or two digits, and
     `.etr-pg-nums` wraps, so a long pager takes a second line instead of shrinking past a finger. */
  .etr-pg-n { min-width: 40px; }

  /* The map's two search fields, 24 and 30 tall. An input is not a flex item with text to lose. */
  .map-search-row input, .map-owner-row input { min-height: 40px; }

  /* THE PLACING BAR GOES UNDER THE TOOLS, because at this width they are on the same line. The
     tools sit at top:10px and are 40px tall once the floor above has had them, so a bar centred at
     top:.5rem runs straight through "Marker" and "Here" — its sentence behind them and its Cancel
     button beside them. There is room on a desktop and none here. 58px clears the row by 8. */
  .map-pickbar { top: 58px; }
  /* A CARD'S ✕ AND THE ROOM ITS TITLE LEAVES ARE ONE DECISION, NOT TWO. The close button is
     absolutely positioned and the head reserves a fixed margin for it, so growing one without the
     other slides the ✕ under the title — which is the same defect the admin panel's own map
     popover shipped with. 40px at right:2 reaches 42px in; the head leaves 2.6rem ≈ 44px. */
  .mp-close { width: 40px; height: 40px; top: 2px; right: 2px; font-size: 22px; }
  .mp-head { margin-right: 2.6rem; }

  /* The footer: seven links and a version notice, every one of them 18 to 22px tall. It is the
     only route to the app status, the Discord invite and the legal pages. */
  .foot-col a, .foot-nav a, .foot-social a, .foot-link, .foot-update {
    min-height: 40px; display: inline-flex; align-items: center;
  }

  /* ── and the two smallest things anybody has to READ ────────────────────────────────────────
     Measured across all 192 views in seventeen languages, the smallest type on this surface was
     10.5px, and it was exactly these two classes: the eyebrow above a record's name ("ITEM ·
     AMMUNITION · 5.56x45MM") and a bunker's state chip. Both are all-caps with letter-spacing,
     which is the shape that suffers most at that size. 0.68rem is 11.6px against the 106% root
     this stylesheet sets below 640px. Phones only — nothing about the desktop reading is wrong. */
  .record-label, .badge { font-size: 0.68rem; }
}
