/* ── Self-hosted display face ──────────────────────────────────────────────
   Chakra Petch, the same woff2 payloads scumsa.com serves (fonts/ChakraPetch-OFL.txt travels
   beside them, which is what OFL clause 2 asks for). These are here because `--ui` has named
   this family since the panel was written and NO FILE EVER SHIPPED: index.html asked
   fonts.googleapis.com for it, so an admin on a box with no internet — a firewalled game
   server, which is the normal case — silently got Segoe UI, and that is most of why the panel
   did not look like the site. It also handed every admin's IP to a third party on each load.

   Subsets are latin + latin-ext, matching the five admin locales. font-display:swap so a slow
   read never blanks the panel. pkg embeds the whole src/web/public tree, so they travel in the exe.

   NEVER WRITE A GLOB IN A CSS COMMENT. This paragraph first said "pkg embeds src/web/public "
   followed by a star-star-slash-star, and those middle characters CLOSE THE COMMENT — everything
   after them was parsed as CSS, which cost the very next rule: the 500 latin-ext face was silently
   dropped and only five of the six loaded. The symptom would have been the narrowest imaginable —
   accented characters at BODY weight, so a Czech or Polish admin would see the plain letters of a
   word in Chakra Petch and its diacritics in a fallback face, mid-word, and nowhere else. */
@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;}

/* ════════════════════════════════════════════════════════════════════════
   SCUM Server Automation — "TEC1 Field Terminal"
   A tactical surveillance/telemetry console for the prison-island server.
   Display: Chakra Petch · Data/readout: Share Tech Mono
   ════════════════════════════════════════════════════════════════════════ */

:root {
  /* WHAT THE BROWSER ITSELF PAINTS, which this document had never told it.
     Everything below 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 or colour picker. With no `color-scheme` those are all rendered
     in the LIGHT scheme — which is why a near-black panel came with pale grey scrollbars down every
     scrolling column and a white box beside every unticked label, and why `accent-color: var(--amber)`
     looked like it had only half worked: that property paints the TICK, and the empty box is the
     scheme's. One word fixes the whole class, and it is the only way to reach any of them.
     It is not a theme switch — this panel has one look — it is telling the browser which one. */
  color-scheme: dark;

  --bg:        #0b0c0e;
  --bg-2:      #0f1013;
  --panel:     #15171b;
  --panel-2:   #1c1f25;
  --inset:     #0b0c0e;

  --amber:     #ff6a1a;
  --amber-hi:  #ff9a3c;
  --on-amber:  #0b0c0e;   /* text on amber fills (buttons, filled badges) */
  --online:    #54c98a;
  --alarm:     #ff5c5c;
  --standby:   #e6b23a;

  --text:      #f1f3f5;
  --muted:     #8b9099;
  --faint:     #6b707a;

  --line:      rgba(241, 243, 245, 0.09);
  --line-2:    rgba(241, 243, 245, 0.05);
  --line-amber: rgba(255, 106, 26, 0.30);
  --hover-bg:  rgba(255, 255, 255, 0.05);  /* neutral hover fill on dark surfaces */
  /* Focused inputs already turn their border amber (border-color: var(--amber)); this ring used to
     add a SECOND concentric amber line just outside it — a double frame. Keep the single clean
     border only. (Every :focus rule that references this also sets border-color, so nothing loses
     its focus indicator; keyboard nav still gets the global :focus-visible outline.) */
  --focus-ring: none;

  /* Entity accents (map markers, lists, popovers) — one source of truth. */
  --c-player:  #35d07f;
  --c-base:    #ff9a2e;
  --c-vehicle: #5aa0ff;
  --c-chest:   #c9a24b;
  --c-storage: #bd7be6;
  --c-trap:    #e0503a;
  --c-turret:  #e8c33a;
  /* Named for the points-of-interest layer, which is gone — its places are drawn by the four
     layers that already had them. The token stays because it has one live reader that never
     was that layer: the dashboard's CPU series (`hist.cpu`, `var(--c-poi, #4fb9c9)`). Kept
     under its own name rather than renamed, because the fallback literal is repeated at that
     call site and a rename that missed it would silently change a chart colour. */
  --c-poi:     #4fb9c9;
  --offline:   #7c8594;
  --data-text: #b6bdc7;   /* raw data/log/INI readouts — one clear step below --text.
                             Was #b9c2ad, a warm sage tuned to the old olive ground. */

  /* Integrations */
  --discord:    #5865f2;
  --discord-dn: #4752c4;
  --discord-hi: #c9cdfb;

  /* 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. The panel used to pull it from
     fonts.googleapis.com, which meant the readouts rendered in Consolas on any box without
     internet (a firewalled game server is the common case) while looking correct here. One
     uniform answer beats a panel that looks different depending on the network. */
  --mono: 'Share Tech Mono', 'SF Mono', 'JetBrains Mono', 'Consolas', ui-monospace, monospace;
  --ui:   'Chakra Petch', 'Segoe UI', system-ui, Tahoma, sans-serif;

  --radius: 4px;      /* inputs, chips, small controls */
  --radius-md: 6px;   /* cards, rows */
  --radius-lg: 8px;   /* panels, modals, popovers */
  --pill: 999px;      /* fully rounded pills */
}

* { box-sizing: border-box; }
img { max-width: 100%; }
a:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }


/* Base scale ≈ 20px (124% of 16px) — sized so the UI reads comfortably at 100%
   browser zoom (matches what previously needed zooming to ~110%). Phones keep
   their own tuned scale; layouts there are tight already. */
html { -webkit-text-size-adjust: 100%; font-size: 124%; }
@media (max-width: 640px) { html { font-size: 106%; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--ui);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.01em;
}

/* faint scanlines — surveillance-monitor atmosphere */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(255, 255, 255, 0.018) 0,
      rgba(255, 255, 255, 0.018) 1px,
      transparent 1px,
      transparent 3px
    );
}

.hidden { display: none !important; }
/* …and the ATTRIBUTE means the same thing, which this sheet had never said.
   The panel's convention is the class above, but two controls use the attribute -- the map
   legend's clear button and the admin-nav back button -- and for those the only rule in play
   was the UA stylesheet's `[hidden] { display: none }`. That is specificity (0,1,0) from the
   USER-AGENT origin, so ANY author rule setting `display` beats it: one class ties and wins on
   order, a descendant selector wins outright. The result is an element the JS believes is
   hidden, sitting on the screen -- 91 findings across this panel's views once the render rig
   started asking unconditionally. The field console ships this same line for the same reason,
   and there is no reason for the two documents to answer differently. */
[hidden] { display: none !important; }

.screen {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

h1, h2, h3 {
  font-family: var(--ui);
  font-weight: 600;
  letter-spacing: 0.02em;
}

a { color: var(--amber); }

code {
  font-family: var(--mono);
  background: var(--inset);
  border: 1px solid var(--line);
  padding: 0.05rem 0.35rem;
  border-radius: 3px;
  color: var(--amber-hi);
  font-size: 0.9em;
}

.error {
  color: var(--alarm);
  font-size: 0.85rem;
  margin: 0;
  min-height: 1em;
}

/* ── Brand lockup ──────────────────────────────────────────────────────── */

.brand {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-width: 0;
  cursor: pointer;
}

.brand-logo {
  height: 50px;
  width: auto;
  flex-shrink: 0;
}

.brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.05;
  min-width: 0;
}

.brand-title {
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: 0.04em;
}

.brand-sub {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--amber);
}

/* ── Header / command bar ──────────────────────────────────────────────── */

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.75rem 1.5rem;
  background: linear-gradient(180deg, var(--panel), var(--bg-2));
  border-bottom: 1px solid var(--line);
}

/* ── Persistent topbar + tab nav (Field-Console-style) ─────────────────────── */
#app-shell { display: flex; flex-direction: column; min-height: 100vh; }
.topbar { position: sticky; top: 0; z-index: 30; transition: transform 0.25s ease; }
.topbar.nav-hidden { transform: translateY(-100%); }
.header-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
/* Manager lifecycle buttons in the header (update / restart / shut down). */
.header-sep { width: 1px; align-self: stretch; background: var(--line); margin: 0.2rem 0.1rem; }
.hdr-icon-btn { padding: 0.42rem 0.7rem; }
.mgr-danger:hover { border-color: var(--alarm) !important; color: #ff6b6b !important; }
/* Field Console is an <a>, so the button{} rules don't reach it — mirror the
   secondary button look so it sits identically next to Logout (no underline). */
.header-actions a.secondary {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem;
  font-family: var(--ui); font-weight: 600; font-size: 0.82rem; letter-spacing: 0.03em;
  padding: 0.55rem 1.05rem; border-radius: var(--radius); text-decoration: none;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--text);
  transition: background 0.14s, border-color 0.14s, color 0.14s;
}
.header-actions a.secondary:hover { border-color: var(--line-amber); color: var(--amber-hi); background: var(--panel-2); text-decoration: none; }
.header-actions a.secondary .ico { width: 16px; height: 16px; }
/* Full-screen overlay while the manager restarts / shuts down. */
.mgr-overlay { position: fixed; inset: 0; z-index: 9999; display: none; align-items: center; justify-content: center; background: rgba(8, 8, 6, 0.92); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.mgr-overlay-card { text-align: center; color: var(--text); padding: 2rem; max-width: 440px; }
.mgr-overlay-card p { margin-top: 1.1rem; color: var(--muted); line-height: 1.55; }
.mgr-overlay-ic { width: 44px; height: 44px; color: var(--amber); }
.mgr-spinner { width: 42px; height: 42px; margin: 0 auto; border: 3px solid var(--line); border-top-color: var(--amber); border-radius: 50%; animation: mgr-spin 0.9s linear infinite; }
@keyframes mgr-spin { to { transform: rotate(360deg); } }
.admin-nav { display: flex; gap: 0.25rem; padding: 0 1.5rem; background: var(--bg-2); border-bottom: 1px solid var(--line-amber); overflow-x: auto; }
.anav-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-weight: 600; font-size: 0.9rem;
  padding: 0.8rem 0.9rem; letter-spacing: 0.02em; text-transform: none;
}
/* The nav items are real <a href="#tab"> so the browser's own link behaviour works —
   middle-click / Ctrl-click opens the tab in a new browser tab, and they can be bookmarked.
   These two rules keep them looking exactly like the buttons they replaced. */
.anav-btn { text-decoration: none; }
.anav-btn:hover { color: var(--text); border-color: transparent; background: transparent; text-decoration: none; }
.anav-btn.active { color: var(--amber); border-bottom-color: var(--amber); }
.anav-btn .ico { font-size: 1.05rem; }

/* Plugins nav dropdown (DESKTOP with a real pointer only). The button still works everywhere; on a
   desktop it also gets a hover menu of plugins that ship a UI. The menu is fixed-positioned on <body>
   (JS) so the horizontally-scrolling nav can't clip it. On touch / narrow screens it's disabled. */
.anav-dd { display: inline-flex; }
.anav-dd.has-dd > .anav-btn::after { content: ''; margin-left: 0.3rem; align-self: center; border: 4px solid transparent; border-top-color: currentColor; border-bottom: 0; opacity: 0.55; }
.anav-dd-menu { position: fixed; z-index: 6000; min-width: 210px; max-width: calc(100vw - 16px); background: var(--panel); border: 1px solid var(--line-amber); border-radius: 10px; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55); padding: 6px; display: none; }
.anav-dd-menu.open { display: block; }
.anav-dd-item { text-decoration: none; display: flex; align-items: center; justify-content: flex-start; gap: 10px; width: 100%; padding: 0.5rem 0.6rem; background: none; border: 0; color: var(--text); cursor: pointer; border-radius: 7px; font-family: var(--ui); font-weight: 500; font-size: 0.88rem; text-align: left; }
.anav-dd-item:hover { background: var(--panel-2); color: var(--amber-hi, var(--amber)); text-decoration: none; }
.anav-dd-item img { width: 22px; height: 22px; border-radius: 5px; object-fit: contain; flex: none; }
.anav-dd-item .ico { width: 18px; height: 18px; color: var(--muted); flex: none; font-size: 1rem; }
.anav-dd-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.anav-dd-sep { height: 1px; background: var(--line); margin: 5px 4px; }
/* On touch the JS opens these menus by adding `.open` — so this may only hide the ones that are
   CLOSED. `display: none !important` on the element itself beat `.open`, so a tap opened the menu
   in the DOM and nothing was ever painted: Settings and its six screens had no route through the
   UI on a phone at all. The caret stays hidden on touch because there is no hover to preview it. */
@media (hover: none), (max-width: 760px) { .anav-dd.has-dd > .anav-btn::after { display: none; } .anav-dd-menu:not(.open) { display: none !important; } }

.tab-panel { flex: 1 0 auto; }
.tab-panel.hidden { display: none !important; }

/* Slim per-screen title bar (replaces the old sub-screen headers) */
.screen-bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 1.25rem 1.5rem 0; }
.screen-title { display: flex; align-items: center; gap: 0.5rem; font-size: 1rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--amber); }
.screen-title .ico { color: var(--amber); }
/* WRAPS. `.screen-bar` wraps its own two children, but that does nothing for the buttons INSIDE
   this one — a row of them just kept extending, and on the Plugins screen it reached 947px
   against a 430px phone and dragged the whole page sideways with it. Measured in a browser,
   not computed: there is no declared width here to find by reading. */
.screen-bar-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }

header h1 {
  margin: 0;
  font-size: 1.15rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.nav-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* ── Buttons ───────────────────────────────────────────────────────────── */

/* ── Unified button system ─────────────────────────────────────────────────
   One consistent look everywhere. Default = PRIMARY (amber). `.secondary` =
   outline, `.danger` / `.warning` = coloured. All buttons share size + shape so
   Save/actions look the same on every screen. */
button {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem;
  font-family: var(--ui); font-weight: 600; font-size: 0.82rem; letter-spacing: 0.03em;
  padding: 0.55rem 1.05rem; cursor: pointer; border-radius: var(--radius);
  border: 1px solid var(--amber); background: var(--amber); color: var(--on-amber);
  transition: background 0.14s, border-color 0.14s, color 0.14s, transform 0.05s;
}
/* The hover paint is a PAIR, and it is read from tokens so it cannot be split in half.
   For years the rule below hard-coded both values, and every button that wanted a different hover
   fill overrode `background` alone — inheriting dark `--on-amber` text onto a dark fill, which is
   invisible. The stylesheet is full of one-at-a-time patches for exactly that (see the "increment"
   blocks further down), and each one left the next case waiting.
   The way to retune a button's hover is now to set the trio ON THE ELEMENT, not to override the
   hover rule: `.my-btn { --hov-bg: transparent; --hov-fg: var(--text); --hov-line: var(--line); }`.
   Both halves then travel together by construction. `npm run check-contrast` fails the build on any
   that still state one half without the other. */
:root { --hov-bg: var(--amber-hi); --hov-fg: var(--on-amber); --hov-line: var(--amber-hi); }
button:hover { background: var(--hov-bg); border-color: var(--hov-line); color: var(--hov-fg); }
button:active { transform: translateY(1px); }
button .ico { font-size: 1.05em; }

button.secondary {
  background: var(--panel-2); border-color: var(--line); color: var(--text);
}
button.secondary:hover { background: var(--panel-2); border-color: var(--line-amber); color: var(--amber-hi); }

button.danger { background: rgba(255, 92, 92, 0.12); border-color: rgba(255, 92, 92, 0.4); color: var(--alarm); }
button.danger:hover { background: rgba(255, 92, 92, 0.2); border-color: var(--alarm); }

button.warning { background: rgba(230, 178, 58, 0.12); border-color: rgba(230, 178, 58, 0.4); color: var(--standby); }
button.warning:hover { background: rgba(230, 178, 58, 0.2); border-color: var(--standby); }

/* ── …and the FORM CONTROLS, which never had one ───────────────────────────
   Buttons have had a single unified system since this file was written. Inputs never did: every
   component states its own surface (`.mf-int input`, `.bpf-row input`, `.settings-field
   input[type=text]`, `.cmd-ip-qn`, `.eco-item .er-f input`, `.su-field input`) and about two
   hundred of them do it correctly — so a NEW component that states none does not inherit a
   near-miss, it falls through to the USER-AGENT stylesheet and is painted `field` on `fieldtext`:
   a white box with black text, on a panel whose every other surface is near-black. Both halves of
   that pair come from the browser, so no rule in this file is wrong on its own, `check-contrast`
   sees two correct declarations and nothing to compare, and the ratio is perfect — it is simply the
   wrong colours. Nothing that reads declarations can find it.

   The same hole one step further in is `accent-color`, which is what paints a native tick. Fourteen
   rules in this file remember it (`.map-toggle input`, `.bpf-bool input`, `.settings-field
   input[type=checkbox]`, `.eco-pick input`, `.adm-perm input`, `.cmd-check input`, …) and five
   components had forgotten — the dashboard's Skip-restart switch, the manager's auto-update switch,
   the economy group toggles, every checkbox on the in-game-mods screen and the zone workspace's —
   all of which rendered as the UA's white-when-off, browser-blue-when-on box. Measured, not
   guessed: a sweep of every `input`/`select`/`textarea` on every view the render rig drives found
   exactly three pale surfaces and those five, and nothing else.

   So the fact is written ONCE, here, the way the button system writes its own. `:where()` keeps the
   specificity at zero, which is the whole point: every component rule that already states a surface
   — including one that deliberately wants something else — beats this without a fight, and only a
   control that states nothing is touched. Types that are not a box of text are excluded by name,
   because a background on a range track or a colour swatch paints over the control itself. */
/* ⚠ THE SURFACE WAS ONLY HALF OF IT, and the other half is the FACE. Measured the same way, by
   rendering all 217 views and asking each control which family it is drawn in: three are drawn in
   **Arial**, the user agent's own control font — the economy row filter, the zone editor's island
   values and the map's in-game chat box. The zone one is the tell, because `.zw-isl-val` sits
   directly beside it in `var(--mono)` holding the same kind of number: two faces, one value, side
   by side. Nothing could see it. The colours are now right (that is what the block above did), the
   text is readable, it fits, and since this document declares its colour scheme it is not even the
   wrong colour any more — only the face gives it away, which is what the rig's `uafont` rule
   measures. Kept in the SAME :where() so there is one floor rather than two, and at zero
   specificity for the same reason as the colours: a component that states a face wins without a
   fight, and only a control that states nothing is touched. */
:where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]),
       select, textarea) {
  background-color: var(--inset);
  color: var(--text);
  font-family: var(--ui);
  font-size: 0.9rem;
  line-height: 1.3;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.45rem 0.55rem;
  /* A control must never be the thing that makes a phone scroll sideways. */
  max-width: 100%;
}
:where(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]),
       textarea)::placeholder { color: var(--faint); }
/* The popup list too: a dark <select> whose options are left to the UA gets dark text on a dark
   list on Windows, which is the same defect one layer down and only visible once the menu is open. */
:where(select) option { background-color: var(--panel); color: var(--text); }
:where(input[type="checkbox"], input[type="radio"]) { accent-color: var(--amber); }

button.btn-icon { padding: 0.5rem; }
button:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── Panels ────────────────────────────────────────────────────────────── */

.status-card,
.leaderboards,
.players,
.players-admin,
.bans,
.cmd-console,
.logs {
  position: relative;
  margin: 1.25rem 1.5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.15rem 1.4rem;
}

/* Front-style corner ticks on every panel. */
.status-card::before, .players::before, .players-admin::before, .bans::before, .cmd-console::before, .logs::before,
.status-card::after, .players::after, .players-admin::after, .bans::after, .cmd-console::after, .logs::after {
  content: ''; position: absolute; width: 10px; height: 10px;
  border-color: var(--line-amber); border-style: solid; border-width: 0; pointer-events: none;
}
.status-card::before, .players::before, .players-admin::before, .bans::before, .cmd-console::before, .logs::before {
  top: -1px; left: -1px; border-top-width: 2px; border-left-width: 2px;
}
.status-card::after, .players::after, .players-admin::after, .bans::after, .cmd-console::after, .logs::after {
  bottom: -1px; right: -1px; border-bottom-width: 2px; border-right-width: 2px;
}

.status-card { margin-bottom: 0; }
.status-card + .status-card { margin-top: 1.25rem; }
.controls + .status-card { margin-top: 0; }

/* ── The dashboard, on a monitor ───────────────────────────────────────────────────────────────
   Everything above lays a panel out as a full-width band, and the dashboard is EIGHT of them in a
   stack. That is right on a phone and wrong on anything else: measured on 2026-08-28, a 2560px
   screen drew every card 2491px wide with nothing in any of them wider than 780px — 1,700px of
   nothing down the right-hand side of a page 2,837px tall. The owner's words were "half the feed
   is empty space out to the side", and this block plus the one at FEED 03 is the answer to them.

   Two columns, and no more, and that is a decision rather than a limit. The width past two columns
   is spent INSIDE the cards, whose own grids all use `auto-fill`/`auto-fit` and were already able
   to take it: at 2560 the world card's blocks go five across, the history graph draws the same
   hours over twice the pixels, and the player list keeps its 210px tiles instead of stretching
   them. A third page-level column would have to come out of one of those.

   Which two cards share the top row is the editorial part. Vitals and Operations are the only two
   panels on the screen made of the same thing — short label/value readouts — and they answer the
   two questions somebody opens this page for, in order: *is it up and healthy* and *what is it
   about to do*. Side by side they read as one instrument strip. Everything below is a panel whose
   content genuinely wants the whole width: the world card's five blocks, the roster, the console's
   output, a time series and a log.

   1240px is where the pair fits without either half crushing its own grid: below it the two
   `.status-grid`s drop to two columns each and the strip is taller split than stacked.

   `#dashboard-screen > .status-card` is (1,1,0) and beats all three sibling rules above it, so the
   margins come off without an `!important`. Only the direct children are placed; a section added
   here later has no area, lands in an implicit row at the end, and is visible rather than lost. */
@media (min-width: 1240px) {
  #dashboard-screen {
    display: grid;
    /* `minmax(0, 1fr)` and never a bare length: a track floored at its content's width is how a
       card with one long log line drags the whole page sideways. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "vitals  ops"
      "ctrl    ctrl"
      "world   world"
      "players players"
      "console console"
      "hist    hist"
      "log     log";
    /* Each card is its own height. Stretched to the row, the shorter of the two top cards would
       grow a band of empty panel under its last reading — the same defect one axis over. */
    align-items: start;
    gap: 1.25rem 1.5rem;
    padding: 1.25rem 1.5rem;
  }
  #dashboard-screen > .status-card,
  #dashboard-screen > .controls,
  #dashboard-screen > .players,
  #dashboard-screen > .cmd-console,
  #dashboard-screen > .logs { margin: 0; }

  #dashboard-screen > .status-card.viewport { grid-area: vitals; }
  #dashboard-screen > #ops-card             { grid-area: ops; }
  #dashboard-screen > .controls             { grid-area: ctrl; }
  #dashboard-screen > #world-card           { grid-area: world; }
  #dashboard-screen > .players              { grid-area: players; }
  #dashboard-screen > #cmd-console          { grid-area: console; }
  #dashboard-screen > #hist-card            { grid-area: hist; }
  #dashboard-screen > .logs                 { grid-area: log; }
}

/* Eyebrow label — names the feed each panel carries.
   `flex-wrap` is load-bearing at a narrow width: icon + title + status chip + FEED id do not fit on
   a 227px phone card, and without it the row pushes the card's own edge out and the whole page
   scrolls sideways. Wrapped, `.feed-id`'s auto margin still holds it to the right of whatever line
   it lands on — a designated place on every width, which is the whole point. */
.panel-eyebrow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem 0.55rem;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--amber);
  margin-bottom: 1rem;
}

.panel-eyebrow .ico {
  width: 13px; height: 13px; flex: none; color: var(--amber);
}
/* The dot is the panel's "live" tick. A panel that leads with an icon has its marker already, and a
   second one beside it is just noise. */
.panel-eyebrow:has(.ico)::before, .panel-eyebrow:has(.live-dot)::before { content: none; }
.panel-eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--amber);
  box-shadow: 0 0 7px var(--amber);
}

.panel-eyebrow .feed-id {
  margin-left: auto;
  color: var(--faint);
  letter-spacing: 0.15em;
}

/* when a live dot is present, drop the static amber marker to avoid two dots */
.panel-eyebrow:has(.live-dot)::before { display: none; }

.panel-eyebrow .live-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--online);
  box-shadow: 0 0 8px var(--online);
  animation: pulse 1.6s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

/* ── Vitals viewport (primary status panel) — the signature element ────── */

.viewport {
  position: relative;
  border-color: var(--line-amber);
}

.viewport::before,
.viewport::after {
  content: "";
  position: absolute;
  width: 18px;
  height: 18px;
  border: 2px solid var(--amber);
  pointer-events: none;
}

.viewport::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.viewport::after  { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }

/* ── Status grid / telemetry readouts ──────────────────────────────────── */

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1.1rem 1.4rem;
}

.status-item {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding-left: 0.7rem;
  border-left: 1px solid var(--line-2);
  min-width: 0;   /* let long values wrap instead of overflowing the grid cell */
}

.status-item .label {
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

.status-item .value {
  font-family: var(--mono);
  font-size: 1.4rem;
  color: var(--text);
  letter-spacing: 0.01em;
  min-width: 0;
  overflow-wrap: anywhere;   /* break long strings (filenames) that have no spaces */
}
/* ── two value sizes, and the rule that picks one ──────────────────────────────────────────────
   A FIGURE is a number you take in at a glance and compare with the last time you looked: the
   player count, the frame rate, how much of the machine is in use. A FACT is a date, a build
   number, a service name, a word like "Up to date" — you read it when you need it and never
   compare it with anything.

   They were the same 1.4rem, so on a 1366 laptop — which is what most owners have — the Operations
   card printed `5. 7. 2026 23:00:00` down THREE LINES in a 153px column while shouting as loudly
   as the player count beside it. `#last-backup-value` had already been stepped down on its own for
   exactly this reason; `v-fact` is that fix stated as a rule instead of six times as an id.

   1.4rem mono is ~12px per character here, so a 153px cell holds twelve characters. Every reading
   that is longer than that by construction carries the class. */
.status-item .value.v-fact {
  font-size: 0.95rem;
  line-height: 1.35;
  /* The figures are 1.4rem on a 1.15 line box; a fact set two thirds that size would float in the
     middle of the row it shares. Pinning the top keeps every value on one baseline across the
     grid, whichever size it is. */
  align-self: flex-start;
}
#last-backup-value { font-size: 0.92rem; }
/* Memory is a FIGURE and keeps a figure's weight, but it is the only one of the seven that carries
   two of them: `9.0 / 32.0 GB` is thirteen characters, and Share Tech Mono at 1.4rem is ~13.9px a
   character, so it needs 181px — more than a cell gets at any width below 1920 and it wrapped onto
   a second line under the bar. 1.08rem is 139px, which fits the narrowest cell the dashboard
   produces. It is the same step `.lw-grid` takes for the same reason one card down. */
#memory-value { font-size: 1.08rem; }

/* The timestamp OF the readings, in the card's eyebrow rather than as an eighth reading beside
   them. Quiet by construction: it inherits the eyebrow's 0.68rem mono and only the time itself is
   lifted to the text colour. `margin-left: 0` because `.feed-id` beside it already claims the free
   space with `margin-left: auto`, and two auto margins in one flex row split it between them —
   which is how the console card's status chip ended up floating in the middle of its own eyebrow. */
.vt-stamp {
  margin-left: 0;
  display: inline-flex; align-items: baseline; gap: 0.35rem;
  color: var(--faint); letter-spacing: 0.15em;
}
.vt-stamp b { font-weight: 400; color: var(--muted); letter-spacing: 0.05em; }

/* ── a reading that is a fraction of a stated maximum ──────────────────────────────────────────
   Three of the eight vitals are: players of `MaxPlayers`, CPU of 100, memory of the machine's own
   total. Each gets the bar the world card draws a weather strength with — deliberately the same
   shape, because it is the same claim, and a second visual language for one idea is how a panel
   stops looking like one product.

   FPS and Entities have none, and that asymmetry is the point rather than an omission: neither has
   a maximum the game or the manager states, so a track under either would need a ceiling this
   panel made up. It is the rule that keeps wind in the game's own units on FEED 03.

   `.vt-track` is only ever painted when the denominator actually arrived — `updateStatus()`
   unhides it — so an unreadable maximum shows no bar rather than an empty one. And it is amber at
   every level: the manager states no CPU or memory threshold anywhere, and colouring a bar red at
   a number nobody configured is a judgement this card has no basis for. */
/* ⚠ `:not([hidden])` USED TO BE LOAD-BEARING HERE AND IS NOT ANY MORE. Kept because it is honest
   about intent and costs nothing, but do not read it as the thing holding this up.

   The original defect: `hidden` worked only because the UA stylesheet says `[hidden] { display:
   none }`, at specificity (0,1,0) from the USER-AGENT origin, which ANY author rule of the same
   specificity beats. A bare `.vt-track { display: block }` therefore painted all three tracks on a
   stopped server — `updateStatus()` set the attribute, `#players-bar[hidden]` was true, and an
   empty bar sat on the screen under "-". Selecting on the attribute left the UA's rule standing.

   Since `[hidden] { display: none !important; }` went in at the top of this sheet, the document
   answers that centrally and this selector is belt as well as braces. That change had a cost worth
   recording: it made the render rig's break-proof for this very defect INERT. The proof strips
   `:not([hidden])` off a selector that no longer needs it, so it drove its two views, went green
   and proved nothing — a fix quietly disarming the proof that guards it, which is the same shape
   as the check below passing for the wrong reason. It is re-aimed at the current cascade.

   The check that was supposed to catch the original passed too, because it counted
   `#players-bar:not([hidden])` — the ATTRIBUTE, which was right — rather than measuring the box,
   which was 183×4px. It measures the box now. Found by looking at a screenshot. */
.vt-track:not([hidden]) {
  display: block; height: 4px; margin-top: 0.1rem; border-radius: var(--pill);
  background: var(--inset); border: 1px solid var(--line-2); overflow: hidden;
}
.vt-fill {
  display: block; height: 100%;
  background: linear-gradient(90deg, rgba(230, 178, 58, 0.55), var(--amber-hi));
}
/* The ceiling a reading is a fraction of — the same job, and the same weight, as `.lw-sub` on the
   world card. Never the reading itself. */
.vt-sub {
  font-family: var(--mono); font-size: 0.66rem; color: var(--faint);
  letter-spacing: 0.02em; overflow-wrap: anywhere;
}

/* ── Vitals badge ──────────────────────────────────────────────────────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  align-self: flex-start;
  padding: 0.2rem 0.7rem 0.2rem 0.6rem;
  border-radius: 3px;
  font-family: var(--ui);
  font-weight: 600;
  font-size: 0.92rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--muted);
}

.badge::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
}

.badge.online {
  color: var(--online);
  border-color: rgba(84, 201, 138, 0.4);
  background: rgba(84, 201, 138, 0.08);
}
.badge.online::before { box-shadow: 0 0 8px var(--online); }

.badge.offline {
  color: var(--alarm);
  border-color: rgba(255, 92, 92, 0.4);
  background: rgba(255, 92, 92, 0.08);
}

.badge.transitional {
  color: var(--standby);
  border-color: rgba(230, 178, 58, 0.4);
  background: rgba(230, 178, 58, 0.08);
}
.badge.transitional::before { animation: pulse 1s ease-in-out infinite; }

/* Win over `.status-item .value` font-size for the badge specifically */
.status-item .value.badge { font-size: 0.9rem; }

/* ── Controls ──────────────────────────────────────────────────────────── */

.controls {
  margin: 1.25rem 1.5rem;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
}
/* "When" picker inside the confirm dialog — scheduling lives in the confirm an admin already
   has to click, so the controls row stays exactly as it was. */
.ssa-when { display: flex; flex-wrap: wrap; gap: 0.4rem 0.75rem; margin: 0.15rem 0 0.9rem; }
.ssa-when-opt { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.85rem;
  color: var(--text); cursor: pointer; }
.ssa-when-opt input[type="radio"] { accent-color: var(--amber, #ff6a1a); cursor: pointer; }
.ssa-when-min { width: 62px; background: var(--inset); border: 1px solid var(--line-2); border-radius: 6px;
  color: var(--text); font: inherit; font-size: 0.82rem; padding: 0.2rem 0.35rem; margin-left: 0.15rem; }
.ssa-when-note { flex-basis: 100%; margin: 0.15rem 0 0; font-size: 0.76rem; color: var(--muted); }
.sched-pending { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.sched-chip { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem;
  background: color-mix(in srgb, var(--amber, #ff6a1a) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--amber, #ff6a1a) 40%, transparent);
  border-radius: 999px; padding: 0.2rem 0.3rem 0.2rem 0.65rem; }
.sched-x { background: none; border: 0; color: var(--muted); cursor: pointer; font-size: 1rem;
  line-height: 1; padding: 0 0.25rem; }
.sched-x:hover { color: var(--alarm, #ff5c5c); }

.controls button {
  padding: 0.6rem 1.25rem;
  border: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--text);
  font-size: 0.82rem;
  text-transform: uppercase;
}

.controls button:hover { border-color: var(--line-amber); color: var(--amber-hi); }

/* Primary action — Start */
#start-btn {
  background: var(--amber);
  border-color: var(--amber);
  color: var(--on-amber);
}
#start-btn:hover { background: var(--amber-hi); border-color: var(--amber-hi); color: var(--on-amber); }

.controls button.danger { color: var(--alarm); border-color: rgba(255, 92, 92, 0.35); }
.controls button.danger:hover { background: rgba(255, 92, 92, 0.12); color: var(--alarm); }

.controls button.warning { color: var(--standby); border-color: rgba(230, 178, 58, 0.35); }
.controls button.warning:hover { background: rgba(230, 178, 58, 0.12); color: var(--standby); }

/* The result of a Start/Stop/Restart, as a chip BESIDE the buttons.
   It began as the last child of the flex row (bare text between the buttons, reflowing them), then
   as a full-width row underneath — which left a permanent empty band above the next panel. A chip
   that hides when empty takes no room, and carries its state in its colour: "update failed" and
   "update succeeded" must not look the same. */
#control-message.ctrl-chip {
  display: inline-flex; align-items: center; gap: 0.4rem;
  margin-left: 0.35rem; padding: 0.34rem 0.75rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: rgba(255, 255, 255, 0.03);
  font-size: 0.8rem; color: var(--muted);
  max-width: 46ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#control-message[hidden] { display: none; }
#control-message.ctrl-chip::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: currentColor; box-shadow: 0 0 7px currentColor;
}
.ctrl-busy  { color: var(--standby) !important; border-color: rgba(230, 178, 58, 0.35) !important; }
.ctrl-ok    { color: var(--online) !important;  border-color: rgba(84, 201, 138, 0.35) !important; }
.ctrl-bad   { color: var(--alarm) !important;   border-color: rgba(255, 92, 92, 0.40) !important; }
.ctrl-note  { color: var(--text) !important; }

/* ── Skip-restart switch ───────────────────────────────────────────────── */

/* A real switch, not the operating system's checkbox. It sits in a row of large mono readouts, and a
   13px native tickbox next to "04:26" and "20.1 / 31.9 GB" read as an unstyled leftover. Built from
   the checkbox itself so it stays a checkbox: keyboard, labels and form semantics are unchanged. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.95rem;
  user-select: none;
}
.switch input {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: none; margin: 0;
  width: 2.35rem; height: 1.25rem; border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.switch input::after {
  content: ''; position: absolute; top: 50%; left: 0.18rem;
  width: 0.85rem; height: 0.85rem; border-radius: 50%;
  background: var(--muted); transform: translateY(-50%);
  transition: transform 0.15s ease, background 0.15s ease;
}
.switch input:checked {
  background: var(--amber);
  border-color: var(--amber);
}
.switch input:checked::after {
  background: var(--on-amber);
  transform: translate(1.03rem, -50%);
}
.switch input:focus-visible { outline: 2px solid var(--amber-hi); outline-offset: 2px; }
.switch:hover input { border-color: var(--line-amber); }

/* ── Section headers w/ feed tick ──────────────────────────────────────── */

.leaderboards h2,
.players h2,
.players-admin h2,
.bans h2,
.cmd-console h2,
.logs h2 {
  margin: 0 0 0.85rem;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

/* Inline SVG icons (shared with the public site look). */
.svg-defs { position: absolute; }
.ico {
  width: 1.05em; height: 1.05em; flex: none; vertical-align: middle;
  stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}
.players h2 .ico, .players-admin h2 .ico, .bans h2 .ico, .cmd-console h2 .ico, .logs h2 .ico { color: var(--amber); }
.nav-actions button, .controls button, #player-search-btn { display: inline-flex; align-items: center; gap: 0.4rem; }

/* ── Players list ──────────────────────────────────────────────────────── */

#players-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: var(--mono);
  font-size: 0.9rem;
  /* Fill the width with as many columns as fit instead of one tall column that wastes the space on
     the right. Items flow left→right, top→bottom in login order. */
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 0 1.4rem;
}

#players-list li {
  padding: 0.4rem 0.2rem;
  border-bottom: 1px solid var(--line-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#players-list li:first-child::before {
  content: none;
}

/* ── Live log feed ─────────────────────────────────────────────────────── */

.logs {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

#log-tail {
  flex: 1;
  overflow-y: auto;
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 400px;
  color: var(--data-text);
}

/* ── App footer ────────────────────────────────────────────────────────── */

/* Footer styled to match the Field Console: full-width, amber top accent, a brand
   line and a bottom bar with social links. */
.app-footer {
  margin-top: auto;   /* pin to the bottom of the screen */
  border-top: 1px solid var(--line-amber);
  background: linear-gradient(180deg, var(--bg-2), var(--bg));
}
.af-top {
  display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 0.4rem 2rem;
  padding: 1.3rem 1.5rem 0.9rem;
}
.af-brand { display: flex; align-items: center; gap: 0.55rem; font-weight: 700; letter-spacing: 0.04em; }
.af-logo { width: 24px; height: 24px; }
.af-bottom {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.6rem;
  padding: 0.7rem 1.5rem 1.1rem; border-top: 1px solid var(--line);
}
/* WRAPS. Three links in an unbreakable row measured 348px starting at x=25 — 14px past a 360px
   phone. The footer sits outside every tab panel, which is why it survived the first sweep:
   it was never in the subtree being measured. */
.af-social { display: flex; flex-wrap: wrap; gap: 1.4rem; }
.af-social a { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--muted); }
.af-social a:hover { color: var(--amber-hi); }

.app-footer-links {
  display: flex;
  gap: 1.5rem;
}

.app-footer-links a {
  color: var(--muted);
  text-decoration: none;
  transition: color 0.14s;
}

.app-footer-links a:hover {
  color: var(--amber-hi);
}

/* ── Login screen ──────────────────────────────────────────────────────── */

#login-screen {
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}

.login-box {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line-amber);
  padding: 2.25rem 2rem 2rem;
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  width: 400px;
  max-width: calc(100vw - 2rem);
  text-align: center;
}

.login-box::before,
.login-box::after {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  border: 2px solid var(--amber);
  pointer-events: none;
}
.login-box::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.login-box::after  { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }

.login-logo {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-lg);
  margin: 0 auto 0.25rem;
  border: 1px solid var(--line);
}

.login-box h1 {
  font-size: 1rem;
  margin: 0 0 0.4rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.login-box .login-tagline {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--amber);
  margin: 0 0 0.5rem;
}

.login-box input {
  padding: 0.6rem 0.7rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--inset);
  color: var(--text);
  font-family: var(--mono);
  text-align: center;
  letter-spacing: 0.1em;
}

.login-box input:focus { outline: none; border-color: var(--amber); }

.login-box button {
  padding: 0.6rem;
  border-radius: var(--radius);
  border: 1px solid var(--amber);
  background: var(--amber);
  color: var(--on-amber);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.login-box button:hover { background: var(--amber-hi); border-color: var(--amber-hi); }

/* ── Settings / Game Settings / Discord notes ──────────────────────────── */

.settings-note, .settings-message {
  margin: 0 1.5rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.settings-note { margin-top: 1rem; line-height: 1.6; }

.settings-message {
  font-family: var(--mono);
  letter-spacing: 0.03em;
}
.settings-message.success { color: var(--online); }
.settings-message.error { color: var(--alarm); }

.settings-form {
  margin: 1rem 1.5rem 2rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* ── Settings search toolbar + collapsible categories ──────────────────── */

.settings-toolbar {
  margin: 1rem 1.5rem 0;
  display: flex;
  gap: 0.75rem;
  align-items: center;
  flex-wrap: wrap;
}

/* Inside the Game Settings tab the container already has horizontal margin */
.gs-tab-content .settings-toolbar {
  margin: 0 0 1rem;
}

/* The six settings search boxes used to carry a COLOUR EMOJI in their placeholder — 🔍, the only
   seven pixels of that hue on the whole page, in a panel where every other search field draws the
   monochrome `#i-search` sprite. An emoji is also the font's picture rather than ours: it changes
   between platforms, it does not take `color`, and it sits inside the placeholder so it dims and
   disappears the moment somebody types. Same shape as `.player-search`: a wrapper that owns the
   border, an icon, and a borderless input. */
.settings-search-wrap {
  flex: 1;
  min-width: min(100%, 200px);
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0 0.8rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--inset);
}
.settings-search-wrap .ico { flex: none; width: 15px; height: 15px; color: var(--muted); }
.settings-search-wrap:focus-within { border-color: var(--amber); box-shadow: var(--focus-ring); }

.settings-search {
  flex: 1;
  /* `min-width: auto` is a flex item's default and an input's intrinsic width is about twenty
     characters, so without this it refuses to shrink and pushes its own wrapper off a phone. */
  min-width: 0;
  padding: 0.6rem 0;
  border-radius: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.85rem;
  letter-spacing: 0.02em;
}

.settings-search:focus {
  outline: none;
}

.settings-toolbar-actions {
  display: flex;
  gap: 0.5rem;
}

/* Expand/collapse buttons use the shared .secondary button style. */
.settings-toolbar-actions button { font-size: 0.76rem; padding: 0.45rem 0.85rem; }

.settings-section { margin-bottom: 1.8rem; }
.settings-section-head { margin: 0 0 0.75rem; padding-bottom: 0.55rem; border-bottom: 1px solid var(--line-amber); }
.settings-section-title { display: flex; align-items: center; gap: 0.5rem; font-size: 1.15rem; font-weight: 700; color: var(--amber-hi); }
.settings-section-title .ico { width: 1.15em; height: 1.15em; }
.settings-section-desc { margin: 0.35rem 0 0; font-size: 0.85rem; color: var(--muted); line-height: 1.45; max-width: 70ch; }
.settings-section .settings-category { margin-bottom: 0.6rem; }

/* Sub-section divider heading (e.g. Discord → Actions) */
.subhead { display: flex; align-items: center; gap: 0.5rem; margin: 2rem 1.5rem 0.9rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--line-amber); font-size: 0.9rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--amber); }
.subhead .ico { width: 1.05em; height: 1.05em; color: var(--amber); }
.group-note { margin: 0 0 0.9rem; font-size: 0.85rem; color: var(--muted); line-height: 1.5; }
.group-actions { display: flex; align-items: center; gap: 0.8rem; margin-top: 0.9rem; flex-wrap: wrap; }
.group-result { font-size: 0.82rem; color: var(--muted); }

/* Data tables (linked accounts, rosters, bans, user lists) */
.table-wrap { overflow-x: auto; margin-top: 0.4rem; }
.data-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.data-table th { text-align: left; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--line-amber); color: var(--muted); font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; }
.data-table td { padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--line-2); }
.data-table tbody tr:hover { background: rgba(255, 106, 26, 0.05); }
.data-table tr.clickable { cursor: pointer; }
.data-table td.mono { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); }
.data-table td.dim { color: var(--muted); font-size: 0.8rem; }

/* Click-to-edit row editors (Raid Times, Notifications…) */
.editor-rows { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.6rem; }
.editor-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.7rem; padding: 0.85rem 0.9rem; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); }
.editor-row .er-f { display: flex; flex-direction: column; gap: 0.28rem; font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
.editor-row .er-day { flex: 1 1 150px; }
.editor-row input { padding: 0.45rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); color: var(--text); font-family: var(--mono); font-size: 0.85rem; }
.editor-row input:focus { outline: none; border-color: var(--amber); box-shadow: var(--focus-ring); }
.editor-row input[type="number"] { width: 6rem; }
.editor-row .rr-del { margin-left: auto; align-self: center; }
/* The time/date picker's own glyph. It used to be inverted here because the browser drew it black,
   which it did because this document never said which colour scheme it was — it does now
   (`color-scheme: dark` on :root), so the browser draws a LIGHT glyph and inverting it would paint
   it back to black on black. That is a real trap and the reason it is written down: `filter:
   invert()` on a control the USER AGENT draws is a bet on which scheme the UA picked, and declaring
   the scheme settles the bet the other way. `opacity` is a claim about visibility rather than about
   the colour underneath, so it survives either answer. */
input[type="time"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator { opacity: 0.75; cursor: pointer; }
.editor-empty { color: var(--muted); font-size: 0.88rem; padding: 1.1rem 0; }
.editor-row.notif-row .er-msg { flex: 1 1 100%; }
.editor-row.notif-row .er-day { flex: 1 1 130px; }
.editor-row.notif-row .er-times { flex: 2 1 220px; }
.editor-row.notif-row .er-num { flex: 0 0 auto; }
.editor-row.notif-row .er-num input { width: 5rem; }
.editor-row.notif-row .er-col { flex: 0 0 auto; }
.editor-row.notif-row .nn-col-wrap { display: inline-flex; align-items: center; gap: 0.4rem; }
.editor-row.notif-row .nn-color-on { width: 1rem; height: 1rem; accent-color: var(--amber); cursor: pointer; }
.editor-row.notif-row .nn-del { align-self: flex-end; margin-bottom: 0.1rem; }
.editor-row input[type="color"] { width: 3rem; height: 2.25rem; padding: 2px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); cursor: pointer; }

/* ── Economy Override editor ─────────────────────────────────────────────── */
.eco-subtabs { display: flex; gap: 0.4rem; margin: 0.6rem 0 1rem; }
.eco-subtab { background: var(--panel-2); border: 1px solid var(--line); color: var(--muted); font-size: 0.82rem; }
.eco-subtab.active { background: var(--amber); border-color: var(--amber); color: var(--on-amber); }
.eco-subtab:not(.active):hover { background: var(--panel); border-color: var(--line-amber); color: var(--amber-hi); }
.eco-subtab.active:hover { background: var(--amber-hi); border-color: var(--amber-hi); color: var(--on-amber); }
.eco-pane.hidden { display: none; }
.eco-global-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); gap: 0.85rem; }
.eco-gfield { display: flex; flex-direction: column; gap: 0.22rem; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.7rem 0.8rem; }
.eco-glabel { font-size: 0.82rem; font-weight: 600; color: var(--text); }
.eco-ghelp { font-size: 0.73rem; color: var(--muted); line-height: 1.35; }
.eco-gin { margin-top: 0.3rem; padding: 0.45rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); color: var(--text); font-family: var(--mono); font-size: 0.85rem; }
.eco-gin:focus { outline: none; border-color: var(--amber); box-shadow: var(--focus-ring); }
/* Toggle fields (0/1 settings) */
.eco-gtoggle-f { flex-direction: row; align-items: center; justify-content: space-between; gap: 0.7rem; }
.eco-gtxt { display: flex; flex-direction: column; gap: 0.22rem; min-width: 0; }
.eco-switch { position: relative; flex: none; width: 40px; height: 22px; cursor: pointer; }
.eco-switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.eco-slider { position: absolute; inset: 0; background: var(--inset); border: 1px solid var(--line); border-radius: var(--pill); transition: background 0.15s, border-color 0.15s; }
.eco-slider::before { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--muted); transition: transform 0.15s, background 0.15s; }
.eco-switch input:checked + .eco-slider { background: rgba(255, 106, 26,0.25); border-color: var(--amber); }
.eco-switch input:checked + .eco-slider::before { transform: translateX(18px); background: var(--amber-hi); }
/* Vehicle limits editor */
.eco-veh { margin-top: 1.3rem; padding-top: 1rem; border-top: 1px solid var(--line-amber); }
.eco-veh-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap; }
.eco-veh-title { margin: 0; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text); }
.eco-veh-sub { font-family: var(--mono); font-size: 0.72rem; color: var(--muted); text-transform: none; letter-spacing: 0; margin-left: 0.35rem; }
.eco-veh-rows { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.6rem; }
.eco-veh-row { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.eco-veh-row .evr-group { flex: 1 1 240px; min-width: 0; }
.eco-veh-row .evr-max { flex: 0 0 150px; }
.eco-veh-row input { padding: 0.45rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); color: var(--text); font-family: var(--mono); font-size: 0.85rem; }
.eco-veh-row input:focus { outline: none; border-color: var(--amber); }
.eco-trader { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 0.55rem; }
.eco-trader[open] { border-color: var(--line-amber); }
.eco-trader-h { display: flex; justify-content: space-between; align-items: center; gap: 0.6rem; padding: 0.7rem 0.9rem; cursor: pointer; list-style: none; }
.eco-trader-h::-webkit-details-marker { display: none; }
.eco-trader-name { font-weight: 600; color: var(--amber); }
.eco-trader-n { font-family: var(--mono); font-size: 0.76rem; color: var(--muted); }
.eco-trader-body { padding: 0 0.9rem 0.9rem; }
.eco-items { display: flex; flex-direction: column; gap: 0.5rem; }
.eco-item { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.6rem; padding: 0.6rem 0.65rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
/* `min-width: 0`, not 140px, because that is the rule for a flex child that has to shrink. It is
   NOT what made this row overflow a phone: putting the 140px floor back and re-running
   `check-panel-render` at 320 passes, so the whole of that defect was the unwrappable code below.
   Said here because the tidy assumption was wrong and the next person will make it again. */
.eco-item-id { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 180px; min-width: 0; }
.eco-item-img { width: 34px; height: 34px; object-fit: contain; flex: none; }
/* item image placeholders (shown when an item has no image — free tier / no active subscription) */
.eco-item-imgslot { width: 34px; height: 34px; flex: none; display: inline-flex; align-items: center; justify-content: center; }
.eco-item-imgslot img { width: 34px; height: 34px; object-fit: contain; }
.eco-item-ph { width: 22px; height: 22px; color: var(--muted); }
.mp-c-ph { width: 14px; height: 14px; color: var(--muted); }
.cmd-sg-ph { display: inline-flex; align-items: center; justify-content: center; color: var(--muted); }
.cmd-sg-ph .ico { width: 16px; height: 16px; }
.gl-preview-ph { width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; background: var(--inset); border-radius: 6px; flex: none; color: var(--muted); }
.gl-preview-ph .ico { width: 30px; height: 30px; }
.eco-item-txt { display: flex; flex-direction: column; min-width: 0; }
/* ⚠ **AND THE NAME ABOVE THAT CODE WAS CUT, ON THE ROW WHOSE JOB IS TO SAY WHICH ITEM THIS IS.**
   Measured across 169 rows: `Improvised Explosive Device Remote Detonator` rendered with 38% of it
   on screen at 125px. The `nowrap` was doing nothing for the row's height either — the code under it
   already wraps, so the row is variable height whatever this says — and a display name is prose with
   spaces in it, so it has somewhere to break. Wrapping it costs a line and buys the whole name. */
.eco-item-name { font-weight: 600; font-size: 0.87rem; white-space: normal; overflow-wrap: anywhere; }
/* A tradeable code is one unbreakable token — `Improvised_Explosive_Device_Remote_Detonator` is
   44 characters with no space in it — so without an explicit break opportunity it does not wrap,
   it is CUT OFF, and it drags the row wider than the viewport on the way. `check-panel-render`
   measured 46px lost at 1280 and 136px at 320. */
.eco-item-code { font-family: var(--mono); font-size: 0.68rem; color: var(--faint); overflow-wrap: anywhere; }
.eco-item .er-f { display: flex; flex-direction: column; gap: 0.22rem; font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.eco-item .er-f input, .eco-item .er-f select { width: 5.4rem; padding: 0.4rem 0.45rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--text); font-family: var(--mono); font-size: 0.82rem; }
/* The game's own figure under the box that can be left at -1. It sits INSIDE the `.er-f` label, so
   the column that already holds a caption and an input holds a third line and nothing reflows; the
   width is the input's, so a long number wraps rather than widening the row on a phone. The label
   above it is uppercase and this is not -- it is a reading, not a heading. Empty when the catalogue
   has no figure, and an empty span with no padding takes no space at all. */
.eco-item .er-base { display: block; line-height: 1.35; min-height: 1.35em; max-width: 5.4rem; font-family: var(--mono); font-size: 0.62rem; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--faint); overflow-wrap: anywhere; }
.eco-item .ei-del { align-self: center; margin-left: auto; }
/* ── Economy → Traders & items ─────────────────────────────────────────────────────────
   The screen an owner spends hours on, so everything here is built around two things: not
   having to type an item's name to find it, and not having to change the same field forty
   times in a row.

   House rules this block follows, both of which are checked mechanically:
   · no `minmax(<length>, 1fr)` — every grid minimum goes through `min(100%, N)`, so it
     collapses to one column instead of widening the page;
   · no `:hover` rule that names only `background` or only `color`. The pair is retuned on the
     ELEMENT through --hov-bg / --hov-fg / --hov-line, so both halves always travel together. */

/* The caveat that decides whether a number an owner types means anything. traders.json says it
   plainly and it belongs on the screen: the game modifies these further by outpost prosperity,
   player count and durability, so tuning against the figure alone tunes against the wrong one. */
.eco-thead { margin-bottom: 0.9rem; }
.eco-basenote { display: flex; align-items: flex-start; gap: 0.45rem; margin: 0 0 0.7rem; }
.eco-basenote .ico { width: 15px; height: 15px; flex: none; margin-top: 0.12rem; color: var(--amber-hi); }
.eco-thead-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.eco-catstate {
  display: inline-flex; align-items: center; gap: 0.35rem; min-width: 0;
  flex: 1 1 16rem; font-size: 0.76rem; color: var(--muted);
}
.eco-catstate .ico { width: 13px; height: 13px; flex: none; color: var(--ok, var(--amber-hi)); }
.eco-catstate-off .ico { color: var(--amber-hi); }
.eco-expand { flex: none; }

/* The trader's own kind, read out of the game's trader table — `A_0_Armory` IS an Armorer at
   outpost A_0, and that is what decides what the picker offers first. Empty until the
   catalogue lands, so it never reserves space for a badge that is not coming. */
.eco-trader-type {
  margin-right: auto; font-size: 0.7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--faint);
}
.eco-trader-type.on {
  padding: 0.05rem 0.4rem; border-radius: var(--pill);
  border: 1px solid var(--line-amber); color: var(--amber-hi);
}
/* The KEY, now that the NAME has the heading. `A_0_Armory` is still worth having on screen — it is
   what an owner searches `EconomyOverride.json` for — but it is a filing reference, so it is the
   size and colour of one. `anywhere` because it is one unbreakable token, the same reason
   `.eco-item-code` carries it. */
.eco-trader-key {
  font-family: var(--mono); font-size: 0.7rem; color: var(--faint);
  min-width: 0; overflow-wrap: anywhere;
}
.eco-trader-h { flex-wrap: wrap; }

/* ── the per-trader toolbar ──────────────────────────────────────────────────────────── */
.eco-tbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem;
  padding: 0.55rem 0 0.7rem;
}
.eco-selall { display: inline-flex; align-items: center; gap: 0.4rem; flex: none; cursor: pointer; font-size: 0.78rem; color: var(--muted); }
.eco-selall input { width: 15px; height: 15px; accent-color: var(--amber); cursor: pointer; }
.eco-rowfilter {
  display: inline-flex; align-items: center; gap: 0.4rem; min-width: 0;
  /* 9rem, not 12. The panel's root font is 124%, so a 12rem basis is 238px — wider than the whole
     content box of a 320px phone, which forces the filter onto a line of its own at every width
     below about 400px. 9rem is 179px and lets it share a line with the tick box. It does NOT
     shorten the 320px toolbar: measured at 177px tall either way, because the three buttons below
     it are what fills those lines. */
  flex: 1 1 9rem; max-width: 22rem;
  padding: 0.35rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--inset);
}
.eco-rowfilter .ico { width: 14px; height: 14px; flex: none; color: var(--muted); }
.eco-rowfilter input {
  flex: 1 1 auto; min-width: 0; border: 0; background: none; padding: 0;
  color: var(--text); font-size: 0.82rem;
}
.eco-rowfilter input:focus { outline: none; }
.eco-rowfilter:focus-within { border-color: var(--amber); box-shadow: var(--focus-ring); }
.eco-selinfo { font-size: 0.76rem; color: var(--faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.eco-selinfo-on { color: var(--amber-hi); font-weight: 600; }
.eco-tbar button { flex: none; }
.eco-tbar button .ico { width: 13px; height: 13px; }
.eco-tbar button[disabled] { opacity: 0.45; cursor: default; }
.eco-item.eco-hidden { display: none; }
/* A row a bulk edit just moved. It fades out on the next render; until then it is the only
   thing on screen that says which of forty rows actually changed. */
.eco-touched { border-color: var(--line-amber); }
.eco-pick { display: flex; align-items: center; flex: none; align-self: center; cursor: pointer; }
.eco-pick input { width: 15px; height: 15px; accent-color: var(--amber); cursor: pointer; }

/* ── the bulk-edit modal ─────────────────────────────────────────────────────────────── */
.eco-bulk-modal { width: min(100%, 46rem); display: flex; flex-direction: column; max-height: 88vh; }
.eco-bulk-body { padding: 0.9rem 1rem 0; overflow-y: auto; min-height: 0; }
.eco-bulk-form {
  display: grid; gap: 0.6rem; margin-bottom: 0.8rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.eco-bf { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.eco-bf > span {
  font-size: 0.62rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted);
}
.eco-bf select, .eco-bf input {
  width: 100%; min-width: 0; padding: 0.45rem 0.55rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--inset); color: var(--text); font-size: 0.85rem;
}
.eco-bf select:focus, .eco-bf input:focus { outline: none; border-color: var(--amber); box-shadow: var(--focus-ring); }
.eco-bf.hidden { display: none; }
.eco-bval-wrap { position: relative; display: flex; align-items: center; min-width: 0; }
.eco-bval-wrap input { font-family: var(--mono); padding-right: 2rem; }
.eco-bunit { position: absolute; right: 0.6rem; color: var(--muted); font-size: 0.82rem; pointer-events: none; }
/* The count, before anything moves. A bulk edit that silently touched 1,124 rows is not a
   feature, so this line is always on screen and always says how many of the chosen rows are
   actually going to move — and how many are skipped, with the reason. */
.eco-bsum {
  padding: 0.5rem 0.65rem; margin-bottom: 0.7rem;
  border: 1px solid var(--line-amber); border-radius: var(--radius);
  background: var(--inset); color: var(--amber-hi);
  font-size: 0.8rem; line-height: 1.5;
}
.eco-bsum-none { border-color: var(--line); color: var(--muted); }
.eco-bprev { margin-bottom: 0.9rem; }
.eco-btable { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.eco-btable th {
  text-align: left; padding: 0.3rem 0.5rem; border-bottom: 1px solid var(--line);
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); font-weight: 600;
}
.eco-btable td { padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--line-2); vertical-align: middle; }
.eco-btable td:first-child { min-width: 0; }
.eco-bt-name { display: block; color: var(--text); overflow-wrap: anywhere; }
.eco-bt-code { display: block; font-family: var(--mono); font-size: 0.68rem; color: var(--faint); overflow-wrap: anywhere; }
.eco-bt-from { font-family: var(--mono); color: var(--muted); text-align: right; white-space: nowrap; }
.eco-bt-to { font-family: var(--mono); color: var(--amber-hi); font-weight: 600; text-align: right; white-space: nowrap; }
.eco-bulk-foot, .eco-add-foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem;
  padding: 0.75rem 1rem; border-top: 1px solid var(--line); flex: none;
}
.eco-bulk-foot .eco-bcancel { margin-left: auto; }

/* ── the add-items modal ─────────────────────────────────────────────────────────────── */
.eco-add-modal { width: min(100%, 52rem); display: flex; flex-direction: column; max-height: 88vh; }
.eco-add-filters {
  display: grid; gap: 0.5rem; padding: 0.8rem 1rem 0.5rem; flex: none;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.eco-af { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.eco-af.hidden { display: none; }
.eco-af > span {
  font-size: 0.62rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--muted);
}
.eco-af select {
  width: 100%; min-width: 0; padding: 0.45rem 0.55rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--inset); color: var(--text); font-size: 0.85rem;
}
.eco-af select:focus { outline: none; border-color: var(--amber); box-shadow: var(--focus-ring); }
.eco-af-q { align-self: end; min-width: 0; }
.eco-add-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem;
  padding: 0.2rem 1rem 0.6rem; flex: none;
}
.eco-acount { margin-left: auto; font-size: 0.76rem; color: var(--faint); font-variant-numeric: tabular-nums; }
.eco-add-list {
  flex: 1 1 auto; min-height: 8rem; overflow-y: auto;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.eco-arow {
  display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
  padding: 0.4rem 1rem; border-bottom: 1px solid var(--line-2); cursor: pointer;
  --hov-bg: var(--hover-bg); --hov-fg: var(--text);
}
.eco-arow:hover { background: var(--hov-bg); color: var(--hov-fg); }
.eco-arow input { width: 15px; height: 15px; flex: none; accent-color: var(--amber); cursor: pointer; }
.eco-aimg { width: 30px; height: 30px; object-fit: contain; flex: none; }
.eco-aph { display: inline-flex; align-items: center; justify-content: center; color: var(--muted); }
.eco-aph .ico { width: 18px; height: 18px; }
.eco-atxt { display: flex; flex-direction: column; min-width: 0; flex: 1 1 10rem; }
.eco-aname { font-size: 0.85rem; color: var(--text); overflow-wrap: anywhere; }
.eco-acode { font-family: var(--mono); font-size: 0.68rem; color: var(--faint); overflow-wrap: anywhere; }
.eco-atag, .eco-acat-tag {
  flex: none; padding: 0.02rem 0.35rem; border-radius: var(--pill);
  border: 1px solid var(--line); color: var(--muted);
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em;
}
.eco-aprice { flex: none; font-family: var(--mono); font-size: 0.72rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Already on this trader: still listed, so an owner can see it is there rather than wonder
   why a search found nothing, and not offered again. */
.eco-arow-had { cursor: default; --hov-bg: transparent; --hov-fg: var(--muted); }
.eco-arow-had .eco-aname, .eco-arow-had .eco-aprice { color: var(--muted); }
.eco-ahad { flex: none; font-size: 0.68rem; color: var(--amber-hi); }
.eco-aseed { display: inline-flex; align-items: center; gap: 0.4rem; min-width: 0; flex: 1 1 14rem; cursor: pointer; font-size: 0.78rem; color: var(--muted); }
.eco-aseed input { width: 15px; height: 15px; flex: none; accent-color: var(--amber); cursor: pointer; }
.eco-aseed.hidden { display: none; }

/* The panel upgrades every <select> into its own `.cdd` dropdown, whose button is `white-space:
   nowrap` and whose wrapper is a bare `inline-flex` with no `min-width: 0`. That is right where it
   is used — a short label in a wide bar — and wrong inside a modal on a phone: "Everything the game
   sells (29)" reached 312px past a 320px viewport and the page scrolled sideways. `.cdd-lbl`
   already ellipsises, so all it needs is permission to shrink.
   Scoped to these two forms on purpose. `.cdd` is used in about thirty places and a bare rule for a
   name that already has scoped ones is the collision that has wrecked the map's marker card twice. */
.eco-af .cdd, .eco-bf .cdd { max-width: 100%; min-width: 0; }
.eco-af .cdd-btn, .eco-bf .cdd-btn { max-width: 100%; min-width: 0; }
/* …and the third place, which is every plugin at once. A plugin's tab is drawn inside
   `#pl-ws-content` with this panel's own stylesheet, so an upgraded <select> there is the same
   `inline-flex` with no `min-width: 0` — and a plugin's labels are sentences rather than the short
   ones this control was designed around. Measured at a true 310px viewport: Loot Zones' "Use one of
   my existing zone configurations" reached 408px and scrolled the whole page sideways by 98px.
   Scoped to the workspace, not written bare, for the reason the paragraph above gives. */
#pl-ws-content .cdd, #pl-ws-content .cdd-btn { max-width: 100%; min-width: 0; }

/* A ONE-WORD HEADING THAT CANNOT WRAP IS CLIPPED, AND THAT IS WHAT A TRANSLATION TURNS A TITLE INTO.
   Plugins ship their own languages now, and "Cargo drops" — two short words with a break between
   them — is one 17-character compound in Hungarian, `Rakományledobások`. `min-width: 0` was already
   on the heading and did not help: there is nothing to break, so the word overflows its column and
   the card's `overflow: hidden` takes 13px off the end of it. Measured at a true 310px viewport, in
   the language, by `check-panel-render`; German and Finnish build the same compounds.
   `break-word` and not `anywhere`, because `anywhere` also shrinks the element's min-content width
   and would re-lay-out flex rows that are correct today. It breaks only a word that could not fit
   on a line of its own, so for every string that fits — which is all of English — it does nothing.
   Scoped to the plugin workspace rather than written bare, for the reason the paragraph above gives. */
#pl-ws-content :where(h1, h2, h3, h4, h5, label, th, td, button, summary, legend, .switch, p, li, dt, dd, figcaption) {
  overflow-wrap: break-word;
}

.settings-category {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.settings-category[open] { border-color: var(--line-amber); }

.settings-cat-summary {
  cursor: pointer;
  list-style: none;
  padding: 0.95rem 1.3rem;
  user-select: none;
}

.settings-cat-summary::-webkit-details-marker { display: none; }
.settings-cat-summary:hover .settings-cat-title { color: var(--amber-hi); }

.settings-cat-title {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.82rem;
  color: var(--text);
}

.settings-cat-title::before {
  content: "▸";
  color: var(--amber);
  margin-right: 0.6rem;
  display: inline-block;
  transition: transform 0.15s;
}

.settings-category[open] .settings-cat-title::before { transform: rotate(90deg); }

.settings-cat-desc {
  display: block;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
  letter-spacing: 0.02em;
  margin-top: 0.3rem;
  padding-left: 1.2rem;
}

.settings-cat-body {
  padding: 0.4rem 1.3rem 1.2rem;
  border-top: 1px solid var(--line);
}

.settings-cat-body > .settings-field:first-child { margin-top: 0.8rem; }

.settings-cat-body .settings-group {
  background: var(--bg-2);
  margin-top: 0.85rem;
}

.filtered-hidden { display: none !important; }

/* INI setting metadata: unit/type badge + description help text */
.ini-type {
  margin-left: 0.5rem;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--line-amber);
  border-radius: 3px;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--amber);
  vertical-align: middle;
}

.ini-desc {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.74rem;
  line-height: 1.45;
  color: var(--muted);
  /* These descriptions are HARVESTED from the game's own settings documentation, so one of them can
     be a hundred unbreakable ">" characters (PuppetWorldEncounterSpawnWeightMultiplier is, today).
     An unbreakable run sets the field's min-content width, and the field is a grid whose single
     narrow-screen column is a bare `1fr` — min `auto` — so that one row grew to 913px inside a
     310px card and took its input's right edge off the screen with it. `anywhere` is the one of the
     wrapping keywords that lowers min-content, which is what has to shrink; break-word does not. */
  overflow-wrap: anywhere;
}

/* checkbox rows need to wrap so the description sits on its own line */
.settings-field.checkbox { flex-wrap: wrap; }
.settings-field.checkbox .ini-desc { flex-basis: 100%; margin-top: 0.35rem; }

.sync-notify-btn {
  align-self: flex-start;
  margin-top: 0.55rem;
  padding: 0.4rem 0.85rem;
  font-size: 0.78rem;
  background: var(--panel-2);
  color: var(--amber);
  border: 1px solid var(--line-amber);
}
/* Keep the quiet background on hover — without it the global amber button
   hover shines through and amber-on-amber text becomes unreadable. */
.sync-notify-btn:hover { background: var(--panel-2); border-color: var(--amber-hi); color: var(--amber-hi); }
.sync-notify-btn:disabled { opacity: 0.5; cursor: default; }

/* `min-width: 0` because a <fieldset> does not behave like other boxes: its default is
   `min-width: min-content`, so it refuses to shrink below its widest child and pushes the page
   sideways instead. Measured at 369px against a 360px phone on the Discord screen. */
.settings-group {
  min-width: 0;
  /* The browser gives every <fieldset> `margin-inline: 2px` of its own. Nothing in this
     stylesheet asked for it, and it put every settings card 2px right of every other card in
     the panel — so switching from the Dashboard to Discord nudged the whole page sideways. */
  margin-inline: 0;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.4rem 1.2rem;
}

.settings-group > legend {
  font-weight: 600;
  padding: 0 0.5rem;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--amber);
}

.settings-group .settings-group {
  background: var(--bg-2);
  margin-top: 0.75rem;
}

/* Premium-locked group (Field Console without an active subscription). */
.settings-group.settings-locked { border-color: var(--line-amber); position: relative; }
.settings-group.settings-locked > legend { color: var(--muted); }
.settings-group.settings-locked .settings-field,
.settings-group.settings-locked .settings-group { opacity: 0.55; pointer-events: none; }
.settings-lock-note {
  opacity: 1; margin: 0.2rem 0 1rem; padding: 0.6rem 0.85rem;
  background: rgba(255, 106, 26, 0.1); border: 1px solid var(--line-amber);
  border-radius: var(--radius); color: var(--amber-hi); font-size: 0.85rem; line-height: 1.5;
}
.settings-lock-note strong { color: var(--amber-hi); }
.settings-lock-note .link-btn { font-size: 0.85rem; vertical-align: baseline; }

/* Two-column row: name + description on the left, the control on the right —
   scannable and clear, instead of stacked mystery inputs. */
.settings-field {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(220px, 360px);
  grid-template-areas: "label control" "desc control";
  align-items: center;
  column-gap: 1.5rem;
  row-gap: 0.25rem;
  margin: 0;
  padding: 0.8rem 0;
  border-top: 1px solid var(--line-2);
}
.settings-field:first-child { border-top: none; }

.settings-field.checkbox {
  grid-template-columns: minmax(0, 1fr) auto;
}
.settings-field.checkbox input[type="checkbox"] { grid-area: control; justify-self: end; }

.settings-field label {
  grid-area: label;
  font-family: var(--ui);
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--text);
  text-transform: none;
  letter-spacing: normal;
}

.settings-field .ini-desc {
  grid-area: desc;
  font-family: var(--ui);
  font-size: 0.8rem;
  color: var(--muted);
  line-height: 1.45;
  margin: 0;
  letter-spacing: normal;
  text-transform: none;
}
/* A field can carry a second hint (e.g. restart times + the sync note) and an
   action button. Only the FIRST description owns the "desc" grid area — the rest
   would overlap it in the same cell. Flow them into fresh rows in column 1. */
.settings-field .ini-desc ~ .ini-desc { grid-area: auto; grid-column: 1; margin-top: 0.15rem; }
.settings-field .sync-notify-btn { grid-area: auto; grid-column: 1; justify-self: start; }
/* Image (logo / wallpaper) upload control sits under the URL input. */
.settings-field .img-upload { grid-area: auto; grid-column: 2; display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.7rem; margin-top: 0.45rem; }

/* Leaderboard/stat visibility checklists: span the FULL field width and flow into columns, with
   group headers — so a long category list stays scannable instead of a cramped single column. */
.settings-field.lb-vis-field { grid-template-columns: 1fr; grid-template-areas: "label" "desc" "control"; align-items: start; }
.settings-field.lb-vis-field > label { grid-area: label; }
.lb-vis {
  grid-area: control; margin-top: 0.6rem;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 0.1rem 1.4rem; align-content: start;
  background: var(--inset, rgba(0,0,0,0.15)); border: 1px solid var(--line-2); border-radius: var(--radius-md, 6px);
  padding: 0.7rem 0.9rem;
}
.lb-vis-cats { max-height: 460px; overflow-y: auto; }
.lb-vis-groups { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
.lb-vis .lb-vis-grp {
  grid-column: 1 / -1; font-family: var(--ui); font-weight: 700; font-size: 0.72rem; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--amber, #ff6a1a); margin: 0.85rem 0 0.3rem; padding-bottom: 0.25rem;
  border-bottom: 1px solid var(--line-2);
}
.lb-vis .lb-vis-grp:first-child { margin-top: 0; }
/* IMPORTANT: `.settings-field label { grid-area: label }` (the 2-col field grid) also matches these
   inner labels and would pile them all into the top-left cell. Reset placement so they flow normally. */
.lb-vis .lang-check, .lb-vis .ini-desc { grid-area: auto; grid-column: auto; grid-row: auto; }
.lb-vis .lang-check { display: flex; align-items: center; gap: 0.5rem; font-family: var(--ui); font-weight: 400;
  font-size: 0.86rem; color: var(--text); padding: 0.2rem 0; cursor: pointer; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lb-vis .lang-check input[type="checkbox"] { flex: none; margin: 0; width: 1rem; height: 1rem; }
@media (max-width: 640px) { .settings-field .img-upload { grid-column: 1; } }
.img-upload-btn { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.4rem 0.7rem; background: var(--panel-2); border: 1px solid var(--line-amber); color: var(--amber-hi); border-radius: var(--radius); cursor: pointer; font-family: var(--ui); font-size: 0.82rem; }
.img-upload-btn:hover { background: var(--amber); color: var(--on-amber); }
.img-upload-btn .ico { width: 0.9em; height: 0.9em; }
.img-upload-status { font-size: 0.8rem; color: var(--muted); }
.img-upload-prev { display: none; max-height: 40px; max-width: 170px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-2); object-fit: contain; }
/* Theme colour picker swatch next to the hex input. */
.settings-field .color-field { grid-area: auto; grid-column: 2; margin-top: 0.45rem; }
@media (max-width: 640px) { .settings-field .color-field { grid-column: 1; } }
.color-pick { width: 48px; height: 30px; padding: 0; border: 1px solid var(--line-amber); border-radius: var(--radius); background: var(--inset); cursor: pointer; }

.settings-field input[type="text"],
.settings-field input[type="number"],
.settings-field input[type="password"],
.settings-field textarea,
.settings-field select { grid-area: control; }

@media (max-width: 640px) {
  /* `minmax(0, 1fr)`, never a bare `1fr`: a bare one is `minmax(auto, 1fr)`, so the track cannot go
     below its content's min-content width and OVERFLOWS the card instead of wrapping — the same
     trap as a bare length in `minmax(N, 1fr)`, and the wide-screen rule above already avoids it.
     One harvested description and one long label were enough to push a row 59px past a 310px
     screen, where the card's own overflow cut the right edge off rather than scrolling to it. */
  .settings-field {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "label" "desc" "control";
    row-gap: 0.4rem;
  }
  .settings-field.checkbox {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "label control" "desc desc";
  }
}

.settings-field input[type="text"],
.settings-field input[type="number"],
.settings-field input[type="password"],
.settings-field textarea,
.setup-box .settings-field input {
  padding: 0.55rem 0.65rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--inset);
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.9rem;
  width: 100%;
}

.settings-field input:focus,
.settings-field textarea:focus,
.setup-box .settings-field input:focus,
.login-box input:focus {
  outline: none;
  border-color: var(--amber);
  box-shadow: var(--focus-ring);
}

.settings-field textarea {
  min-height: 4.5rem;
  resize: vertical;
  white-space: pre;
}

.settings-field input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--amber);
}

/* ── Game Settings tabs ────────────────────────────────────────────────── */

.gs-tabs {
  display: flex;
  gap: 0.35rem;
  flex-wrap: wrap;
  margin: 1.2rem 1.5rem 0;
  border-bottom: 1px solid var(--line);
}

/* These are <a href> now — each of the five screens has an address, so it can be opened in a new
   tab, bookmarked and sent. That means undoing the two things an anchor does and a button does not:
   underline itself and paint itself with the link colour. */
.gs-tab {
  display: inline-block; text-decoration: none; cursor: pointer;
  padding: 0.55rem 1.1rem;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  background: transparent;
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.gs-tab:hover { color: var(--text); background: var(--panel-2); border-color: var(--line); text-decoration: none; }

.gs-tab.active {
  background: var(--panel);
  border-color: var(--line);
  color: var(--amber);
  margin-bottom: -1px;
}

.gs-tab-content {
  margin: 0 1.5rem 2rem;
  padding-top: 1.25rem;
}
/* Notes/messages inside a tab already sit in the padded tab body — drop the extra
   horizontal margin so they line up with the header and form fields. */
.gs-tab-content > .settings-note,
.gs-tab-content > .settings-message { margin-left: 0; margin-right: 0; }

/* Editor ⇄ JSON mode toggle */
.gs-mode { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.gs-mode-btn { background: var(--panel-2); color: var(--muted); border: 0; padding: 0.42rem 0.85rem; font-family: var(--ui); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer; }
.gs-mode-btn + .gs-mode-btn { border-left: 1px solid var(--line); }
.gs-mode-btn.active { background: var(--amber); color: var(--bg); font-weight: 700; }
.gs-mode-btn.active:hover { background: var(--amber-hi); color: var(--bg); }
.gs-mode-btn:not(.active):hover { background: var(--panel); color: var(--amber-hi); }
.gs-tab-content.json-mode .gs-editor-body { display: none; }
.gs-tab-content:not(.json-mode) .gs-json-pane { display: none; }
.gs-tab-content.json-mode .gs-add-btn { display: none; }
.gs-json-ta { width: 100%; min-height: 320px; box-sizing: border-box; }
.gs-json-err { margin: 0.55rem 0 0; }
.gs-json-err.hidden { display: none; }

.gs-tab-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.85rem;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.gs-tab-header h2, .gs-tab-header h3 {
  margin: 0;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text);
}

.gs-btn-row { display: flex; gap: 0.5rem; }

/* .gs-save-btn / #settings-save-btn / #discord-config-save inherit the unified
   primary button — all Save buttons now look identical. */

.gs-tab-content .secondary {
  padding: 0.45rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--text);
  font-size: 0.78rem;
  text-transform: uppercase;
}
.gs-tab-content .secondary:hover { border-color: var(--line-amber); color: var(--amber-hi); }

.gs-ini-form { margin: 0; }

.gs-user-lists {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.4rem;
}

@media (max-width: 700px) {
  .gs-user-lists { grid-template-columns: 1fr; }
}

.gs-list-section {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.gs-list-section textarea,
.gs-code-textarea {
  width: 100%;
  padding: 0.75rem 0.85rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--inset);
  color: var(--data-text);
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1.6;
  resize: vertical;
}

.gs-code-textarea:focus,
.gs-list-section textarea:focus {
  outline: none;
  border-color: var(--amber);
}

/* ── Interactive user lists (admins / bans / whitelist / exclusive) ────────── */
.ul-section { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem 1rem; gap: 0.6rem; }
.ul-wide { grid-column: 1 / -1; }
.ul-count { color: var(--muted); font-family: var(--mono); font-weight: 600; letter-spacing: 0; text-transform: none; }
.ul-note { margin: 0; }
.ul-add { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: flex-start; }
.ul-ac-wrap { position: relative; flex: 1 1 200px; min-width: 0; }
.ul-add-input { width: 100%; box-sizing: border-box; }
.ul-add-note { flex: 1 1 140px; min-width: 0; }
.ul-add input { background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); color: var(--text); font-family: var(--ui); font-size: 0.85rem; padding: 0.45rem 0.6rem; }
.ul-add input:focus { outline: none; border-color: var(--amber); }
/* Styled autocomplete dropdown */
.ul-ac { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20; max-height: 260px; overflow-y: auto; background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45); padding: 0.25rem; }
.ul-ac.hidden { display: none; }
.ul-ac-item { display: flex; align-items: center; gap: 0.55rem; width: 100%; text-align: left; background: transparent; border: 0; border-radius: var(--radius); color: var(--text); padding: 0.4rem 0.5rem; cursor: pointer; }
.ul-ac-item:hover, .ul-ac-item:focus { background: var(--hover-bg); color: var(--amber-hi); }
.ul-ac-name { font-weight: 600; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ul-ac-id { margin-left: auto; font-size: 0.72rem; color: var(--muted); white-space: nowrap; }
.ul-avatar.sm { width: 24px; height: 24px; }
.ul-avatar.sm .ico { font-size: 0.85rem; }
.ul-rows { display: flex; flex-direction: column; gap: 0.35rem; }
.ul-empty { color: var(--muted); font-size: 0.85rem; margin: 0.2rem 0; }
.ul-row { display: flex; align-items: center; gap: 0.6rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.45rem 0.6rem; }
.ul-avatar { display: inline-flex; width: 30px; height: 30px; flex: none; align-items: center; justify-content: center; border-radius: 50%; background: var(--panel); border: 1px solid var(--line); }
.ul-avatar .ico { font-size: 1rem; color: var(--amber); }
.ul-idcol { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; flex: 1 1 auto; }
.ul-name { font-weight: 600; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ul-sid { font-size: 0.74rem; color: var(--muted); }
.ul-flags { display: flex; align-items: center; gap: 0.7rem; flex: none; }
.ul-flag { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.76rem; color: var(--muted); white-space: nowrap; cursor: pointer; }
.ul-flag input { accent-color: var(--amber); }
.ul-extra { font-family: var(--mono); font-size: 0.7rem; color: var(--standby); border: 1px dashed var(--line); border-radius: var(--radius); padding: 0.05rem 0.35rem; }
.ul-bancol { display: flex; flex-direction: column; gap: 0.1rem; flex: 1 1 140px; min-width: 0; text-align: right; }
.ul-bannote { font-size: 0.8rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.ul-when { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.ul-del { flex: none; width: 28px; height: 28px; cursor: pointer; background: transparent; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); font-size: 0.85rem; line-height: 1; }
.ul-del:hover { color: var(--alarm); border-color: var(--alarm); background: transparent; }

/* ── Setup wizard ──────────────────────────────────────────────────────── */

.setup-center {
  align-items: center;
  justify-content: center;
  padding: 2.5rem 1rem;
}

.setup-box {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg, 14px);
  padding: 2rem 2.4rem 2.2rem;
  width: 100%;
  max-width: 720px;
  box-shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.7);
}
@media (max-width: 640px) { .setup-box { padding: 1.5rem 1.3rem 1.7rem; } }

.setup-brand {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.4rem;
  padding-bottom: 1.2rem;
  border-bottom: 1px solid var(--line);
}

.setup-brand img {
  width: 52px;
  height: 52px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  flex-shrink: 0;
}
.setup-brand-text { min-width: 0; }

.setup-box h1 {
  font-size: 1.15rem;
  margin: 0;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.setup-tagline {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  margin: 0.25rem 0 0;
}

/* ── Wizard progress bar ───────────────────────────────────────────────── */
.su-progress {
  height: 6px;
  border-radius: 99px;
  background: var(--inset);
  overflow: hidden;
  margin-bottom: 1rem;
}
.su-progress-lg { height: 9px; margin: 0.4rem 0 1.2rem; }
.su-progress-fill {
  height: 100%;
  width: 0;
  border-radius: 99px;
  background: linear-gradient(90deg, var(--amber), var(--amber-hi));
  transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Indeterminate shimmer for the long install phases. */
.su-progress-anim {
  background-image: linear-gradient(90deg, var(--amber), var(--amber-hi)),
    linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.35) 50%, transparent 100%);
  background-size: 100% 100%, 40% 100%;
  background-repeat: no-repeat, no-repeat;
  animation: su-shimmer 1.4s linear infinite;
}
@keyframes su-shimmer { from { background-position: 0 0, -40% 0; } to { background-position: 0 0, 140% 0; } }

/* Indeterminate "working" bar — moving diagonal stripes filling the track. Used
   during the game download, where SteamCMD gives no reliable fine-grained
   progress. Clearly animating (never a frozen fill), and the stripes read as
   "in progress, unknown %" rather than a solid "done" bar. */
.su-progress-fill.su-progress-indet {
  width: 100% !important;
  transition: none;
  background-image: repeating-linear-gradient(
    -45deg,
    var(--amber) 0, var(--amber) 12px,
    var(--amber-hi) 12px, var(--amber-hi) 24px
  );
  background-size: 34px 100%;
  animation: su-indet 0.7s linear infinite;
}
@keyframes su-indet { from { background-position: 0 0; } to { background-position: 34px 0; } }

/* ── Stepper dots ──────────────────────────────────────────────────────── */
.su-stepper {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.su-dot {
  flex: 1 1 0;
  min-width: 12px;
  height: 4px;
  border-radius: 99px;
  background: var(--line-2, #2a2c22);
  border: 0;
  padding: 0;
  transition: background 0.3s;
}
.su-dot.done { background: var(--amber); }
.su-dot.active { background: var(--amber-hi); box-shadow: 0 0 0 3px rgba(255, 106, 26, 0.18); }
.su-dot.done { cursor: pointer; }

.su-step-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1.2rem;
}
.su-step-title {
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--text);
}
.su-step-count {
  font-family: var(--mono);
  font-size: 0.74rem;
  color: var(--faint);
  white-space: nowrap;
}

/* ── Wizard steps + stacked fields ─────────────────────────────────────── */
.su-step { animation: su-fade 0.28s ease; }
.su-step[hidden] { display: none; }
@keyframes su-fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.su-field { margin-bottom: 1.35rem; }
.su-field:last-child { margin-bottom: 0; }
.su-field > label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 0.15rem;
}
.su-field .su-help {
  margin: 0 0 0.5rem;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--muted);
}
.su-field input[type="text"],
.su-field input[type="number"],
.su-field input[type="password"] {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--inset);
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.9rem;
}
.su-field input:focus {
  outline: none;
  border-color: var(--amber);
  box-shadow: var(--focus-ring);
}

/* Checkbox field: box + label on one row, help underneath. */
.su-field.su-check {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: 0.6rem;
  row-gap: 0.35rem;
}
.su-field.su-check input[type="checkbox"] {
  width: 1.2rem;
  height: 1.2rem;
  accent-color: var(--amber);
  margin: 0;
}
.su-field.su-check > label { margin: 0; grid-column: 2; }
.su-field.su-check .su-help { grid-column: 1 / -1; margin: 0; }

.su-req { color: #f87171; font-weight: 700; }
.su-opt, .su-hint { font-weight: normal; font-size: 0.82em; color: var(--faint); }

/* ── Review summary ────────────────────────────────────────────────────── */
.su-review {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-2);
  overflow: hidden;
}
.su-review-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.9rem;
  border-top: 1px solid var(--line-2, #2a2c22);
  font-size: 0.86rem;
}
.su-review-row:first-child { border-top: 0; }
.su-review-row .k { color: var(--muted); }
.su-review-row .v { font-family: var(--mono); color: var(--text); text-align: right; word-break: break-word; }
.su-review-row .v.muted { color: var(--faint); }

/* ── Wizard nav ────────────────────────────────────────────────────────── */
.su-nav {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: 1.6rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line);
}
.su-btn-primary, .su-btn-ghost {
  padding: 0.65rem 1.3rem;
  border-radius: var(--radius);
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
}
.su-btn-primary {
  margin-left: auto;
  border: 1px solid var(--amber);
  background: var(--amber);
  color: var(--on-amber);
}
.su-btn-primary:hover { background: var(--amber-hi); border-color: var(--amber-hi); }
.su-btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
.su-btn-ghost {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
}
.su-btn-ghost:hover { color: var(--text); border-color: var(--faint); }
.su-btn-ghost[hidden] { display: none; }

/* ── Install progress log + done / error ───────────────────────────────── */
.su-log {
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.7;
  min-height: 180px;
  max-height: 420px;
  overflow-y: auto;
  color: var(--data-text);
}
.su-done-title { color: #4ade80; margin: 1.2rem 0 0.4rem; font-weight: 700; }
.su-done-text { color: var(--muted); font-size: 0.9em; margin: 0; line-height: 1.6; }
.su-install-error { color: #f87171; margin: 1rem 0 0; }

/* First-run wizard language picker (top-right of the setup box header) — compact
   so it doesn't dominate the header. */
.su-lang { margin-left: auto; flex: none; }
.su-lang[hidden] { display: none; }
.su-lang .admin-lang-select { padding: 0.32rem 0.55rem; font-size: 0.76rem; gap: 0.4rem; letter-spacing: 0.02em; }
.su-lang .lang-flag { width: 16px; height: 12px; }
.su-lang .lang-dd-chev { font-size: 0.62rem; }
.su-lang .lang-dd-list { min-width: 150px; }
.su-lang .lang-dd-item { font-size: 0.8rem; padding: 0.4rem 0.55rem; }
.su-lang .lang-dd-item .lang-flag { width: 18px; height: 13.5px; }

/* ── Field Console premium promo (Frontend tab without a subscription) ────── */
.fc-promo {
  max-width: 580px;
  margin: 2.5rem auto;
  text-align: center;
  padding: 2.2rem 1.8rem;
  background: linear-gradient(135deg, rgba(255, 106, 26, 0.14), rgba(255, 106, 26, 0.02));
  border: 1px solid var(--line-amber);
  border-radius: var(--radius-lg, 14px);
}
.fc-promo-icon {
  width: 54px; height: 54px; margin: 0 auto 1rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(255, 106, 26, 0.16);
  border: 1px solid var(--line-amber);
  color: var(--amber-hi);
}
.fc-promo-icon .ico { width: 1.6em; height: 1.6em; }
.fc-promo h3 { margin: 0 0 0.6rem; font-size: 1.05rem; letter-spacing: 0.02em; color: var(--text); }
.fc-promo p { margin: 0 auto 1.4rem; max-width: 46ch; color: var(--muted); font-size: 0.9rem; line-height: 1.6; }
.fc-promo-btn {
  padding: 0.65rem 1.4rem;
  border: 1px solid var(--amber);
  border-radius: var(--radius);
  background: var(--amber);
  color: var(--on-amber);
  font-size: 0.9rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  cursor: pointer;
}
.fc-promo-btn:hover { background: var(--amber-hi); border-color: var(--amber-hi); }

/* ── Premium lock overlay (frosted glass over visible-but-locked settings) ── */
.prem-lock-wrap { position: relative; min-height: 210px; border-radius: var(--radius-lg); }
.prem-lock-overlay {
  position: absolute; inset: 0; z-index: 6;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 1.6rem 1.2rem;
  background: rgba(15, 16, 19, 0.5);
  -webkit-backdrop-filter: blur(2.5px);
  backdrop-filter: blur(2.5px);
  border-radius: var(--radius-lg);
}
.prem-lock-card {
  position: sticky; top: 1.2rem;
  max-width: 480px; text-align: center;
  padding: 1.5rem 1.6rem;
  background: linear-gradient(135deg, rgba(255, 106, 26, 0.18), rgba(18, 18, 14, 0.94));
  border: 1px solid var(--line-amber);
  border-radius: var(--radius-lg);
  box-shadow: 0 18px 44px -14px rgba(0, 0, 0, 0.7);
}
.prem-lock-card h3 { margin: 0 0 0.5rem; font-size: 1rem; letter-spacing: 0.02em; color: var(--text); }
.prem-lock-card p { margin: 0 auto 1.2rem; max-width: 44ch; color: var(--muted); font-size: 0.86rem; line-height: 1.55; }

/* ── Focus visibility (a11y) ───────────────────────────────────────────── */

button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.gs-tab:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 1px;
}

/* ── Responsive ────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
  header { padding: 0.85rem 1rem; }
  .brand-title { font-size: 1.1rem; }
  /* Tighten the 1.5rem desktop gutters to 1rem so content isn't squeezed. */
  .status-card, .leaderboards, .players, .logs, .controls,
  .screen-bar, .subhead, .settings-form, .settings-note, .settings-message,
  .prem-body, .adm-card, .adm-form, .gs-tabs, .gs-tab-content,
  .players-admin, .bans, .cmd-console, #pl-lockwrap, .pl-workspace, .mu-card {
    margin-left: 1rem; margin-right: 1rem;
  }
  .admin-nav { padding: 0 0.75rem; }
  .anav-btn { padding: 0.75rem 0.7rem; font-size: 0.85rem; }
  /* Modals fill the phone width with slimmer padding. */
  .amodal { padding: 0.6rem; }
  .amodal-box { padding: 1.1rem 1rem; }
  /* Roster table scrolls sideways instead of crushing its columns. */
  .roster-table { min-width: 640px; }
}
/* The roster wrapper always allows horizontal scrolling (kicks in on phones). */
#player-roster { overflow-x: auto; }

/* Phone: enlarge icon/close buttons to a comfortable ~40px touch target. */
@media (max-width: 640px) {
  .amodal-close, .pd-close, .ul-del, .rr-admin-btn {
    width: 40px; height: 40px;
  }
  /* `.mp-close` used to ask for 34px here. It never got it: the touch floor at the end of this file
     puts `min-width: 44px` on the same button from 760px down, and a minimum beats a width. The
     card now sizes it from `--mp-close-box` in that same block, which is the one place it is
     decided. */
}
/* Body-positioned dropdowns never grow wider than the viewport on mobile. */
.cdd-menu, .roster-country-menu, .lang-dd-list { max-width: calc(100vw - 16px); }

/* ── Reduced motion ────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ── Players & bans (admin) ───────────────────────────────────────────────── */
/* Wraps at ANY width it needs to. It used to wrap only below 900px, which left a tablet (and a
   half-width desktop window) at 1024 pushing the page 160px sideways — measured, in a browser,
   one iframe per width. A breakpoint is the wrong tool for "this row got too long". */
.roster-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-bottom: 0.9rem; min-width: 0; }
.player-search { flex: 1; display: flex; align-items: center; gap: 0.5rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.5rem 0.7rem; }
.player-search .ico { color: var(--muted); flex: none; }
/* `min-width: 0` — a flex item defaults to `min-width: auto`, and an <input>'s intrinsic width is
   about twenty characters. Without this the field refuses to shrink and pushes out of its own
   rounded box: measured 30px past it on the Plugins toolbar at tablet width. */
/* `padding: 0` is the third thing this has to say, and it said only the first two.
   The WRAPPER is the field — it owns the border, the fill and the 0.5rem of padding — so the input
   inside it must own none of the three. Background and border were stated here from the start and
   padding was not, which cost nothing while no rule anywhere painted a bare control. The moment the
   control floor above existed, its padding landed here as well and every search box built this way
   — Players, Plugins, the log filters — grew taller than the buttons and dropdowns beside it. The
   owner found it the same afternoon. The field console's `.search input` is the identical shape and
   was fixed for the identical reason; a component that hands its box to a wrapper states all three. */
.player-search input { flex: 1; min-width: 0; background: transparent; border: 0; padding: 0; outline: none; color: var(--text); font-family: var(--ui); font-size: 0.95rem; }
.roster-count { font-family: var(--mono); font-size: 0.82rem; color: var(--muted); white-space: nowrap; }
/* Filter / sort / page-size dropdowns in the roster + chat toolbars. */
.roster-select {
  padding: 0.5rem 0.6rem; font-family: var(--ui); font-size: 0.85rem;
  background: var(--inset); color: var(--text); border: 1px solid var(--line); border-radius: var(--radius);
  cursor: pointer; outline: none;
}
.roster-select:hover, .roster-select:focus { border-color: var(--line-amber); }
/* Custom country filter dropdown (flags in a native <select> aren't possible) */
.roster-country { position: relative; }
.roster-country-btn { display: inline-flex; align-items: center; gap: 0.4rem; }
.roster-country-btn .rr-flag, .roster-country-btn .rr-flag-unknown { margin-right: 0.1rem; }
.dd-caret { color: var(--muted); font-size: 0.7rem; }
.roster-country-menu { position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; min-width: 12rem; max-height: 320px; overflow-y: auto; background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 0.3rem; box-shadow: 0 8px 24px rgba(0,0,0,0.45); }
.rc-item { display: flex; align-items: center; gap: 0.5rem; width: 100%; 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; }
.rc-item:hover { background: var(--hover-bg); color: var(--amber-hi); }
.rc-item.on { background: rgba(255, 106, 26, 0.18); color: var(--amber-hi); }
.rc-name { flex: 1; }
.rc-n { font-family: var(--mono); font-size: 0.78rem; color: var(--muted); }
.rc-empty { display: block; padding: 0.5rem; color: var(--muted); font-size: 0.82rem; }
/* Enhanced native <select> → styled dropdown identical to the country/language pickers */
.cdd { position: relative; display: inline-flex; }
/* The real <select> the custom dropdown replaces. It is visually gone, but it was still 1px wide
   at its STATIC position — out at x=412 on a 360px phone — and an absolutely positioned element
   still extends the page's scroll width from wherever it sits. Pinning it to its (relative)
   wrapper's top-left takes it out of the page width entirely. The `!important` widths are belt
   and braces against any rule that sizes selects by id or by context — and `min-width: 0` is the
   one that actually mattered: `.tr-pick select { min-width: 11rem }` is 187px, and a min-width
   beats a width no matter how many `!important`s the width carries. */
.cdd-native { position: absolute; left: 0; top: 0; width: 1px !important; min-width: 0 !important; max-width: 1px !important; height: 1px; padding: 0; margin: 0; opacity: 0; pointer-events: none; }
/* ⚠ **A DROPDOWN SQUEEZED TO 30px CANNOT SAY WHICH OPTION IS CHOSEN**, which is the only thing a
   dropdown does. Measured across 92 rows: `Eine meiner vorhandenen Zonenkonfigurationen verwenden`
   showed 39% of itself, and even `Not set` did not fit. Two halves to it — the label had `nowrap`
   inherited from here so it could not use a second line, and the button itself had no floor, so
   whatever it sits in was free to take all of its width. It wraps and it keeps nine characters. */
.cdd-btn { display: inline-flex; align-items: center; gap: 0.5rem; white-space: normal; min-width: 9ch; }
/* higher specificity than the global `button { justify-content:center }` + chevron rule → left-align + drop bg arrow */
.cdd .cdd-btn { justify-content: flex-start; text-align: left; background-image: none; padding-right: 0.7rem; }
/* ⚠ **A DROPDOWN THAT CANNOT SAY WHICH OPTION IS CHOSEN.** `Eine meiner vorhandenen
   Zonenkonfigurationen verwenden` rendered in 30px, 39% of it, across 92 measured rows — a
   control whose entire job is reporting a choice. The button sizes to its content, so letting the
   label wrap makes it two lines tall where the option is long and leaves every short one exactly
   as it was. */
.cdd-lbl { white-space: normal; overflow-wrap: anywhere; text-align: left; }
.cdd .cdd-btn .dd-caret { margin-left: auto; padding-left: 0.4rem; }
/* Menu is moved to <body> and absolutely positioned in page coords by JS (escapes clip/transform). */
.cdd-menu { position: absolute; z-index: 3000; min-width: 8rem; overflow-y: auto; background: var(--panel); border: 1px solid var(--line-amber); 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 { 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; }
/* The hover PAIR travels on the element, per the rule at the end of this file. Two hover rules used
   to fight here and a third beat them both: `.cdd-item.cdd-check.on` is three classes, so on a TICKED
   row in a multi-select it won the background (none) while the hover rule still supplied the colour
   (near-black on amber) — near-black text on the dark panel, invisible, and only while hovering a row
   you had already selected. Retuned as a pair instead, so neither half can be overridden alone. */
.cdd-item { --hov-bg: var(--amber); --hov-fg: var(--on-amber); }
.cdd-item.on { background: rgba(255, 106, 26, 0.18); color: var(--amber-hi); }
/* Checkable items — a multi-select rendered as the same dropdown, so "vehicles AND chests"
   is one familiar control instead of a native multi-list. */
.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);
}
/* A ticked row keeps its quiet ground — the tick is the state, not a fill — so it retunes the pair
   rather than cancelling one half of it. */
.cdd-item.cdd-check.on { background: none; --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); }
.cdd-item.cdd-check.on .cdd-box {
  background: var(--amber); border-color: var(--amber);
  /* tick drawn in the box, so no sprite icon is needed inside a generated menu */
  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; }
/* full-width contexts: settings forms (grid control cell), row editors, game settings */
.settings-field .cdd, .er-f .cdd, .gs-field .cdd { grid-area: control; display: flex; width: 100%; }
.settings-field .cdd-btn, .er-f .cdd-btn, .gs-field .cdd-btn { flex: 1; width: 100%; }
/* Live-map layer filters: compact + full-width, matching the original .sf-select look */
.map-layers .cdd { display: flex; width: 100%; }
.map-layers .cdd-btn { width: 100%; padding: 0.4rem 0.55rem; font-size: 0.78rem; font-weight: 600; background: var(--panel-2); }
.map-layers .cdd-menu { font-size: 0.8rem; }
@media (max-width: 900px) { .player-search { flex-basis: 100%; } }
/* Chat history: channel badge + wrapping message cell. */
/* ⚠ A timestamp is the SUBJECT of a chat log and of the activity log, and `nowrap` is what cut
   it: `Jul 5, 2026, 8:00:00 PM` came back at 89% here and 81% in the audit table — a date whose
   time is missing, which is the one field a reader came for. It has commas and spaces to break
   at, and the row is already as tall as its tallest cell. */
.chat-ts { white-space: normal; }
.chat-ch { display: inline-block; padding: 0.1rem 0.5rem; border-radius: var(--pill); font-size: 0.75rem; font-weight: 600; background: var(--inset); border: 1px solid var(--line); color: var(--muted); }
.chat-ch-global { color: var(--amber-hi); border-color: var(--line-amber); }
.chat-ch-squad { color: #57f287; }
.chat-ch-local { color: #6cb2f7; }
.chat-ch-admin { color: var(--alarm); }
.chat-msg { word-break: break-word; }
/* Chat table: shrink time/channel/player to their content so the MESSAGE column
   gets all remaining width — everything packs left instead of spreading out. */
.chat-table th, .chat-table td { text-align: left; }
.chat-table th:nth-child(1), .chat-table td:nth-child(1),
.chat-table th:nth-child(2), .chat-table td:nth-child(2),
.chat-table th:nth-child(3), .chat-table td:nth-child(3) { width: 1%; white-space: nowrap; }
/* ⚠ **`width: 1%` IS SHRINK-TO-FIT ONLY WHILE THE TABLE IS AUTO-LAID-OUT, AND THIS ONE IS FIXED.**
   `.data-table.roster-table` declares `table-layout: fixed`, so that 1% is taken literally and the
   timestamp column came out at 174px against text that needs about 195 — `Jul 5, 2026, 8:00:00 PM`
   cut to 89% of itself at a 1414px viewport, which is a desktop. The trick was written for an
   auto-laid-out table and quietly stopped being one.
   So the first column states a real width and the time wraps inside it. It breaks at its own
   spaces, never mid-token, and the row is already as tall as its message cell. */
.chat-table th:nth-child(1), .chat-table td:nth-child(1) { width: 9.5rem; }
/* …and the cell itself, because `.roster-table td` carries the ellipsis as well as the nowrap and a
   column that wraps while its cell still elides is a column that still cuts. This rule was tried
   BEFORE the width above existed and made things worse — without a stated width the column really
   is 1% and the text spills out of it — so the two belong together and neither works alone. */
.data-table.roster-table td.chat-ts { white-space: normal; overflow: visible; text-overflow: clip; }
/* Log Viewer table (time · category · entry): undo the chat-table shrink on the
   LAST column so the log entry takes all remaining width and wraps normally. */
.gamelog-table th:last-child, .gamelog-table td:last-child { width: auto; white-space: normal; }
.gamelog-line { font-family: var(--mono); font-size: 0.85rem; }
/* Player names inside a log entry are clickable; 📍 opens the position on scum-map. */
/* Raw log line reads as plain text; players + locations get a subtle underline to show they're
   clickable, without recolouring the sentence (see increment 19). */
.gl-player { color: inherit; cursor: pointer; text-decoration: underline; text-decoration-color: rgba(255, 106, 26, 0.4); text-decoration-thickness: 1px; text-underline-offset: 2px; }
.gl-player:hover { color: var(--amber-hi); text-decoration-color: var(--amber); }
.gl-loc { color: inherit; cursor: pointer; text-decoration: underline dotted; text-decoration-color: rgba(255, 106, 26, 0.45); text-underline-offset: 2px; }
.gl-loc:hover { color: var(--amber); text-decoration-color: var(--amber); }
.gl-map {
  display: inline-flex; align-items: center; gap: 0.15rem; margin-left: 0.3rem;
  padding: 0 0.45rem; border: 1px solid var(--line); border-radius: var(--pill);
  color: var(--muted); font-size: 0.78rem; text-decoration: none; white-space: nowrap;
}
.gl-map:hover { color: var(--amber-hi); border-color: var(--line-amber); }
.gl-dim { color: var(--muted); }
.gl-sid { font-size: 0.8em; }
/* Item / vehicle / weapon class codes → compact mono chips. */
.gl-item { white-space: nowrap; }   /* raw look — styling is in the increment-21 block (no chip box) */
/* Category badge palettes: stable colour per tag (hash-picked), plus semantic
   green/red for login/activated vs logout/finished. */
.gl-tag-ok  { color: #57f287; }
.gl-tag-bad { color: #eb6a6a; }
.gl-tag-0 { color: #6cb2f7; }
.gl-tag-1 { color: #c792ea; }
.gl-tag-2 { color: var(--amber-hi); border-color: var(--line-amber); }
.gl-tag-3 { color: #4dd0c4; }
.gl-tag-4 { color: #e8a87c; }
.gl-tag-5 { color: #9fb2f7; }

/* Player roster table (admin: status · name · steam · squad · IP · Discord · logins) */
.roster-table td.rr-status { width: 1.8rem; text-align: center; }
/* A player's name, which is the column every one of these tables is read BY. It wraps for the
   reason the squad board's own name column does: a name can be one unbroken word, and three
   quarters of one identifies nobody. */
.roster-table td.rr-name { font-weight: 600; color: var(--text); white-space: normal; overflow-wrap: anywhere; }
/* ⚠ **THE `nowrap` HERE IS WHAT CUT EVERY TIMESTAMP IN THE PANEL.** `Jul 5, 2026, 8:00:00 PM`
   came back at 81% in the activity log, 89% in chat and 95% on the squad board — a date with its
   time missing, on three tables whose subject is WHEN something happened. A timestamp has commas
   and spaces in it, so it has somewhere to break, and the row is already as tall as its tallest
   cell. What `nowrap` was protecting was the row height, which nothing here promises. */
.roster-table td.dim { white-space: normal; }
.rr-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--faint); }
.rr-dot.on { background: var(--online); box-shadow: 0 0 7px var(--online); }
.rr-squad, .rr-discord { font-size: 0.85rem; }
.rr-online { color: var(--online); font-weight: 600; }
.roster-table td.rr-act { width: 2.2rem; text-align: center; }
.rr-admin-btn { border: 1px solid var(--line); background: var(--inset); color: var(--amber); cursor: pointer;
  width: 26px; height: 26px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; }
.rr-admin-btn:hover { background: var(--amber); color: var(--on-amber); border-color: var(--amber); }
.rr-admin-btn .ico { width: 15px; height: 15px; }
#pd-admin-actions { color: var(--amber); }

/* Chat: send-message box */
.chat-send { display: flex; flex-direction: column; gap: 0.7rem; }
.cs-head { display: flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.18em; color: var(--text); }
.cs-head .ico { color: var(--amber); }
.cs-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.cs-inline { display: inline-flex; align-items: center; gap: 0.45rem; }
/* Why the form above is grey. `:empty` rather than a class, so "usable" is the absence of a reason
   rather than a second state something has to remember to clear. */
.cs-why { margin: 0.35rem 0 0; color: var(--muted); font-size: 0.82rem; line-height: 1.5; }
.cs-why:empty { display: none; }

/* ── who the message goes to ──────────────────────────────────────────────────
   The picker is a column: the chips that have been chosen, then the field that adds
   another. No width is declared anywhere in here — everything wraps or scrolls, which
   is what keeps it inside a 320px card (see `check-mobile`). */
.cs-pick { display: flex; flex-direction: column; gap: 0.45rem; }
/* The one line above the chips, and it is not decoration: the SAME list of names means "send to
   these" in one mode and "send to everyone but these" in the other, so without it a reader has to
   remember which way round the dropdown two rows up was left. `csShowPick()` writes it and
   `data-mode` on the parent is the machine-readable half of the same claim.

   In the excluding mode it is `--standby`, which is this stylesheet's existing word for "read this
   before you act" (the parts panel's own note uses it). Colour only, on the inherited background —
   no pair is retuned here, which is what keeps `check-contrast` out of it. */
.cs-pick-lab { margin: 0; color: var(--muted); font-size: 0.78rem; line-height: 1.45; }
.cs-pick[data-mode="except"] .cs-pick-lab { color: var(--standby); }
.cs-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.cs-chips:empty { display: none; }
/* Modelled on `.roster-fchip`, the panel's other removable chip, so a recipient reads as the
   same kind of object an active filter does. */
.cs-chip {
  display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0;
  padding: 0.2rem 0.25rem 0.2rem 0.5rem; font-size: 0.8rem;
  background: rgba(255, 106, 26, 0.12); border: 1px solid var(--line-amber);
  border-radius: var(--pill); color: var(--amber-hi);
}
.cs-chip .ico { width: 13px; height: 13px; flex: none; }
.cs-chip-n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The half of the chip that is the warning: a squad with nobody online, or a name that has left.
   Muted rather than red — it is a fact about the world, not a fault. */
.cs-chip-sub { color: var(--muted); font-size: 0.74rem; white-space: nowrap; }
.cs-chip-off { background: var(--inset); border-color: var(--line-2); color: var(--muted); }
.cs-chip-x {
  background: none; border: none; color: inherit; cursor: pointer;
  font-size: 1.05rem; line-height: 1; padding: 0 0.15rem;
}
.cs-find { position: relative; }
.cs-search { width: 100%; }
/* The suggestions hang over whatever is under them, so choosing a second recipient does not push
   the message box down the screen between one click and the next. */
.cs-suggest {
  position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + 0.25rem);
  max-height: 15rem; overflow-y: auto;
  background: var(--panel-2, var(--panel)); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.cs-sug {
  display: flex; align-items: center; gap: 0.45rem; width: 100%; min-width: 0;
  text-align: left; font: inherit; letter-spacing: 0; cursor: pointer;
  background: none; border: 0; border-radius: 0; padding: 0.35rem 0.5rem; color: var(--text);
  /* Tinted: it sits in a list of plain rows and must still read as one of them. */
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: transparent;
}
.cs-sug .ico { width: 14px; height: 14px; flex: none; color: var(--amber); }
.cs-sug-n { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-sug-sub { flex: none; color: var(--muted); font-size: 0.74rem; }
.cs-sug-off .ico { color: var(--muted); }
.cs-sug-none, .cs-saved-none { margin: 0; padding: 0.5rem; color: var(--muted); font-size: 0.8rem; }
/* The list was CUT. It sits under the last row and is ruled off from it, because it is a fact about
   the list rather than another thing to pick. */
.cs-sug-more { border-top: 1px solid var(--line-2); }
/* The sentence saying who is about to get this. It is never empty on this screen — "Nobody chosen
   yet" is one of the things it has to say — so there is no `:empty` rule to hide it. */
.cs-reach { margin: 0; color: var(--muted); font-size: 0.82rem; line-height: 1.5; }

/* ── the messages an admin keeps ─────────────────────────────────────────────
   The text in each row is the owner's own, so it is the one thing here given room:
   it wraps rather than being cut, because half a sentence is not a message you can
   recognise. */
.cs-saved { display: flex; flex-direction: column; gap: 0.4rem; }
.cs-saved-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap;
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted); }
.cs-saved-list { display: flex; flex-direction: column; gap: 0.25rem; max-height: 14rem; overflow-y: auto; }
.cs-saved-row { display: flex; align-items: flex-start; gap: 0.3rem; }
.cs-saved-use {
  flex: 1 1 auto; min-width: 0; text-align: left; font: inherit; letter-spacing: 0; cursor: pointer;
  background: var(--inset); border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 0.3rem 0.45rem; color: var(--text); line-height: 1.45; word-break: break-word;
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: var(--line-amber);
}
.cs-saved-x {
  flex: none; background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 1.05rem; line-height: 1; padding: 0.3rem 0.2rem;
}

/* locked config fields (premium/bridge-gated, e.g. Discord→game relay) */
.cfg-field-locked { opacity: 0.5; }
.cfg-lock-note { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--amber);
  background: rgba(230,185,106,0.08); border: 1px solid var(--line-amber); border-radius: 8px; padding: 0.5rem 0.7rem; margin: 0.5rem 0; }
.cfg-lock-note .ico { width: 14px; height: 14px; }
/* Roster column widths. Everything except Name and Squad is fixed-shape content (an id, a figure,
   a timestamp), so those columns are told to take only what they need — `width: 1%` on a table cell
   collapses it to its content — and Name absorbs the slack. Without this the browser shared the
   width evenly and a 3-digit ID claimed as much room as the player name. */
/* Column WIDTHS live with the fixed-layout block further down (the table is `table-layout: fixed`,
   so a width set here on a <td> would do nothing anyway). This only keeps their content on one line. */
.roster-table td.c-uid, .roster-table td.c-steam, .roster-table td.num,
.roster-table td.c-ip, .roster-table td.c-login, .roster-table td.c-logout { white-space: nowrap; }
/* Hidden by CLASS, never :nth-child — column positions move whenever a column is added.
   ⚠ EVERY ONE OF THESE BREAKPOINTS IS DERIVED FROM THE WIDTHS FURTHER DOWN, and they were 200px
   too low. A column set that does not fit is not hidden — it is SHOWN, ellipsised, and a value cut
   to `1…` is worse than a column that is not there, because the reader cannot tell a truncated
   number from a real one. Each threshold below is the narrowest viewport at which the set above it
   still fits: 14 columns need ~1315px of table, 13 need ~1193, 12 need ~1067, 10 need ~897, and a
   viewport gives the table about 100px less than itself. Move a width and move these with it —
   `check-panel-render` measures the roster at seven viewports for exactly this reason. */
@media (max-width: 1440px) {
  .roster-table th.c-logout, .roster-table td.c-logout { display: none; }
}
@media (max-width: 1290px) {
  .roster-table th.c-login, .roster-table td.c-login { display: none; }
}
@media (max-width: 1160px) {
  .roster-table th.c-fame, .roster-table td.c-fame,
  .roster-table th.c-playtime, .roster-table td.c-playtime { display: none; }
}
@media (max-width: 990px) {
  .roster-table th.c-ip, .roster-table td.c-ip,
  .roster-table th.c-money, .roster-table td.c-money,
  .roster-table th.c-gold, .roster-table td.c-gold { display: none; }
}
/* Squad block in the player detail */
.pd-squad-head { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.55rem; }
.pd-squad-name { font-weight: 700; font-size: 1.05rem; }
.pd-squad-meta { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); }
/* The squad's own crest, composited by /squadEmblem.js. Sized in `em` where it stands in for the
   shield glyph beside a line of text, so it tracks whatever that line is set in; the square is the
   mask's own and must not be stretched, which is why both halves are pinned rather than one.
   `align-self` is not decoration here: `.pd-squad-head` aligns on the BASELINE, and an image has
   its baseline at its bottom edge, so an unaligned crest would hang the whole row off its own foot. */
.pd-crest { width: 1.1em; height: 1.1em; flex: 0 0 auto; align-self: center; border-radius: 2px; object-fit: contain; }
/* 2.2rem is the field console's own `.sq-head-face`, deliberately: one squad wears one crest at one
   size whichever surface is naming it, and two numbers a rem apart read as two different badges. */
.pd-crest-lg { width: 2.2rem; height: 2.2rem; }
.pd-members { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.4rem; margin-bottom: 0.9rem; }
.pd-member {
  display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left;
  padding: 0.45rem 0.6rem; background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius); color: var(--text); font-family: var(--ui);
}
.pd-member:hover { border-color: var(--amber); background: rgba(255, 106, 26, 0.14); }
.pd-m-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--faint); flex: none; }
.pd-m-dot.on { background: var(--online); box-shadow: 0 0 7px var(--online); }
.pd-m-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-m-rank { margin-left: auto; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--amber); white-space: nowrap; }

/* ── A PLAYER'S FACE (`/playerFace.js`, shared with the field console) ──────────────────────────
   ONE BOX, TWO CONTENTS: a linked Discord account's picture is an <img>, an unlinked player is a
   <span> holding their initial, and they are the same element at the same size in the same place.
   A list where some rows carry a picture and others carry nothing has two left edges, which is what
   the owner meant by *"spravne zarovnany vse"*.

   ⚠ **`:where()` FOR THE BASE, AND THAT IS NOT A PREFERENCE.** `.ssa-face` and `.pd-m-face` are
   both one class, so equal specificity would be settled by ORDER — the same tie that gave this
   product's own control floor a second border on every search box on the public site. Inside
   `:where()` a selector weighs nothing, so every size below wins wherever it is written.

   The face is CLIPPED TO A CIRCLE where `.pd-crest` above deliberately is not: a badge's corners are
   part of the shape a squad chose, a photograph's are not. */
:where(.ssa-face) {
  width: 1.25rem; height: 1.25rem; 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(--inset); border: 1px solid var(--line);
  font-family: var(--mono); font-weight: 700; font-size: 0.62rem; line-height: 1;
  color: var(--amber); text-transform: uppercase; user-select: none;
}
/* Beside a squad member's name, at the row size the crest above this list uses for a heading. */
.pd-m-face { width: 1.4rem; height: 1.4rem; font-size: 0.7rem; }
/* In the dashboard roster and the linked-accounts table, both of which are mono text rows. */
.ol-face, .dc-face { width: 1.25rem; height: 1.25rem; margin-right: 0.35rem; }
.dc-player { white-space: nowrap; }

.player-detail { background: var(--inset); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 0.9rem 1rem; }
.player-detail.hidden { display: none; }
/* Player detail hero: avatar + name/flag/status + Steam ID subline */
.pd-hero { display: flex; align-items: center; gap: 0.9rem; margin-bottom: 0.9rem; padding-right: 2.2rem; }
.pd-avatar { position: relative; width: 54px; height: 54px; border-radius: 50%; flex: none; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.pd-avatar-ph { background: var(--inset); border: 1px solid var(--line-amber); color: var(--amber); font-weight: 700; font-size: 1.5rem; }
.pd-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.pd-hero-main { min-width: 0; display: flex; flex-direction: column; gap: 0.28rem; }
.pd-hero-top { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
.pd-hero-sub { display: flex; align-items: center; gap: 0.4rem; font-family: var(--mono); font-size: 0.8rem; color: var(--muted); }
.pd-hero-sub .ico { width: 1em; height: 1em; color: var(--amber); }
.pd-hero-top .rr-flag { width: 1.5em; height: 1.12em; margin: 0; }
.pd-name { font-size: 1.25rem; font-weight: 700; }
.pd-online { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.15rem 0.5rem; border-radius: var(--pill); border: 1px solid var(--line); color: var(--muted); }
.pd-online.on { color: var(--online); border-color: var(--online); }
.pd-banned { font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--alarm); border: 1px solid var(--alarm); padding: 0.15rem 0.5rem; border-radius: var(--pill); }
.pd-id { display: flex; flex-wrap: wrap; gap: 0.35rem 1.4rem; margin-bottom: 0.8rem; padding-bottom: 0.7rem; border-bottom: 1px solid var(--line); }
/* CFTools-style player summary card */
.pd-card { background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 1rem 1.1rem; margin: 0.5rem 0 1.1rem; }
.pd-id-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 0.8rem 1.5rem; }
.pd-idc { display: flex; flex-direction: column; gap: 0.22rem; min-width: 0; }
.pd-il { display: flex; align-items: center; gap: 0.35rem; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 600; }
.pd-il .ico { width: 1.5em; height: 1.5em; color: var(--amber); flex: none; }
.pd-iv { font-size: 0.9rem; color: var(--text); overflow-wrap: anywhere; display: flex; align-items: center; gap: 0.3rem; }
.pd-iv.mono { font-family: var(--mono); font-size: 0.85rem; }
/* Position → compact map link (mono, fits its cell; full value in the tooltip). */
.pd-maplink {
  font-family: var(--mono); font-size: 0.82rem; font-variant-numeric: tabular-nums;
  color: var(--text); text-decoration: none; border-bottom: 1px dashed var(--line-amber); white-space: nowrap;
}
.pd-maplink:hover { color: var(--amber-hi); }
.pd-quick { display: flex; flex-wrap: wrap; gap: 0.55rem; margin: 0 0 0.9rem; }
.pd-q { flex: 1 1 88px; display: flex; flex-direction: column; align-items: center; gap: 0.18rem; padding: 0.55rem 0.4rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); }
.pd-qv { font-family: var(--mono); font-size: 1.05rem; font-weight: 700; color: var(--amber-hi); }
.pd-qk { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.pd-card .pd-ban { margin-top: 1rem; padding-top: 0.9rem; border-top: 1px solid var(--line); }
.pd-id span { font-size: 0.86rem; color: var(--text); font-family: var(--mono); }
.pd-id i { font-style: normal; color: var(--muted); margin-right: 0.3rem; text-transform: uppercase; font-size: 0.66rem; letter-spacing: 0.04em; }
/* Stat cards (icon + value/label), uniform like the public My Stats. */
.pd-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 0.5rem; margin-bottom: 1.1rem; }
.pd-stat { display: flex; align-items: center; gap: 0.55rem; padding: 0.55rem 0.7rem; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); }
.pd-stat .ico { color: var(--amber); font-size: 1.3rem; flex: none; }
.pd-stat-t { display: flex; flex-direction: column; line-height: 1.1; min-width: 0; }
.pd-stat-v { font-family: var(--mono); font-size: 1.05rem; color: var(--text); overflow-wrap: anywhere; min-width: 0; }
.pd-stat-k { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
/* Player-detail tabs + close */
.pd-close { margin-left: auto; cursor: pointer; background: transparent; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); width: 28px; height: 28px; font-size: 0.9rem; line-height: 1; }
.pd-close:hover { background: transparent; color: var(--alarm); border-color: var(--alarm); }
/* Detail tabs: pill buttons (Stats · Skills · Squad · Bank) — clearer active
   state than the old underline-only look. */
.pd-tabnav { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 1rem 0 0.9rem; }
.pd-tab {
  display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--pill);
  color: var(--muted); font-family: var(--ui); font-weight: 600; font-size: 0.85rem;
  padding: 0.45rem 0.95rem;
}
.pd-tab .ico { font-size: 1rem; }
.pd-tab:hover { color: var(--text); background: var(--hover-bg); border-color: var(--line-amber); }
.pd-tab.active { color: var(--amber-hi); border-color: var(--line-amber); background: rgba(255, 154, 60, 0.09); }
.pd-pane[hidden] { display: none; }
/* Skills & attributes (grouped by attribute, with the attribute value) */
.pd-sec-title { display: flex; align-items: center; gap: 0.4rem; margin: 0.2rem 0 0.7rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--amber); font-weight: 700; }
.pd-sec-title .ico { color: var(--amber); }
.pd-skills { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 0.8rem; margin-bottom: 0.9rem; }
.pd-sg { display: flex; flex-direction: column; gap: 0.4rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.8rem 0.85rem; }
.pd-sg-head { display: flex; align-items: center; gap: 0.4rem; padding-bottom: 0.3rem; border-bottom: 1px solid var(--line-amber); }
.pd-sg-head .ico { color: var(--amber); }
.pd-sg-head b { color: var(--amber); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
.pd-attr { margin-left: auto; font-family: var(--mono); color: var(--amber-hi); font-size: 0.95rem; }
.pd-sk { display: grid; grid-template-columns: 1fr auto; gap: 0.1rem 0.5rem; font-size: 0.82rem; }
.pd-sk-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-sk-tier { font-size: 0.68rem; text-transform: uppercase; color: var(--muted); letter-spacing: 0.04em; }
.pd-sk-bar { grid-column: 1 / -1; height: 4px; background: var(--inset); border: 1px solid var(--line); border-radius: 3px; overflow: hidden; }
.pd-sk-bar > span { display: block; height: 100%; background: var(--amber); }
.pd-sk.sk-l0 .pd-sk-bar > span { background: var(--faint); }
.pd-sk.sk-l1 .pd-sk-bar > span { background: var(--standby); }
.pd-sk.sk-l2 .pd-sk-bar > span { background: #b8bcc2; }
.pd-sk.sk-l3 .pd-sk-bar > span { background: var(--amber); }
.pd-sk.sk-l4 .pd-sk-bar > span { background: var(--amber-hi); }
.pd-ban-note { font-size: 0.82rem; color: var(--standby); }
/* Admin action bar on the player card */
.pd-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 1rem; padding-top: 0.9rem; border-top: 1px solid var(--line); }
.pd-actions .ico { font-size: 0.95rem; }
.pd-note-in { display: block; width: 100%; margin-top: 0.6rem; min-height: 68px; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius); color: var(--text); font-family: var(--ui); font-size: 0.88rem; line-height: 1.4; padding: 0.55rem 0.7rem; box-sizing: border-box; resize: vertical; }
.pd-note-in:focus { outline: none; border-color: var(--amber); }
.pd-action-msg { display: inline-block; margin-top: 0.5rem; font-size: 0.85rem; color: var(--standby); }
#ban-msg { font-size: 0.85rem; color: var(--standby); }

/* Player Intelligence / risk */
.bad { color: #e39a9a; }
.pd-stat-v.bad { color: #e39a9a; }
.pd-risk { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--line); }
.pd-risk.high  { color: #f0b2b2; border-color: rgba(224,80,80,0.6);  background: rgba(224,80,80,0.12); }
.pd-risk.watch { color: #e6c789; border-color: rgba(230,166,26,0.55); background: rgba(230,166,26,0.12); }
.pd-risk.clean { color: #a4d6ad; border-color: rgba(59,165,93,0.5);  background: rgba(59,165,93,0.12); }
.pd-risk-head { display: flex; align-items: center; gap: 0.8rem; padding: 0.7rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); }
.pd-risk-head.high  { border-color: rgba(224,80,80,0.5); }
.pd-risk-head.watch { border-color: rgba(230,166,26,0.45); }
.pd-risk-badge { font-weight: 700; font-size: 0.9rem; white-space: nowrap; }
.pd-risk-bar { flex: 1; height: 8px; background: var(--bg); border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.pd-risk-fill { height: 100%; background: linear-gradient(90deg, var(--standby), var(--amber-hi)); }
.pd-risk-head.high .pd-risk-fill  { background: linear-gradient(90deg, #d98b8b, #e05050); }
.pd-risk-head.watch .pd-risk-fill { background: linear-gradient(90deg, #d9bd7a, #e6a61a); }
.pd-risk-head.clean .pd-risk-fill { background: linear-gradient(90deg, #7ec48a, #3ba55d); }
.pd-risk-score { font-family: var(--mono); font-size: 1.1rem; font-weight: 700; white-space: nowrap; }
.pd-risk-score span { font-size: 0.72rem; color: var(--muted); }
.pd-flags { margin: 0.3rem 0 0.6rem; padding-left: 1.1rem; font-size: 0.85rem; color: var(--standby); }
.pd-flags li { margin: 0.15rem 0; }
/* Intel tab — readable label→value rows (values wrap fully, no truncation) */
.pi-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 1rem; margin-bottom: 1.1rem; }
.pi-rows { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--panel-2); }
.pi-row { display: grid; grid-template-columns: minmax(104px, 36%) 1fr; gap: 0.3rem 1rem; padding: 0.5rem 0.8rem; border-top: 1px solid var(--line); align-items: baseline; }
.pi-row:first-child { border-top: none; }
.pi-k { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); font-weight: 600; }
.pi-v { color: var(--text); font-size: 0.88rem; overflow-wrap: anywhere; display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; }
.pi-row.bad .pi-v { color: #e39a9a; }
.pi-v .rr-flag { margin: 0; }
/* Intel-tab section headings get a bit more breathing room */
.pd-pane[data-ppane="intel"] .pd-sec-title { margin-top: 1.5rem; margin-bottom: 1rem; }
.pd-intel-checked { align-self: center; font-size: 0.76rem; color: var(--muted); }
/* Risk marker shown after the player name — icon coloured by level */
.rr-risk { display: inline-flex; vertical-align: -0.15em; }
.rr-risk .ico { width: 0.95em; height: 0.95em; }
.rr-risk.high { color: #e05050; }
.rr-risk.watch { color: #e6a61a; }
.rr-risk.clean { color: #57b87f; opacity: 0.85; }
.rr-risk.watch .ico, .rr-risk.clean .ico { stroke-width: 1.9; }
/* Country flag before a player name (dashboard + roster) — bundled SVG, real flag on every OS */
img.rr-flag { width: 1.15em; height: 0.86em; margin-right: 0.35rem; vertical-align: -0.08em; border-radius: 1px; box-shadow: 0 0 0 1px rgba(0,0,0,0.25); object-fit: cover; }
/* "unknown country" placeholder — identical footprint to a flag so rows stay aligned */
.rr-flag-unknown { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 1.15em; height: 0.86em; margin-right: 0.35rem; vertical-align: -0.08em; border-radius: 1px; background: var(--inset); box-shadow: 0 0 0 1px rgba(0,0,0,0.25); color: var(--muted); overflow: hidden; }
.rr-flag-unknown i { font-size: 0.58rem; font-style: normal; font-weight: 700; line-height: 1; }
.pd-hero-top .rr-flag-unknown { width: 1.5em; height: 1.12em; font-size: 0.8em; }

/* Player Intelligence dashboard panel (above the online-players box) */
/* Dashboard panel — clickable stat tiles (each filters the Players table) */
.intel-tiles { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
.intel-tile { display: inline-flex; align-items: center; gap: 0.45rem; padding: 0.55rem 0.85rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); color: var(--text); cursor: pointer; font: inherit; transition: border-color 0.12s, background 0.12s; }
.intel-tile:hover { border-color: var(--amber); background: rgba(255, 106, 26, 0.14); }
.intel-tile .ico { width: 1.15em; height: 1.15em; color: var(--muted); }
.intel-tile .v { font-size: 1.2rem; font-weight: 700; font-family: var(--mono); color: var(--text); }
.intel-tile .k { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.intel-tile.high .ico, .intel-tile.high .v { color: #e05050; }
.intel-tile.watch .ico, .intel-tile.watch .v { color: #e6a61a; }
.intel-tile.wl .ico { color: var(--amber); }
.intel-tile.bad .ico { color: #e05050; }
/* panel sub-sections: top countries + flagged */
.intel-sub-block { margin-top: 0.9rem; }
.intel-sub-h { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); font-weight: 700; margin-bottom: 0.45rem; }
.intel-countries { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.intel-cc { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.24rem 0.55rem 0.24rem 0.4rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--pill); color: var(--text); cursor: pointer; font: inherit; }
.intel-cc:hover { border-color: var(--amber); background: rgba(255, 106, 26, 0.14); }
.intel-cc .rr-flag { margin: 0; }
.intel-cc-n { font-family: var(--mono); font-size: 0.82rem; font-weight: 700; color: var(--text); }
/* flagged players — name button + a "mark reviewed" ✓ */
.intel-flagged { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.intel-fp { display: inline-flex; align-items: stretch; background: var(--inset); border: 1px solid var(--line); border-radius: var(--pill); overflow: hidden; max-width: 17rem; }
.intel-fp:hover { border-color: var(--line-amber); }
.intel-fp-open { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.55rem; background: none; border: none; color: var(--text); cursor: pointer; font: inherit; font-size: 0.84rem; min-width: 0; }
.intel-fp-open:hover { background: rgba(255, 106, 26, 0.14); }
.intel-fp .rr-risk .ico { width: 0.9em; height: 0.9em; }
.intel-fp-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.intel-fp-on { width: 7px; height: 7px; border-radius: 50%; background: var(--online); flex: none; margin-left: 0.1rem; }
.intel-fp-ok { display: inline-flex; align-items: center; justify-content: center; padding: 0 0.45rem; background: none; border: none; border-left: 1px solid var(--line); color: var(--muted); cursor: pointer; }
.intel-fp-ok:hover { background: rgba(87,184,127,0.16); color: #57b87f; }
.intel-fp-ok .ico { width: 0.95em; height: 0.95em; }
/* active dashboard filter chip in the roster bar */
.roster-intel-chip:empty { display: none; }
.roster-fchip { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.2rem 0.25rem 0.2rem 0.5rem; background: rgba(255, 106, 26,0.12); border: 1px solid var(--line-amber); border-radius: var(--pill); font-size: 0.8rem; color: var(--amber-hi); }
.roster-fchip .rr-flag { margin: 0 0.15rem 0 0; }
.roster-fchip-x { background: none; border: none; color: inherit; cursor: pointer; font-size: 1.05rem; line-height: 1; padding: 0 0.15rem; }
.roster-fchip-x:hover { color: var(--text); }

.bans code { font-family: var(--mono); color: var(--amber); }
#bans-table { width: 100%; border-collapse: collapse; }
#bans-table th, #bans-table td { text-align: left; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--line); font-size: 0.9rem; }
#bans-table th { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
#bans-table .mono { font-family: var(--mono); color: var(--muted); }
#bans-table .muted { color: var(--muted); }

/* ── Roster pagination ────────────────────────────────────────────────────── */
.roster-pager { display: flex; align-items: center; justify-content: center; gap: 0.9rem; margin-top: 0.9rem; }
.roster-pager.hidden { display: none; }
.roster-pager button[disabled] { opacity: 0.4; cursor: default; }
.rr-uid, .pd-uid { margin-left: .45rem; color: var(--muted); font-size: .78rem; opacity: .85; }
.roster-pager-info { flex-basis: 100%; text-align: center; font-family: var(--mono); font-size: 0.8rem; color: var(--muted); }

/* ── Clickable player rows / online list ──────────────────────────────────── */
.clickable { cursor: pointer; transition: color 0.12s, background 0.12s; }
#players-list li.clickable:hover { color: var(--amber-hi); }
#dc-linked-tbody tr.clickable:hover { background: rgba(255, 106, 26, 0.06); }
.players-hint { color: var(--muted); font-size: 0.82rem; margin: 0 0 0.6rem; }

/* ── Field Console link (back to public site) ─────────────────────────────── */
.nav-actions .navlink, .header-actions .navlink {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.5rem 1rem; border: 1px solid var(--line-amber); border-radius: var(--radius);
  background: var(--panel-2); color: var(--amber-hi);
  font-family: var(--ui); font-weight: 600; font-size: 0.82rem; letter-spacing: 0.05em; text-transform: uppercase;
}
.nav-actions .navlink:hover, .header-actions .navlink:hover { background: var(--panel); border-color: var(--amber); }

/* ── Player detail: bank cards ────────────────────────────────────────────── */
.pd-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); gap: 0.7rem; margin-bottom: 0.9rem; }
.pd-cards .pd-card { display: flex; gap: 0.8rem; padding: 0.7rem 0.8rem; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); margin: 0; }
.pd-card-img { width: 80px; height: 52px; flex: none; object-fit: contain; align-self: center; }
.pd-card-body { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; }
.pd-card-type { font-weight: 700; color: var(--amber); text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.82rem; }
.pd-card-rows { display: flex; flex-wrap: wrap; gap: 0.1rem 1rem; font-size: 0.8rem; color: var(--muted); }
.pd-card-rows b { color: var(--text); font-family: var(--mono); margin-left: 0.2rem; }

/* ── Dashboard top menu: lighter, Field-Console-like nav (ghost buttons) ───── */
#dashboard-screen .nav-actions .secondary,
#dashboard-screen .nav-actions .navlink {
  background: transparent;
  border: 1px solid transparent;
  color: var(--muted);
}
#dashboard-screen .nav-actions .secondary:hover,
#dashboard-screen .nav-actions .navlink:hover {
  color: var(--amber-hi);
  border-color: var(--line-amber);
  background: var(--panel-2);
}
#dashboard-screen .nav-actions #logout-btn:hover { color: var(--alarm); border-color: rgba(255, 92, 92,0.4); background: transparent; }

/* ── App footer + App status modal ─────────────────────────────────────────── */
.foot-link { background: none; border: 0; padding: 0; color: var(--amber); cursor: pointer; font: inherit; }
.foot-link:hover { background: none; border: 0; color: var(--amber-hi); text-decoration: underline; }
.amodal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.6); padding: 1rem; }
.amodal.hidden { display: none; }
.amodal-box { position: relative; background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 1.4rem 1.6rem; max-width: 440px; width: 100%; max-height: 85vh; overflow: auto; }
.amodal.wide .amodal-box { max-width: 1040px; }
.amodal-close { position: absolute; top: 0.7rem; right: 0.7rem; cursor: pointer; background: transparent; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); width: 28px; height: 28px; }
.amodal-close:hover { background: transparent; color: var(--alarm); border-color: var(--alarm); }
.amodal-box h2 { margin: 0 0 0.4rem; font-size: 1.1rem; }
.appstat { display: flex; flex-direction: column; margin-top: 0.6rem; }
.appstat-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.55rem 0.1rem; border-bottom: 1px solid var(--line); }
.appstat-row:last-child { border-bottom: 0; }
.appstat-k { color: var(--muted); font-size: 0.85rem; }
.appstat-v { color: var(--text); font-family: var(--mono); margin-left: auto; text-align: right; }

/* ── Premium ──────────────────────────────────────────────────────────────── */
.prem-body { margin: 1rem 1.5rem 2rem; }
/* Notes/messages inside the premium column already sit in the padded body — drop
   the global horizontal margin (was double-indenting them) and give real spacing. */
.prem-body .settings-note, .prem-body .settings-message { margin-left: 0; margin-right: 0; }
.prem-form .settings-note { margin: 0.4rem 0 1.1rem; }
.prem-form .settings-message { margin: 0 0 0.6rem; }
/* "These boxes are not your credentials." A `.settings-note` is `--muted` by design, which is the
   right weight for a standing explanation and the wrong one for a line that is only ever on screen
   because a read failed. Same amber rule the audit IP hint uses — one idiom, not a second. */
.prem-form .prem-failnote { border-left: 2px solid var(--amber); padding-left: 0.7rem; color: var(--text); }
.prem-hero { display: flex; align-items: center; gap: 1.4rem; flex-wrap: wrap; background: linear-gradient(135deg, rgba(255, 106, 26,0.14), rgba(255, 106, 26,0.02)); border: 1px solid var(--line-amber); border-radius: var(--radius); padding: 1.3rem 1.5rem; margin: 0 0 1.2rem; }
.prem-hero-txt { flex: 1 1 320px; min-width: 0; }
.prem-h { margin: 0 0 0.4rem; font-size: 1.35rem; color: var(--amber-hi); letter-spacing: 0.01em; }
.prem-lead { margin: 0; color: var(--text); line-height: 1.55; font-size: 0.92rem; }
.prem-lead code { font-family: var(--mono); color: var(--amber); }
.prem-price { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; padding: 0.6rem 1.1rem; border: 1px solid var(--line-amber); border-radius: var(--radius); background: var(--panel); }
.prem-price-row { display: flex; align-items: baseline; gap: 0.3rem; }
.prem-price-amt { font-size: 2.1rem; font-weight: 800; color: var(--amber-hi); font-family: var(--mono); }
.prem-price-per { font-size: 0.85rem; color: var(--muted); }
.prem-price-extra { font-size: 0.72rem; color: var(--muted); white-space: nowrap; }
.prem-features { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem 1.3rem; margin-bottom: 1.2rem; }
.prem-list { list-style: none; margin: 0.3rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.prem-list li { position: relative; padding-left: 1.5rem; font-size: 0.9rem; color: var(--text); line-height: 1.45; }
.prem-list li::before { content: '✓'; position: absolute; left: 0; color: var(--online); font-weight: 700; }
.prem-list li.prem-soon { color: var(--muted); }
.prem-list li.prem-soon::before { content: '+'; color: var(--amber); }
.prem-note-min { margin: 0.9rem 0 0; font-size: 0.82rem; color: var(--muted); line-height: 1.5; }
.prem-status { display: flex; align-items: center; gap: 0.85rem; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.85rem 1.1rem; margin-bottom: 1.4rem; }
.prem-status-dot { width: 12px; height: 12px; border-radius: 50%; flex: none; background: var(--faint); }
.prem-status-dot[data-state="active"] { background: var(--online); box-shadow: 0 0 8px var(--online); }
.prem-status-dot[data-state="inactive"] { background: var(--standby); }
.prem-status-dot[data-state="error"] { background: var(--alarm); }
.prem-status-dot[data-state="unknown"] { background: var(--faint); }
.prem-status-txt { display: flex; flex-direction: column; gap: 0.12rem; min-width: 0; flex: 1 1 auto; }
.prem-status-line { font-weight: 700; font-size: 0.95rem; color: var(--text); }
.prem-status-sub { font-size: 0.8rem; color: var(--muted); }
.prem-manage { flex: none; display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.9rem; border: 1px solid var(--line-amber); border-radius: var(--radius); background: var(--panel-2); color: var(--amber-hi); font-weight: 600; font-size: 0.82rem; }
.prem-manage:hover { background: var(--panel); border-color: var(--amber); text-decoration: none; }
.prem-manage .ico { width: 1em; height: 1em; }
/* The status row is a dot, two lines of text and a "Manage subscription" button on one line, and on
   a phone those three do not fit: the button is `flex: none` at roughly 140px, the dot and gaps take
   another 40, and what is left is too narrow for the text to wrap INSIDE — so it was clipped by the
   card's `overflow: hidden` instead. Nothing that reads declarations can see it (`.prem-status-txt`
   already carries `min-width: 0`, which is the usual cause and was not this one); it was found by
   `check-panel-render`'s `premium · phone` view measuring the rendered span. The button takes its own
   line below the text rather than being shrunk into an ellipsis, because "Manage subscription" is the
   only way off this screen and it is longer in every other language. */
@media (max-width: 560px) {
  .prem-status { flex-wrap: wrap; }
  .prem-status-txt { flex: 1 1 100%; }
  .prem-manage { flex: 1 1 100%; justify-content: center; }
}
/* A translated status line can be one long word ("Předplatné", "Abonnement-Verwaltung"). Breaking
   inside a word is ugly and is still better than a sentence that leaves the card. */
.prem-status-line, .prem-status-sub { overflow-wrap: anywhere; }
.prem-form { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem 1.3rem 1.3rem; }
.prem-field { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.9rem; }
.prem-field label { font-size: 0.8rem; font-weight: 600; color: var(--text); }
.prem-field input { width: 100%; max-width: 620px; box-sizing: border-box; padding: 0.55rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); color: var(--text); font-family: var(--mono); font-size: 0.88rem; }
.prem-field input:focus { outline: none; border-color: var(--amber); box-shadow: var(--focus-ring); }
/* The shared .subhead carries a 1.5rem side margin; inside the premium cards that
   double-indents it — reset so it lines up with the card content. */
.prem-features .subhead, .prem-form .subhead { margin: 0 0 0.9rem; }
/* Kill the browser autofill's light background/text on the dark theme. */
.prem-field input:-webkit-autofill,
.prem-field input:-webkit-autofill:hover,
.prem-field input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--inset) inset;
  box-shadow: 0 0 0 1000px var(--inset) inset;
  caret-color: var(--text);
}
.prem-form-actions { display: flex; gap: 0.6rem; margin-top: 1.1rem; flex-wrap: wrap; }

/* ── Live Map ────────────────────────────────────────────────────────────── */
.map-updated { color: var(--muted); font-size: 0.82rem; align-self: center; }
.map-wrap {
  position: relative; display: flex; margin: 1.2rem;
  border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden;
}

/* Side panel — layer toggles, options + searchable entity lists (CFTools-style). */
.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;
}
/* The lists, on the other side of the map from the controls. */
.map-side-r { border-right: 0; border-left: 1px solid var(--line); }

/* Either side slides away. Width and flex-basis both go to zero — flex-basis alone leaves the
   panel's min-content width behind, which is how a "collapsed" panel keeps a 60px stub. */
.map-side.collapsed { width: 0; flex-basis: 0; min-width: 0; border-width: 0; overflow: hidden; }
.map-side.collapsed > * { display: none; }

/* The rail that brings it back: a full-height sliver, so the target is easy to hit without taking
   width from the map. */
.map-rail {
  flex: 0 0 14px; width: 14px; align-self: stretch; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-2); border: 0; border-right: 1px solid var(--line);
  color: var(--muted); font-size: 0.8rem; line-height: 1; cursor: pointer;
  --hov-bg: var(--inset); --hov-fg: var(--amber-hi); --hov-line: transparent;
}
.map-rail-r { border-right: 0; border-left: 1px solid var(--line); }
.map-rail span { display: block; transition: transform .15s ease; }
.map-rail.closed span { transform: rotate(180deg); }
/* The two find fields — "filter by player" on the left, "search name, sector" on the right — are
   ONE style. They were written apart and drifted into two looks for the same job: a boxed input on
   one side, a bare one on the other, at different sizes. Two treatments for one control read as two
   different kinds of control. Anything specific to either field belongs below this block, not in a
   second copy of it. The player one is a type-to-search combobox rather than a <select>: a full
   server has hundreds of names. */
.map-find { position: relative; display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--line); }
.map-find > .ico { width: 15px; height: 15px; color: var(--muted); flex: none; }
.map-find 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-find input:focus { outline: none; border-color: var(--amber); }
.map-find input::placeholder { color: var(--muted); }
/* Both fields clear the same way. The search one had no clear button at all, which is half the
   reason the pair looked unrelated — one row was visibly a field with controls, the other was not. */
.map-find > button[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 each suggestion solid amber. */
.map-owner-sug .mos-item,
.map-owner-sug .mos-item:hover {
  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(--panel-2); color: var(--amber-hi); }

/* 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-reset { margin-left: auto; }
/* .map-search-row keeps its name for the scripts that query it; its LOOK comes from .map-find. */

/* Collapsible "Layers & filters" dropdown — keeps the tall toggle list from crowding
   out the entity lists below. */
.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; }

/* Layer cards: a header (badge + name + count, toggles visibility) + segmented sub-filter. */
.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;
}
/* ── one place where a kind's badge colour is written ────────────────────────
   Three lists used to say this: the layer picker (.ml-badge), the card icon (.mp-ico) and the card's
   fallback glyph behind an image (.mp-ico-fb). They drifted, as three copies of one fact do — the
   seven live kinds were added to the picker and never to the card, so a puppet or an NPC with no
   artwork opened a card with no colour behind its glyph at all while the same thing in the filter
   list was properly coloured. Now the kind carries its colour as a token and every badge reads it. */
.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-turret   { --badge-bg: var(--c-turret); }
/* An item's condition sits between its name and its count, and must not squeeze either. */
/* `.mp-c-hp` held the bar at the END of a contents row. The condition is a dot and a figure beside
   the picture now, so the wrapper is gone rather than left behind styling an element nothing emits. */
.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); }
.mk-corpse   { --badge-bg: var(--lk-corpse); }
/* The per-KIND classes need the token too, and this is where the same bug was hiding one level
   further in. A live thing's card is styled by its KIND (`lk-puppet`), not by its layer (`mk-puppet`)
   — LIVE_KIND_STYLE against MAP_LAYERS — and every `lk-*` rule in this stylesheet is a DESCENDANT
   rule painting `.mk-dot`. A card icon has no dot inside it, so a puppet, an NPC or a sentry with no
   artwork opened a card with a transparent square while the same kind was properly coloured in the
   filter list. Same colours, from the same tokens, so the two can no longer disagree. */
.lk-player   { --badge-bg: var(--c-player); }
.lk-puppet   { --badge-bg: var(--lk-puppet); }
.lk-animal   { --badge-bg: var(--lk-animal); }
.lk-shark    { --badge-bg: var(--lk-shark); }
.lk-sentry   { --badge-bg: var(--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); }
.lk-boss2    { --badge-bg: var(--lk-boss2); }
.lk-vehicle  { --badge-bg: var(--c-vehicle); }
.lk-base     { --badge-bg: var(--c-base); }
/* No colour on the bare wrapper: .mp-ico.mp-img carries no kind class, so it stays transparent and
   the artwork sits on the panel rather than on a coloured square. */
.ml-badge, .mp-ico, .mp-ico-fb { background: var(--badge-bg, transparent); }
/* …and the list's no-artwork lead, which has to be written with BOTH classes: `.ml-thumb`
   paints its own `--panel-2` further down this file, and at equal specificity the later rule
   wins — so the single-class form left the plate panel-coloured with a `var(--bg)` glyph on
   it, at 1.14:1. That is the nineteen-invisible-icons defect this stylesheet already has a
   paragraph about, and `check-panel-render` caught it on the first render. */
.ml-thumb.ml-thumb-fb { background: var(--badge-bg, var(--line-amber)); }

.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); }







/* The live layers. Their marker colour is written as `.mk-puppet .mk-dot` — a DESCENDANT rule, and
   a badge has no dot inside it, so these seven had no background at all and showed the bare panel
   through a coloured-looking border. The picker is the one place a layer is nothing BUT its icon
   and its colour, so this is not cosmetic. Same values as the markers, from the same tokens. */







.map-layer .ml-name { flex: 1; }
.map-layer:hover { background: rgba(255, 255, 255, 0.06); 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); }
/* Hidden layer: dim the group, grey the badge, strike the name. */
.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); }

/* `nowrap` was right when this row held three toggles and two links in one panel. Split across two
   panels the contents differ per side, and a fixed row squeezed the exports into unreadable slivers
   beside a checkbox — so it wraps, and the export links take a line of their own where they read as
   the actions they are. */
.map-opts { display: flex; align-items: center; flex-wrap: wrap;
  gap: 0.5rem; padding: 0.4rem 0.7rem 0.6rem; border-bottom: 1px solid var(--line); }

/* The three view toggles, as three equal cells rather than three labels crammed edge to edge on one
   line — where the last one ran into the panel border and the row read as an afterthought under the
   search field rather than as a control. Equal columns also mean a longer word in another language
   cannot squeeze the other two. `:has(:checked)` lights the cell, so on/off is legible at a glance
   instead of resting on a 14 px native tick. */
/* Cells sized by their own word, not three equal thirds. Equal thirds fitted English and clipped
   German the moment "Sectors" became "Sektoren" — a layout that only works in the language it was
   designed in. They still fill the row: 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.3rem; min-width: 0;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  --hov-bg: var(--panel-2); --hov-fg: var(--text);
}
/* The native tick is gone from the cell but not from the document: it still takes the click, the
   keyboard focus and the checked state, and screen readers still read a checkbox. Only its 14 px of
   box is reclaimed — with it, three cells in a 280 px panel left about 55 px of text each and every
   label rendered as "Na…", "Sec…", "In vi…". The cell itself is now the tick. */
.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(255, 154, 60, 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:has(:focus-visible) { outline: 2px solid var(--amber); outline-offset: 1px; }

/* An empty layer keeps its row and loses its dropdowns: "All (0)" and "All types (0)" are two
   controls that cannot do anything, and with the bridge off there were five such layers spending
   three rows each. The row itself stays — "Sentries: 0" is information. */
.mlyr.empty .map-subf { display: none; }
.mlyr.empty .map-layer { opacity: 0.55; }
.map-toggle { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--muted);
  font-size: 0.78rem; line-height: 1; white-space: nowrap; flex: 0 0 auto; cursor: pointer; }
.map-toggle input { accent-color: var(--amber); width: 14px; height: 14px; margin: 0; flex: none; }
.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; }

.map-lists { flex: 0 0 auto; padding: 0 0 0.6rem; }
.ml-sec { margin-bottom: 0.3rem; }
/* Prominent, flush sticky header — full width, bottom rule, no top gap. */
.ml-sec-h {
  display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.6rem; position: sticky; top: 0; z-index: 2;
  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;
}
.ml-sec-h .ico { width: 13px; height: 13px; color: var(--amber-hi); }
/* The caveat beside a section title ("loaded areas"), which had no style of its own at all — so it
   inherited the header's uppercase, 700 weight and letter-spacing, came out as wide as the title it
   was qualifying, and wrapped onto a second line that pushed the count pill out of the row. It is an
   aside: lower case, lighter, quieter, and it never wraps. */
.ml-sec-h .ml-sec-note {
  flex: 0 0 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-transform: none; letter-spacing: 0; font-weight: 400; font-size: 0.64rem;
  color: var(--muted); cursor: help;
}
/* Section count — same pill as .map-count so all counters in the panel match. */
.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;
}
.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: rgba(255, 255, 255, 0.06); 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, and the same rule the field console's copy carries. Both lines wrap: a
   vehicle puts 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
   — `Онлайн · 💀 Мёртв по данным последнего сохранения` at 48% across 135 measured rows. The list
   scrolls vertically already, so a second line costs nothing that was not already there. */
.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.mk-player  { background: var(--c-player); }
.ml-dot.mk-base    { background: var(--c-base); }
.ml-dot.mk-vehicle { background: var(--c-vehicle); }
.ml-dot.mk-chest   { background: var(--c-chest); }
.ml-dot.mk-storage { background: var(--c-storage); }
.ml-dot.mk-trap    { background: var(--c-trap); }
.ml-dot.mk-turret  { background: var(--c-turret); }

/* Sub-filter = a compact dropdown at the bottom of each layer card (scales to any
   number of options, stays readable in the narrow panel — unlike a row of chips). */
.map-subf { padding: 0.4rem 0.5rem 0.5rem; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 0.35rem; }
.sf-select, .sf-type-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='%23ff9a3c' stroke-width='2.5'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 0.55rem center;
}
.sf-select:hover, .sf-select:focus, .sf-type-select:hover, .sf-type-select:focus { border-color: var(--line-amber); outline: none; }
.sf-select option, .sf-type-select option { background: var(--panel); color: var(--text); }
.map-mk.mk-player:not(.off) .mk-dot { box-shadow: 0 0 0 2px rgba(53, 208, 127, 0.4), 0 1px 3px rgba(0, 0, 0, 0.6); }
.map-mk.mk-player.off .mk-dot { background: var(--offline); }
/* Online but dead (awaiting respawn): red ring so the state reads at a glance. */
.map-mk.mk-player.dead:not(.off) .mk-dot { background: var(--alarm); box-shadow: 0 0 0 2px rgba(237, 66, 69, 0.45), 0 1px 3px rgba(0, 0, 0, 0.6); }
.map-mk.mk-player.off { opacity: 0.72; }
.ml-row.off .ml-dot { background: var(--offline); }
.ml-row.off .mlr-name { color: var(--muted); }

/* 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.20); }
/* ⚠ **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))); }
.sec-lab {
  position: absolute; transform: scale(var(--mz, 1)); transform-origin: top left;
  /* 700, not 800: Chakra Petch tops out at Bold, so 800 named a face that does not exist in the
     family at all and resolved to this same 700. Declaring the weight we actually get. */
  padding: 3px 6px; color: #fff; font-family: var(--ui); font-weight: 700; font-size: 22px;
  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.75rem; }
  .map-side { width: auto; flex: none; height: auto; max-height: 46vh; border-right: none; border-bottom: 1px solid var(--line); }
  .map-side-r { border-left: 0; border-top: 1px solid var(--line); border-bottom: 0; }
  /* Stacked, a collapsed panel has to lose its HEIGHT — zero width means nothing in a column. */
  .map-side.collapsed { max-height: 0; height: 0; width: auto; flex-basis: auto; }
  /* …and a vertical sliver between stacked panels would be unhittable, so the rail lies flat. */
  .map-rail { flex: 0 0 16px; width: auto; height: 16px; align-self: auto; border: 0; border-bottom: 1px solid var(--line); }
  .map-rail-r { border-bottom: 0; border-top: 1px solid var(--line); }
  .map-rail span { transform: rotate(90deg); }
  .map-rail.closed span { transform: rotate(-90deg); }
  .map-viewport { height: 58vh; }
}
.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); }
/* No will-change/GPU layer here: it would cache one rasterisation and upscale it,
   blurring the markers/labels (and the map) on zoom. Without it the browser
   re-rasterises at the current zoom, keeping icons + text crisp. */
/* 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 every marker resolved to the same line and then flew
   apart to its real position when the image landed. On a cold connection, or the 3.8 MB flat image
   on a host with no pyramid, that is seconds of a map that is visibly wrong and then jumps.
   `aspect-ratio` is not a guess about the artwork: the world span is identical on both axes
   (1,524,000 cm), `sectorOf` cuts the box into a 5x5 grid of 20% cells in BOTH directions, and the
   pyramid's z0 is one square tile — every placement on this map 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-img.map-img-missing {
  min-height: 640px;
  background:
    repeating-linear-gradient(0deg, rgba(255, 106, 26,0.05) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(90deg, rgba(255, 106, 26,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; }

.map-area {
  position: absolute; border: 1px solid; border-radius: 3px;
}
.map-area.area-player { border-color: rgba(255, 154, 60, 0.55); background: rgba(255, 106, 26, 0.10); }
.map-area.area-npc { border-color: rgba(120, 170, 255, 0.45); background: rgba(90, 140, 240, 0.08); }

/* The dot is anchored exactly on the point; the label floats below (out of flow) so
   it never shifts the dot, and ::before widens the click target. */
/* Zero-size anchor placed EXACTLY at the point (its top-left = X%,Y%); the dot and
   label position + counter-scale themselves against that anchor. No box-model math. */
.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 sprite is drawn for a 1.05em icon; at 12px a 1.6 stroke is under a pixel and detail turns
     to mush. Heavier here only, so a puppet or a sentry reads as clearly as a truck does. */
  stroke-width: 2.4;
}
.map-mk .mk-label {
  /* The vertical offset is counter-scaled too (calc · --mz), so the gap between the
     dot and the name stays constant on screen at every zoom (was growing with zoom).
     `--mk-dx` and `--mk-dy` are NUMBERS OF SCREEN PIXELS written by `mapDeclutterLabels()` to push
     a name that the map's own edge would otherwise cut back inside it. They ride in a translate
     placed BEFORE the scale, so they land in the marker's coordinate space, and they are multiplied
     by --mz for the same reason the 13px is: everything in here is scaled by the canvas and has to
     be undone.

     ⚠ There was only `--mk-dx` for as long as this rule has existed, and "a name the edge would cut
     is pushed back inside" was therefore half a rule for four edges: a marker near the TOP or the
     BOTTOM of the viewport had its name cut and nothing moved it. The fixture never put one there,
     so every gate was green about it. */
  display: none; position: absolute; left: 0; top: 0;
  transform: translate(-50%, calc(13px * var(--mz, 1)))
             translate(calc(var(--mk-dx, 0) * var(--mz, 1) * 1px),
                       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);
  /* OPAQUE, and it used to be rgba(…, 0.85). A translucent plate does not hide the label under it,
     it BLENDS with it: an admin reading C2 was shown "Gen Saloon re" — the trader "General Store"
     with "Saloon" laid across its middle and the 15% that came through reading as a third word.
     Decluttering is what stops labels stacking at all; this is what stops the failure being
     unreadable when something does end up over something else (a dot, a zone, a base rectangle). */
  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; }
/* Decluttered away: a name that could not be read where it landed, because another one is already
   there. It keeps its box — `visibility` rather than `display`, so the next pass can still measure
   where it WANTS to be — and it comes back the moment the pointer or the selection asks for it,
   which is why those two rules sit here and outrank it on specificity. Nothing is lost: the dot is
   untouched and the side list names every marker on the map. */
.mk-label.mk-lab-off { visibility: hidden; }
/* A ZONE'S PILL IS SUPPRESSED DIFFERENTLY, AND THE DIFFERENCE IS THE CARD.
   `visibility: hidden` is not clickable, and this pill is the zone's only pointer target -- the
   zone body takes none on purpose. Hiding it the marker way would take the zone's card away with
   its name. So the box stays and everything that draws goes transparent; `mapDeclutterLabels`
   still measures it, because it still has a rectangle. */
.zn-name.zn-lab-off { color: transparent; background: transparent; border-color: transparent; }
.map-mk:hover .mk-label, .map-mk.mk-selected .mk-label { visibility: visible; }
.map-mk:hover { z-index: 5; }
.map-mk:hover .mk-dot { outline: 2px solid var(--amber-hi); }

.mk-player .mk-dot { background: var(--c-player); }
.mk-base   .mk-dot { background: var(--c-base); }
.mk-vehicle .mk-dot { background: var(--c-vehicle); }
.mk-chest  .mk-dot { background: var(--c-chest); }
.mk-storage .mk-dot { background: var(--c-storage); }
.mk-trap   .mk-dot { background: var(--c-trap); }
.mk-turret .mk-dot { background: var(--c-turret); }
/* A POI is a place, not a thing — a hollow marker, so it reads as scenery beside the entities rather
   than competing with them. It is never "live" and never fades: it is there whether the game is
   running or not, which is the whole point of moving it out of the live layers. */
/* Popover header icon fallback tints for the new layers */



/* Info card — anchored next to the clicked marker (position set in JS). */
.map-popover {
  /* The card's edge, as ONE pair of numbers. Everything that has to line up with the card's inner
     edge — the close button, the header's divider, the rows, the buttons — is derived from these
     rather than each carrying its own guess. The close button used to sit at a raw `top: 5px;
     right: 7px` while the content was inset 21px, so the one control in the corner was visibly
     tighter to the border than anything else in the card. */
  --mp-pad-x: 1.1rem;
  --mp-pad-y: 0.95rem;
  /* The close button's BOX and its INK. They are different on purpose: the hit area is 2rem here
     and 44px under `(max-width: 760px)` — the accessibility floor, which this card must keep — but
     what a reader aligns against is the glyph, not the invisible box around it. So the box is
     placed by the ink: it overhangs the padding by half the difference, the ✕ lands exactly on the
     card's inner edge, and the fingertip target stays whatever size it needs to be. */
  --mp-close-box: 2rem;
  --mp-close-ink: 1.05rem;
  /* Wider than it was. Every card carries artwork, a title, a status row and up to a dozen detail
     rows, and at 330px the names of things were clipped mid-word. */
  position: absolute; z-index: 20; width: 380px; max-width: calc(100% - 24px);
  padding: var(--mp-pad-y) var(--mp-pad-x);
  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 card with an open parts list and
     an open contents list runs well past the bottom of the viewport, and what fell off the end was
     the buttons — the things you opened the card to press. */
  display: flex; flex-direction: column; max-height: calc(100% - 16px);
}
/* Header fixed, everything below it scrolls. The header is also the drag handle, which is the other
   reason it sits outside the scrolling part. */
.map-popover .mp-body {
  /* `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 grew past its own max-height and the
     overflow rule never came into play. The card looked capped and simply was not. */
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain; margin: 0 -0.35rem; padding: 0 0.35rem;
  /* The body owns the spacing between the things in it. Each button used to carry its own
     margin-top — 0.5, 0.6 and 0.65rem — so three buttons in a row sat at three different distances
     and the gaps added up into a gulf. One gap, applied once, is also one number to change. */
  display: flex; flex-direction: column; gap: 0.45rem;
}
/* …except a content box, which belongs TO the button above it and should read as attached to it. */
.map-popover .mp-body > .mp-contents { margin-top: -0.3rem; }
/* The rows are the card's own detail and sit tighter to the header than the actions do. */
.map-popover .mp-body > .mp-rows { margin-bottom: 0.15rem; }
/* Declared, not inherited. `.mp-rows` is a name the in-game setup screen also uses, and while that
   screen's rules are now scoped, saying what this box IS costs one line and closes the hole for
   good: whatever else in the stylesheet claims the name, this stays a plain block whose children
   are separated by their own hairline and nothing else. */
.map-popover .mp-rows { display: block; }
.map-popover .mp-head { flex: none; cursor: grab; user-select: none; }
.map-popover.dragging { cursor: grabbing; }
.map-popover.dragging .mp-head { cursor: grabbing; }
/* Dragged away from its marker, it is no longer anchored to anything — say so quietly. */
.map-popover.moved { border-color: var(--line); }
/* ⚠ **THIS CARD IS DELIBERATELY NOT WIDENED ON A PHONE, AND THE FIELD CONSOLE'S COPY IS.** The
   two were made to match and the measurement said no.

   The starting point is shared: `max-width: calc(100% - 24px)` is a percentage of the card's own
   OFFSET PARENT rather than of the screen, so on a phone the card is far narrower than the page and
   the names inside it are cut. On the public map that was severe — 165px wide at a true 310px
   viewport, with `7.62x39mm Round` showing 28% of itself — and `min()` against the viewport is
   what fixes it there.

   Here it was mild (74%), and widening it cost something worse. At `calc(100vw - 20px)` and again
   at `- 40px`, `.mp-owner`'s touch slop came within 8px of the map rail's own button with 12px of
   reach each: a reader aiming at the rail gets the owner instead, and a wrong action is not a miss
   and cannot be corrected by trying again. Narrowing did not help, because the overlap is VERTICAL,
   and capping the height did not move it either, because the card is not at its cap.

   So the width stays as it is and only the height is capped further, which keeps the card's bottom
   clear of the rail whatever wraps inside it. This panel has a rail at that edge and the public map
   does not; the two surfaces differ because the two screens do, and both numbers were driven. */
@media (max-width: 560px) {
  .map-popover { max-height: calc(100% - 72px); }
}
.map-popover, .map-popover .mp-body { scrollbar-width: thin; scrollbar-color: rgba(255, 106, 26, 0.4) transparent; }

/* Right-click context menu — "Copy teleport" for the clicked spot. */
.map-ctx {
  position: absolute; z-index: 25; padding: 0.25rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
}
.map-ctx-item {
  display: flex; align-items: center; gap: 0.45rem; width: 100%; white-space: nowrap;
  background: none; border: none; color: var(--text); font-size: 0.82rem; font-weight: 600;
  padding: 0.45rem 0.6rem; border-radius: var(--radius); letter-spacing: 0;
}
.map-ctx-item:hover { background: var(--hover-bg); border: none; color: var(--amber-hi); }
.map-ctx-item .ico { color: var(--amber); }
.map-ctx-item.copied, .map-ctx-item.copied:hover { color: var(--online); background: none; }
.map-ctx-xy { color: var(--muted); font-family: var(--mono); font-size: 0.75rem; font-weight: 400; }
/* Placed by its INK, not by its box — see `--mp-close-ink` on `.map-popover`. The offset is half
   the difference between the two, so a 2rem button and a 44px one both land the ✕ on the card's
   inner edge and neither of them needs a number of its own. */
.mp-close {
  position: absolute;
  top: calc(var(--mp-pad-y) - (var(--mp-close-box) - var(--mp-close-ink)) / 2);
  right: calc(var(--mp-pad-x) - (var(--mp-close-box) - var(--mp-close-ink)) / 2);
  width: var(--mp-close-box); height: var(--mp-close-box);
  display: flex; align-items: center; justify-content: center;
  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; }
/* The artwork is the thing being looked at, so it gets room: a 26px chip could not show what a
   vehicle or a crate actually was. Aligned to the TOP, because the text beside it is now two or
   three lines and centring made the plate drift down the card.
   FULL content width, and a hairline under it. It used to stop 1.4rem short so the title would
   clear the close button, which put a 27px step in the card's right edge — the header ended in one
   place and every row below it in another. The clearance now lives on the text column alone (see
   `.mp-headtxt`), so the divider runs the whole width and the header lines up with the rows. */
.mp-head {
  display: flex; align-items: flex-start; gap: 0.7rem;
  margin: 0 0 0.6rem; padding-bottom: 0.6rem; border-bottom: 1px solid var(--line-2);
}
/* One plate size for every card. A photographed thing and an unphotographed one opened at two
   different header heights, so the card visibly changed shape depending on what you clicked and the
   title and rows moved with it. The artwork fills the square when there is artwork; when there is
   not, the layer's own colour and glyph fill the same square. */
/* 72px, and the size is not a taste call. The creature portraits are 512x512 renders whose subject
   fills 92% of the frame, and `scum-exporter/build_renders.py` states what they are drawn FOR:
   telling one record from another at 48-96px. Minus the 1px border and the 4px inset, a 72px plate
   gives 62px of artwork, which is inside that band; the 58px this rule declared gave 48px and the
   30px it was actually rendering at gave 20px, which is a speck.
   The cost is real and bounded: the card is `max-height: calc(100% - 16px)` with `.mp-body`
   scrolling inside it, so every pixel here is a pixel of visible rows. 14px of scroll for artwork
   nobody could read is the trade. SQUARE, and one size for every kind — a photographed thing and an
   unphotographed one opening at two different header heights is what this rule was written to stop,
   and a taller plate for the tall portraits would bring it straight back. */
.mp-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 72px; height: 72px; border-radius: var(--radius-md); flex: none;
  border: 1px solid rgba(0, 0, 0, 0.4);
}
/* `color: var(--bg)` is a CUT-OUT and must stay one: the plate is filled with the layer's own
   colour by `--badge-bg`, and the glyph is punched out of it. On a parent with no layer colour it
   is background on background — the nineteen invisible icons `check-panel-render` was written to
   find. Anything reusing this plate off the map needs its own class, as `.mps-ico` has. */
.mp-ico .ico { width: 34px; height: 34px; color: var(--bg); }

/* The SETUP screen's card icon. Its own class, and the reason is worth keeping: these cards used to
   reuse `.mp-ico` above, whose glyph is `color: var(--bg)` because on the map it sits inside a
   square filled with a layer colour. A preset card has no layer, `--badge-bg` falls back to
   transparent, and the result was the page's background colour drawn on the page's background —
   nineteen icons, none of them visible, on the one screen whose whole job is to make forty-one
   systems approachable.
   Smaller than the map badge on purpose: it sits beside a title in a text card, not above artwork. */
.mps-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--radius-md); flex: none;
  background: var(--inset);
  border: 1px solid var(--line-2);
}
.mps-ico .ico { width: 18px; height: 18px; color: var(--amber-hi); }





/* The text column takes the rest of the header and keeps the close button's clearance to ITSELF.
   `(box + ink) / 2` is exactly the button's left edge measured back from the card's inner edge —
   half of it overhangs the padding by construction — so the title cannot run under the ✕ at either
   button size, and the header still ends where the rows do. `min-width: 0` is what lets a long name
   shrink instead of pushing the plate off the card. */
.mp-headtxt {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 0.15rem; padding-top: 0.1rem;
  padding-right: calc((var(--mp-close-box) + var(--mp-close-ink)) / 2 + 0.3rem);
}
/* Two lines rather than an ellipsis: "Kinglet Duster (Squad Bravo)" is a name worth reading, and
   clipping it to one line to keep a fixed height told the reader nothing.
   ⚠ **AND THE CLAMP IS GONE FOR THE SAME REASON, ONE STEP FURTHER.** At a narrow card two lines is
   not enough either — measured, a title needing a third had it hidden with no ellipsis to say so,
   because a clamp that runs out simply stops. The fixed height it was keeping is not a promise
   anything makes: the chips row above it already varies, and the BODY below scrolls, so a taller
   header costs the card nothing. The field console's copy of this card is unbounded too. */
.mp-title {
  color: var(--text); font-weight: 700; font-size: 1rem; line-height: 1.25; overflow-wrap: anywhere;
}
.mp-sub { font-size: 0.76rem; color: var(--muted); line-height: 1.35; }
/* Status markers get a row of their own instead of being concatenated onto the end of the subtitle —
   see the comment where they are built. Same shape on every kind of thing, and they wrap. */
.mp-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.12rem; }
.mp-chip {
  display: inline-flex; align-items: center; gap: 0.22rem;
  padding: 0.08rem 0.42rem; border-radius: var(--pill);
  background: var(--inset); border: 1px solid var(--line-2);
  font-size: 0.7rem; font-weight: 600; line-height: 1.5; white-space: nowrap;
}
.mp-on { color: var(--c-player); font-weight: 600; }
.mp-off { color: var(--offline); font-weight: 600; }
/* A label on the left and its value on the right, on ONE line, with one hairline between
   neighbours. Every property here is declared rather than left to whatever else in the stylesheet
   claims the name `.mp-row`:
     · `flex-wrap: nowrap` — the value never drops to its own line. It used to, left-aligned under
       its own label, which read as two broken rows rather than one; a value too long to share the
       line wraps INSIDE its own column instead, still right-aligned against the card's edge.
     · `border: 0` before `border-top` — three of the four edges were being drawn by a rule
       belonging to a different screen, so every row was a box.
     · `align-items: center` — the condition bars and the risk mark sit on the label's line rather
       than hanging above it.
   The padding is the row's own rhythm and the only thing separating one from the next; there is no
   gap, deliberately, because the hairline is the separator and a gap around it makes the line float
   in the middle of nothing. */
.map-popover .mp-row {
  display: flex; flex-wrap: nowrap; align-items: center; justify-content: space-between;
  gap: 0.75rem; font-size: 0.82rem; padding: 0.42rem 0;
  border: 0; border-top: 1px solid var(--line-2);
}
.map-popover .mp-rows .mp-row:first-child { border-top: none; padding-top: 0.1rem; }
/* The label holds its width; the value is what gives way. Shrinking the label first turns
   "Last accessed" into two words on two lines while the value beside it sits on one. */
.map-popover .mp-row > span:first-child { color: var(--muted); flex: 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;
  /* Both halves of "a long value wraps in its own column": `min-width: 0` lets the flex item
     shrink below its content at all, and `overflow-wrap: anywhere` gives a 17-digit Steam ID or a
     bracketed timestamp somewhere to break. Without the first the value simply overflows the card
     at 320px; without the second it shrinks and then overflows anyway, because the browser has no
     break opportunity to use. */
  min-width: 0; overflow-wrap: anywhere;
}
/* The condition bar is a FIXED 5.5rem everywhere else in the panel, and in a value column that has
   to shrink that is a box which cannot. With the row no longer able to wrap the bar out of the way
   (which is what used to happen, by accident), 5.5rem + the percentage no longer fitted the ~90px a
   320px card leaves for a value, and the card's body — whose `overflow-x` is `auto` only because
   setting `overflow-y` implies it — started scrolling SIDEWAYS. A card you have to scroll left and
   right to read a percentage is worse than the wrapping it replaced.
   A percentage is the wrong tool here and was tried first: `min(5.5rem, calc(100% - 3.2rem))`
   resolves against a parent whose own width the browser worked out FROM this bar, so the bar came
   out around 45px on a 1920px desktop where 109px fits comfortably — right at the size that
   matters, wrong everywhere else. A width that depends on the viewport and not on its own parent
   has no such loop. `check-panel-render` fails on sideways scroll inside the card, so if this ever
   stops being enough the run says so instead of an owner. */
@media (max-width: 640px) {
  /* 2.5rem, not 3: at 3rem the bar, its 0.4rem margin and "85%" came to exactly the 84px the
     longest label on the card ("Average condition") leaves beside itself, and the percentage
     dropped under the bar. The bar is a proportion — it reads fine at 42px — and the number beside
     it is the part somebody is actually reading. */
  .map-popover .mp-row .hpb { width: 2.5rem; }
}
/* A sentence about the card rather than a value in it — full width, left aligned, quiet. */
/* A row whose value needs the whole width: label on its own line, content under it. Used where a
   value is a group of chips or a bar set that cannot share a line with its label at 380px. */
.map-popover .mp-row.mp-stack { display: block; }
.map-popover .mp-row.mp-stack > span:first-child { display: block; margin-bottom: 0.28rem; }
/* `.map-popover .mp-row.mp-stack .mp-bands { display: flex; flex-wrap: wrap }` used to sit here, and
   it was the SAME collision as `.mp-row` above, one level down. `.mp-bands` further down this file
   says in its own comment that the three damage bands must be a grid because "a grid cannot wrap" —
   that rule was written to fix exactly the wrapping this one caused, and it never took effect: bare
   `.mp-bands` is 0-1-0 and this was 0-3-0, so the layout the fix replaced went on shipping. Removed
   rather than corrected, because `.mp-bands` is already right and one rule for one thing is the
   point. Found by asking which class names in this stylesheet carry BOTH a bare rule and a scoped
   rule that each declare `display` — there are ten more, and none of them has been looked at. */
.map-popover .mp-note {
  border-top: 1px solid var(--line-2); padding: 0.4rem 0 0.2rem;
  color: var(--muted); font-size: 0.78rem; line-height: 1.45;
}
/* The provenance line under the four vitals bars: a footnote about the READING, not a warning
   about the player, so it is quieter than an ordinary note and keeps the separator off — the bars
   above it are one block with it. */
.map-popover .mp-vitals-src { border-top: 0; padding-top: 0.15rem; font-size: 0.72rem; color: var(--faint); }
/* ── THE GAUGE BLOCK ──────────────────────────────────────────────────────────────────────────
   Health / stamina / water / food on a player, fuel and battery on a vehicle. A GRID for the same
   reason `.mp-bands` is one: the question a reader asks of four bars is comparative — which of
   these is the one in trouble — and that is only answerable if the bars line up in a column. Four
   `label — bar — %` ROWS is what this card did before, and it cost four of its twelve lines while
   presenting one reading of one body as four separate measurements.

   `minmax(min(100%, …), 1fr)` rather than a bare length, which is this panel's standing rule: a
   bare minimum inside a card that is `overflow: hidden` clips its own right edge rather than
   scrolling, and at 320px the card has about 270px to work with. Two columns where they fit, one
   where they do not. */
.map-popover .mp-gauges {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr)); gap: 0.3rem 0.6rem;
}
/* ── A HAIRLINE UNDER THE BLOCK'S HEADING, WHICH IS HOW THE GAME RULES ITS OWN PANELS ──────────
   The owner would like the map to read more like SCUM's own UI, and the six zone-editor
   screenshots in `moding_ressource/ingame/` are the reference this tree already names for that
   screen: a heading is large and left-aligned with a rule under it spanning the panel, values sit
   in dark inset boxes, and separators go between GROUPS rather than between every row.
   Two of those three were already true here — a `.mp-gauge` IS an inset box, and this map's rows
   carry no separators — so the rule is the whole of what was missing, and it lands on exactly the
   place the game puts one: under the heading of a group of values.

   ⚠ It is the LABEL that is ruled, not the block, and the difference matters: a border on
   `.mp-gauges` would sit between the heading and the cells only until a card had two blocks, at
   which point the lower one's rule reads as a separator belonging to the block above it. And it is
   `--line-2`, a token, never a colour sampled off a screenshot — the borders on the cells below it
   are the same token, so the two cannot drift, and a literal here would be invisible to every rule
   in this tree that resolves theme tokens. No background and no colour is touched, which is what
   keeps `check-contrast` out of it: this adds furniture, it does not retune a pair.

   ⚠ And it is scoped by a class the block ITSELF carries rather than by `.mp-stack`, which is the
   shape of several other things on this card — a row of state chips, three damage bands, a garden's
   slot tallies, and one heading with nothing under it at all. A rule under that last one would be a
   line drawn across nothing. `:has(.mp-gauges)` would have said the same thing and left the rule
   answerable only in a browser; the class is answerable from the call site. */
.map-popover .mp-gauge-row > span:first-child {
  border-bottom: 1px solid var(--line-2); padding-bottom: 0.22rem;
}
.map-popover .mp-gauge {
  display: flex; align-items: center; gap: 0.35rem; min-width: 0;
  background: var(--inset); border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 0.22rem 0.4rem;
}
/* The label gives way before the bar does: a bar squeezed to nothing is a cell that says nothing,
   while a clipped word is still readable from its first letters and from its position. */
.map-popover .mp-gauge i {
  font-style: normal; font-size: 0.68rem; color: var(--muted); flex: 1 1 auto;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.map-popover .mp-gauge .hpb { flex: none; width: 2.6rem; }
.map-popover .mp-gauge .hpb-n { flex: none; font-size: 0.72rem; min-width: 2.4em; text-align: right; }
/* ⚠ A vehicle's FRAME is a cell whose bar arrives from a fetch, so its value sits inside a wrapper
   (`.mp-chassis`, which `mapFillChassis` finds by id and replaces the inside of). `display:
   contents` is what keeps that wrapper out of the layout: without it the bar and its percentage
   are one flex item rather than two, the `flex: none` above lands on the wrapper instead of on the
   bar, and the cell's arithmetic — label gives way, bar does not — is being done about the wrong
   box. The wrapper is a HANDLE for the fill, never a layout step. */
.map-popover .mp-gauge .mp-chassis { display: contents; }
/* …and what a cell holds when there is no bar to draw: the chassis answered "no chassis fitted",
   "not known yet", or a failure. A sentence in a 136px cell has to give way exactly as the label
   does, or it takes the width the bar would have had and the cell stops lining up with the ones
   beside it. */
.map-popover .mp-gauge .dim, .map-popover .mp-gauge .mp-bad {
  flex: 1 1 auto; min-width: 0; font-size: 0.72rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right;
}
/* ── THE STATE CHIPS ──────────────────────────────────────────────────────────────────────────
   Twelve booleans as twelve label/value rows would have tripled this card; as chips they cost one
   line for as many as are true. `.mp-chip` already carries the shape — this only says how they sit
   together and which of the two kinds each is. */
.map-popover .mp-flags { display: flex; flex-wrap: wrap; gap: 0.3rem; }
/* Body state: something the person is doing. Quiet — it is information, not a warning. */
.map-popover .mp-chip.mp-flag { color: var(--text); }
/* Admin state: something SOMEBODY WITH THIS PANEL DID and may have forgotten to undo. It is the
   single most useful thing a map can say about a person, so it is the one chip here that is loud.
   Both halves are stated on the element, which is this stylesheet's rule: a rule that changes only
   the background or only the colour of a paired state is how an unreadable one gets shipped. */
.map-popover .mp-chip.mp-flag-adm {
  color: var(--bg); background: var(--amber-hi); border-color: var(--amber-hi); font-weight: 700;
}
.map-popover .dim { color: var(--muted); }
.mp-id { font-family: var(--mono); font-size: 0.78rem; user-select: all; cursor: text; }
/* Player card → Property: collapsible groups, one row per owned thing, the whole row opens it
   on the live map.

   Everything below fights ONE thing: the global `button:hover` at the top of this file paints
   every button solid amber. These are buttons for keyboard/AT reasons, not visually — so each
   state is stated explicitly and at a specificity that beats it, or the amber block comes back
   and the grey sub-text on it becomes unreadable. */
.pd-stuff-head {
  display: flex; align-items: center; gap: .45rem; width: 100%; margin: .8rem 0 .1rem;
  padding: .35rem .1rem; text-align: left; cursor: pointer; font: inherit; letter-spacing: .04em;
  background: none; border: 0; border-bottom: 1px solid var(--line-2); color: var(--text); border-radius: 0;
}
.pd-stuff-head:hover, .pd-stuff-head:focus-visible { background: none; border-color: var(--line-2); color: var(--amber-hi); }
.pd-stuff-head:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
/* Drawn, not from the sprite — there is no chevron in it, and inventing one would mean an
   icon that renders as an empty box everywhere it is reused. */
.pd-stuff-head .pd-caret {
  margin-left: auto; width: 0; height: 0; transition: transform .15s, border-top-color .14s;
  border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid var(--muted);
}
.pd-stuff-head[aria-expanded="false"] .pd-caret { transform: rotate(-90deg); }
.pd-stuff-head:hover .pd-caret { border-top-color: var(--amber-hi); }
.pd-stuff-n { padding: 0 .4rem; border-radius: 999px; background: var(--inset); color: var(--muted); font-size: .72rem; font-weight: 400; }
.pd-stuff-list { display: flex; flex-direction: column; gap: .3rem; margin: .45rem 0 .3rem; }
.pd-stuff-list[hidden] { display: none; }
.pd-stuff {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .15rem .6rem;
  width: 100%; padding: .5rem .65rem; text-align: left; cursor: pointer; font: inherit; letter-spacing: 0;
  background: var(--panel-2); border: 1px solid var(--line-2); border-radius: var(--radius); color: var(--text);
}
/* Tinted, not filled: the row keeps a dark ground so both lines stay legible on hover. */
.pd-stuff:hover:not([disabled]), .pd-stuff:focus-visible:not([disabled]) {
  background: rgba(255, 106, 26, 0.10); border-color: var(--amber); color: var(--text);
}
.pd-stuff:focus-visible { outline: none; }
.pd-stuff[disabled] { cursor: default; opacity: .55; background: var(--panel-2); border-color: var(--line-2); color: var(--text); }
.pd-stuff-img {
  grid-column: 1; grid-row: 1 / span 2; display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--radius); background: var(--inset); overflow: hidden;
}
.pd-stuff-img img { max-width: 100%; max-height: 100%; object-fit: contain; }
.pd-stuff-img-fb { color: var(--muted); }
.pd-stuff-main { grid-column: 2; font-weight: 600; }
.pd-stuff-sub { grid-column: 2; font-size: .76rem; color: var(--muted); font-weight: 400; }
.pd-stuff:hover:not([disabled]) .pd-stuff-sub { color: var(--text); opacity: .75; }
.pd-stuff-go { grid-column: 3; grid-row: 1 / span 2; color: var(--muted); }
.pd-stuff:hover:not([disabled]) .pd-stuff-go { color: var(--amber-hi); }

/* Owner name inside a popup row. It sits in running text, so it must NOT look or behave like a
   button — the global button skin is cancelled in every state and only the colour reacts. */
.map-popover .mp-owner,
.map-popover .mp-owner:hover,
.map-popover .mp-owner:active {
  display: inline; padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none;
  transform: none; font: inherit; letter-spacing: 0;
}
.map-popover .mp-owner { color: var(--text); cursor: pointer; border-bottom: 1px dotted var(--muted); }
.map-popover .mp-owner:hover { color: var(--amber-hi); border-bottom: 1px solid var(--amber-hi); }
.map-popover .mp-owner:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
/* The card's stack of full-width buttons, as ONE shape. They were three near-identical copies that
   agreed on everything except font size — 0.82rem on "Intel & details" against 0.8rem on the other
   three — so a card with four of them drew four buttons at three different heights (39, 40 and
   42px), which is the kind of raggedness nobody can name and everybody sees. What actually differs
   between them is the border and the text colour, and only that is stated per button below. */
.mp-card-btn, .mp-tp, .mp-contents-btn, .mp-parts-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.4rem; width: 100%;
  padding: 0.45rem 0.6rem; 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;
}
/* The one that goes somewhere else rather than opening something here — the only amber-edged
   button in the stack, because it is the only one that leaves the card. */
.mp-card-btn { border-color: var(--line-amber); color: var(--amber-hi); }
.mp-card-btn:hover { background: var(--amber-hi); color: var(--on-amber); }
.mp-card-btn .ico { width: 14px; height: 14px; }
.mp-tp:hover { border-color: var(--line-amber); color: var(--amber-hi); background: var(--panel-2); }
.mp-tp.copied { border-color: var(--c-player); color: var(--c-player); }
.mp-tp .ico { width: 14px; height: 14px; }
/* Container contents (chest/vehicle) */
.mp-contents-btn:hover, .mp-parts-btn:hover { border-color: var(--line-amber); color: var(--amber-hi); background: var(--panel-2); }
.mp-contents-btn.open, .mp-parts-btn.open { color: var(--amber-hi); border-color: var(--line-amber); }
.mp-contents-btn .ico, .mp-parts-btn .ico { width: 14px; height: 14px; }
/* No height cap and no scrollbar of its own. It used to stop at 240px and scroll internally, which
   meant the CARD almost never reached its own limit — so the only scrollbar anyone ever saw was the
   inner one, and the card itself felt stuck. Opening two panels gave you two of them. Now these grow
   to their content and the card's body is the single thing that scrolls. */
.mp-contents { margin-top: 0.5rem; border-top: 1px solid var(--line); }
.mp-contents[hidden] { display: none; }
.mp-c-head {
  /* Same horizontal inset as the rows below it (0.1rem), or the heading sits visibly off from the
     column it heads. Opaque and above them, so nothing shows through while scrolling. */
  display: flex; align-items: center; gap: 0.4rem; padding: 0.45rem 0.1rem; position: sticky; top: 0; z-index: 2;
  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; }
.mp-c-ph { width: 13px; height: 13px; color: var(--muted); opacity: 0.7; }
/* ⚠ **A MARKER CARD LISTS WHAT IS INSIDE SOMETHING, AND THE NAMES WERE THE PART IT CUT.**
   Measured across 274 rows on a phone: `7.62x39mm Round` came back with 28% of it on screen in
   33px, beside a picture of the thing it would not name. The row already has a variable height
   (the count and the record button are taller than one line of this), so wrapping costs nothing
   and an item name has spaces to break at. */
.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; }
/* Vehicle-parts group header (kind: body / wheel / armor / …). */
.mp-c-grp { display: flex; align-items: center; gap: 0.35rem; margin: 0.35rem 0 0.1rem; padding: 0.1rem 0.1rem;
  color: var(--amber-hi); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; }
.mp-c-grp:first-of-type { margin-top: 0.1rem; }
.mp-c-gn { color: var(--muted); font-weight: 600; font-variant-numeric: tabular-nums; }

/* Slim, themed scrollbars for the map's scroll areas. */
.map-side, .mp-contents { scrollbar-width: thin; scrollbar-color: rgba(255, 106, 26, 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(255, 106, 26, 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(255, 106, 26, 0.6); background-clip: content-box;
}
/* Header thumbnail (item-DB image) with icon fallback */
/* A photographed thing gets a proper plate rather than a coloured chip — bigger, padded so the art
   is not flush to the edge, and on an inset ground so a cut-out icon reads against the panel. */
/* Same square as every other plate; what changes is the ground — an inset, so a cut-out icon reads
   against it — and the padding, so the art is not flush to the edge. */
.mp-ico.mp-img { position: relative; overflow: hidden; padding: 4px; background: var(--inset); border-color: var(--line-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; }
/* In step with the plate: this is the glyph drawn when the artwork 404s, and a fallback that stays
   small on a plate that grew reads as a broken image rather than as the deliberate stand-in it is. */
.mp-ico.mp-img .mp-ico-fb .ico { width: 28px; height: 28px; color: var(--amber-hi); }
.mp-ico.mp-img.mp-imgfail img { display: none; }
.mp-ico.mp-img.mp-imgfail .mp-ico-fb { display: flex; }
/* List-row thumbnail */
.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; }
/* A row with no artwork: the kind's glyph on the kind's colour, in the picture's own place. Its
   background comes from `--badge-bg` in the block above, beside `.ml-badge` and `.mp-ico-fb` — the
   layer picker's badge and the marker card's fallback — so all three say what a kind looks like
   with no portrait, and none of them can drift from the others. The glyph is `var(--bg)` on the
   kind's colour, which is the pair `.ml-badge` already uses and `check-contrast` already reads. */
.ml-thumb.ml-thumb-fb { border: 1px solid rgba(0, 0, 0, 0.35); }
.ml-thumb.ml-thumb-fb .ico { width: 13px; height: 13px; color: var(--bg); }
.ml-row.off .ml-thumb-fb { opacity: 0.7; }

.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-locked {
  position: absolute; inset: 0; z-index: 30; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 0.5rem; text-align: center;
  background: rgba(8, 10, 8, 0.9); backdrop-filter: blur(3px); border: 1px solid var(--line); border-radius: var(--radius-md);
}
.map-locked .map-lock-ico { width: 40px; height: 40px; color: var(--amber); margin-bottom: 0.3rem; }
.map-locked h3 { margin: 0; color: var(--text); }
.map-locked p { margin: 0; color: var(--muted); max-width: 340px; }
.map-locked button { margin-top: 0.6rem; }
.map-locked.hidden { display: none; }

/* ── Login (Discord + username) + Admins section ─────────────────────────── */
.login-discord {
  display: flex; align-items: center; justify-content: center; gap: 0.5rem; width: 100%; margin-top: 0.5rem;
  padding: 0.6rem; background: var(--discord); border: 1px solid var(--discord); color: #fff;
  font-family: var(--ui); font-weight: 600; border-radius: var(--radius-md); cursor: pointer; text-decoration: none;
}
.login-discord:hover { background: var(--discord-dn); border-color: var(--discord-dn); }
.login-discord .ico { width: 18px; height: 18px; }
.anav-btn[hidden] { display: none; }

.amodal.amodal-md .amodal-box { max-width: 620px; }
#admins-content { margin-top: 1.25rem; }
.adm-card {
  display: flex; align-items: center; gap: 1rem; padding: 0.85rem 1rem; margin: 0 1.5rem 0.6rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-lg);
}
.adm-card.adm-default { border-color: var(--line-amber); }
.adm-card.adm-off { opacity: 0.6; }
.adm-main { flex: 1; min-width: 0; }
.adm-avatar { width: 44px; height: 44px; border-radius: 50%; flex: none; object-fit: cover; border: 1px solid var(--line); }
.adm-avatar-fb {
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 1.1rem; color: var(--muted); background: var(--panel-2);
}
.adm-avatar-fb.discord { color: #fff; background: var(--discord); border-color: var(--discord); }
.adm-name { font-weight: 700; color: var(--text); }
.adm-sub { display: flex; align-items: center; font-size: 0.8rem; color: var(--muted); margin: 0.1rem 0 0.4rem; }
.adm-did { font-variant-numeric: tabular-nums; opacity: 0.75; }
.adm-dot { margin: 0 0.4rem; opacity: 0.5; }
.adm-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }
.adm-methods { display: inline-flex; gap: 0.3rem; margin-right: 0.15rem; }
.adm-method, .adm-badge {
  display: inline-flex; align-items: center; height: 20px; box-sizing: border-box;
  padding: 0 0.5em; border-radius: var(--radius); background: var(--panel-2); border: 1px solid var(--line);
  color: var(--muted); font-size: 0.7rem; line-height: 1;
}
.adm-method { font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }
.adm-method.discord { color: var(--discord-hi); background: rgba(88, 101, 242, 0.18); border-color: rgba(88, 101, 242, 0.5); }
.adm-method.off { color: var(--alarm); border-color: rgba(255, 92, 92, 0.4); }
.adm-perms { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.adm-badge.full { font-weight: 700; color: var(--on-amber); background: var(--amber); border-color: var(--amber); letter-spacing: 0.01em; }
/* "no permissions" — an ABSENCE, so the quietest chip on the row rather than the loudest thing on
   the card. It used to be a bare `.dim` span with no rule reaching it. */
.adm-badge-none { color: var(--faint); background: transparent; border-style: dashed; }
.adm-badge.dim { opacity: 0.7; }
.adm-actions { display: flex; gap: 0.4rem; flex: none; }
.adm-actions .secondary { padding: 0.35rem 0.6rem; font-size: 0.82rem; }
.adm-toggle { display: inline-flex; align-items: center; gap: 0.45rem; color: var(--text); font-weight: 600; font-size: 0.85rem; cursor: pointer; flex: none; }
.adm-toggle input { accent-color: var(--amber); }
.adm-form { margin: 1rem 1.5rem 0; padding: 1rem; background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius-lg); }
.adm-form-title { font-weight: 700; color: var(--amber-hi); margin-bottom: 0.6rem; }
.adm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.75rem; }
.adm-field { display: flex; flex-direction: column; gap: 0.3rem; }
.adm-field label { font-size: 0.8rem; color: var(--muted); font-weight: 600; }
.adm-field input { padding: 0.5rem 0.6rem; background: var(--panel-2); border: 1px solid var(--line); color: var(--text); border-radius: var(--radius-md); font-family: var(--ui); }
.adm-field input:disabled { opacity: 0.6; }
.adm-perm-actions { display: flex; align-items: center; gap: 0.5rem; margin: 0.5rem 0; }
.adm-perm-heading { font-size: 0.82rem; color: var(--muted); font-weight: 600; margin-right: auto; }
.adm-perm-group { margin-bottom: 0.7rem; }
.adm-perm-gtitle { font-size: 0.68rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; margin: 0 0 0.35rem; }
.adm-perm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); gap: 0.45rem; }
.adm-perm {
  display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.6rem;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-md); cursor: pointer;
  font-size: 0.82rem; font-weight: 600; color: var(--text); user-select: none;
}
.adm-perm:hover { border-color: var(--line-amber); background: var(--hover-bg); }
.adm-perm:has(input:checked) { border-color: var(--amber); background: rgba(255, 106, 26, 0.14); color: var(--amber-hi); }
.adm-perm input { accent-color: var(--amber); width: 15px; height: 15px; flex: none; }
.adm-form-actions { display: flex; gap: 0.6rem; margin-top: 1rem; }
.link-btn { background: none; border: none; color: var(--amber-hi); cursor: pointer; font-family: var(--ui); font-weight: 600; font-size: 0.8rem; padding: 0.15rem 0.25rem; }
.link-btn:hover { background: none; color: #fff; text-decoration: underline; }
@media (max-width: 640px) { .adm-grid { grid-template-columns: 1fr; } .adm-card { flex-direction: column; align-items: stretch; } }

/* Language settings: default-language dropdown + enabled-languages checkbox list */
.settings-field select { font-family: var(--ui); font-size: 0.9rem; padding: 0.5rem 0.6rem; background: var(--panel-2, #14150f); color: var(--fg, #e8e6df); border: 1px solid var(--line, #2a2c22); border-radius: 6px; }
.lang-checks { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; margin-top: 0.4rem; }
.lang-check { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; cursor: pointer; }
.lang-check input { width: auto; margin: 0; }
.lang-check input:disabled { opacity: 0.6; cursor: default; }
/* Header per-admin language switcher (premium) — same metrics as the nav links
   next to it (padding/font/border), so all header controls share one height. */
.admin-lang-select {
  padding: 0.5rem 0.8rem; font-size: 0.82rem; font-family: var(--ui); font-weight: 600; letter-spacing: 0.05em;
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: var(--radius);
  cursor: pointer; outline: none;
}
.admin-lang-select:hover { background: var(--panel-2); border-color: var(--line-amber); color: var(--amber-hi); }
/* Custom language dropdown with flags (native selects can't show images). */
.lang-dd { position: relative; display: inline-flex; }
/* line-height 1.5 = what the neighbouring <a class="navlink"> controls inherit
   from body; buttons default to ~1.2 and end up a few px shorter without it. */
.lang-dd-btn { display: inline-flex; align-items: center; gap: 0.5rem; line-height: 1.5; }
.lang-flag { width: 1.333em; height: 1em; border-radius: 2px; object-fit: cover; flex: none; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); } /* 4:3, matches flag-icons SVGs (640×480) */
.lang-dd-chev { color: var(--muted); font-size: 0.7rem; }
.lang-dd-list {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 90; min-width: 175px;
  display: flex; flex-direction: column; gap: 2px; padding: 0.35rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}
.lang-dd-list[hidden] { display: none; }
/* Two columns: nineteen languages in one ran past the bottom of the window. The heading and the
   translations link span both; a phone goes back to one column. */
.lang-dd-list:not([hidden]) { display: grid; grid-template-columns: repeat(2, max-content); align-content: start; max-height: calc(100vh - 5rem); overflow-y: auto; }
.lang-dd-list > .lang-dd-head, .lang-dd-list > .lang-dd-foot { grid-column: 1 / -1; }
@media (max-width: 640px) { .lang-dd-list:not([hidden]) { grid-template-columns: minmax(0, 1fr); } }
.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.85rem; cursor: pointer; text-align: left;
}
.lang-dd-item:hover { background: var(--hover-bg); color: var(--amber-hi); }
.lang-dd-item.active { color: var(--amber-hi); }

/* Unified MOTD editor: full-width field — label + language buttons on the top row,
   one large textarea spanning underneath (the default label/control grid is too
   narrow for a text banner). */
.settings-field.motd-field { display: flex; flex-direction: column; align-items: stretch; gap: 0.45rem; }
.motd-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.motd-langs { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.motd-lang-btn {
  padding: 0.22rem 0.65rem; font-size: 0.8rem; cursor: pointer;
  background: var(--inset); color: var(--muted); border: 1px solid var(--line); border-radius: var(--pill);
}
.motd-lang-btn:hover { background: var(--inset); color: var(--text); border-color: var(--line-amber); }
.motd-lang-btn.active { background: var(--panel-2); color: var(--amber-hi); border-color: var(--line-amber); }
.motd-editor { display: block; }
.motd-editor-text { width: 100%; min-height: 5.5rem; resize: vertical; line-height: 1.4; }

/* Custom dropdown chevron with breathing room — the native arrow sits glued to the
   right border and ignores padding, so replace it (appearance:none) with an inline
   SVG placed a comfortable 0.7rem from the edge. Kept at the END of the sheet so no
   later `background:` shorthand can wipe the image. Covers the header language
   switcher, roster/chat toolbars, config pickers and row editors. */
.roster-select, .settings-field select, .er-f select {
  appearance: none; -webkit-appearance: none;
  padding-right: 2rem;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23949aa8' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
}

/* Floating "back to top" button (bottom-right). Overrides the global amber button
   base — this one is a quiet circular outline control that fades in on scroll. */
.back-top {
  position: fixed; right: 1.1rem; bottom: 1.1rem; z-index: 80;
  width: 46px; height: 46px; padding: 0; border-radius: 50%;
  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-2); border-color: var(--amber); color: var(--amber); }
.back-top svg { width: 20px; height: 20px; }

/* ══ Motion design (mirrors the public site) ════════════════════════════════
   Tab transitions, modal reveal and microinteractions. CSS-only; animations
   restart naturally when an element is re-shown (.hidden toggles display).
   NOTE: nothing here sets a `background:` shorthand on selects — the custom
   dropdown-chevron rule above must stay effective. */

/* Tab switch: the shown panel fades in and rises slightly. */
@keyframes viewIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.tab-panel { animation: viewIn 0.26s ease both; }

/* Intro: the nav buttons settle in once on load.

   ⚠ **IT MOVES AND DOES NOT FADE. AN ENTRY ANIMATION MUST NEVER BE THE THING THAT MAKES THE
   NAVIGATION VISIBLE.** This keyframe was `from { opacity: 0; … }` with `both`, which is the same
   rule the field console shipped and the same defect: a CSS animation begins on the browser's next
   RENDERING UPDATE, not when it is created, and until that frame arrives it is play-pending —
   `startTime: null`, hold time `currentTime: 0`. Zero is inside the active interval, so the first
   keyframe applies and `animation-fill-mode` cannot help; dropping `both` was tried first and the
   buttons stayed invisible. Measured on the field console's identical pair on 2026-09-18: the
   active tab read `pending=true startTime=null currentTime=0` and computed `opacity: 0` at 100 ms
   through 2500 ms, with one rAF callback in the whole run. A phone's first paint owes the same
   frame, and that is the paint where the tab bar has to be there. `.tab-panel` above keeps its fade
   because it is the content of a screen already reached; this is the row you reach it with.
   See `src/web/public-site/style.css` for the full measurement. */
@keyframes topIn { from { transform: translateY(-6px); } to { transform: none; } }
.anav-btn { animation: topIn 0.3s ease; }

/* Modal reveal (player detail / app status). */
@keyframes modalIn { from { opacity: 0; transform: translateY(14px) scale(0.985); } to { opacity: 1; transform: none; } }
.amodal-box { 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 nav/tab/quiet controls (the main
   button system already transitions via the global `button` rule). */
.anav-btn, .pd-tab, .lang-dd-btn, .lang-dd-item, .navlink, .roster-select,
.motd-lang-btn, .foot-link, .gl-map, .gl-player, .pd-maplink {
  transition: color 0.15s, border-color 0.15s, background-color 0.15s, transform 0.15s;
}

/* Summary/stat cards lift gently on hover. */
.pd-stat, .pd-card { transition: border-color 0.18s, transform 0.18s; }
.pd-stat:hover { border-color: var(--line-amber); transform: translateY(-2px); }

/* Themed scrollbars for the whole panel (the map lists already had them). */
html { scrollbar-width: thin; scrollbar-color: rgba(255, 154, 60, 0.35) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255, 154, 60, 0.28); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 154, 60, 0.45); }

/* Accessibility: honour the OS "reduce motion" preference. */
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; scroll-behavior: auto; } }

/* ── Manager self-update: badge, footer link, card, modal, progress ───────── */
#mgr-update-btn.has-update { border-color: var(--amber); color: var(--amber-hi); }
#mgr-update-btn.has-update::after { content: ''; position: absolute; top: -3px; right: -3px; width: 9px; height: 9px; border-radius: 50%; background: var(--amber); box-shadow: 0 0 0 2px var(--panel), 0 0 6px var(--amber); animation: mu-pulse 1.8s ease-in-out infinite; }
#mgr-update-btn { position: relative; }
@keyframes mu-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

.foot-update { margin-left: .5rem; background: none; border: 0; padding: 0; font: inherit; color: var(--amber); cursor: pointer; text-decoration: none; }
.foot-update:hover { color: var(--amber-hi); text-decoration: underline; }
.foot-update.hidden { display: none; }

.mu-card { border: 1px solid var(--line); background: var(--panel); border-radius: var(--radius); padding: 1.1rem 1.3rem; margin: 0.5rem 1.5rem 2rem; }
.mu-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.mu-head-l { min-width: 0; }
.mu-title { display: flex; align-items: center; gap: .5rem; font-family: var(--display, inherit); font-size: .82rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text); }
.mu-title .ico { width: 16px; height: 16px; color: var(--amber); }
.mu-ver { margin-top: .35rem; color: var(--muted); font-size: .84rem; }
.mu-ver b { color: var(--text); font-weight: 600; }
.mu-head #mu-check-btn { flex: none; }
.mu-status { margin-top: .7rem; font-size: .9rem; min-height: 1.2em; }
.mu-avail { color: var(--amber); font-weight: 600; }
.mu-ok { color: var(--online); }
.mu-muted { color: var(--muted); }
.mu-link { background: none; border: 0; padding: 0 0 0 .3rem; font: inherit; color: var(--amber); cursor: pointer; text-decoration: underline; }
.mu-auto { display: flex; align-items: center; gap: .5rem; margin-top: .9rem; padding-top: .8rem; border-top: 1px dashed var(--line); color: var(--muted); font-size: .86rem; cursor: pointer; }
.mu-auto.hidden { display: none; }

.mu-modal-ver { font-size: 1.15rem; margin: .3rem 0 .8rem; }
.mu-vfrom { color: var(--muted); }
.mu-arrow { color: var(--amber); margin: 0 .3rem; }
.mu-vto { color: var(--amber-hi); font-weight: 700; }
.mu-notes { max-height: 40vh; overflow: auto; background: var(--bg-2); border: 1px solid var(--line); border-radius: 6px; padding: .7rem .9rem; font-size: .86rem; line-height: 1.55; color: var(--data-text); white-space: pre-wrap; }
.mu-links { margin: .8rem 0; font-size: .88rem; }
.mu-links a { color: var(--amber); }
.mu-modal-actions { margin: 1rem 0 .5rem; }
.mu-install-btn { background: var(--amber); color: var(--on-amber); border: 0; padding: .6rem 1.4rem; border-radius: 6px; font-weight: 700; cursor: pointer; letter-spacing: .02em; }
.mu-install-btn:hover { background: var(--amber-hi); }
.mu-prem-note { margin: 1rem 0 .5rem; font-size: .86rem; line-height: 1.5; }
.mu-dismiss { display: flex; align-items: center; gap: .5rem; margin-top: 1rem; padding-top: .8rem; border-top: 1px solid var(--line); color: var(--muted); font-size: .84rem; cursor: pointer; }

.mu-progbar { width: 100%; max-width: 320px; height: 8px; margin: 1rem auto 0; background: var(--bg-2); border: 1px solid var(--line); border-radius: 5px; overflow: hidden; }
.mu-progfill { height: 100%; width: 0; background: var(--amber); transition: width .3s ease; }
.mu-progfill.mu-progerr { background: #d9534f; }

.mu-feed { margin-top: .9rem; padding-top: .8rem; border-top: 1px dashed var(--line); }
.mu-feed-label { display: block; font-size: .8rem; letter-spacing: .03em; text-transform: uppercase; color: var(--muted); margin-bottom: .4rem; }
.mu-feed-row { display: flex; gap: .5rem; flex-wrap: wrap; }
.mu-feed-input { flex: 1 1 340px; min-width: 0; background: var(--bg-2); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .7rem; color: var(--text); font-family: inherit; font-size: .86rem; }
.mu-feed-input:focus { outline: none; border-color: var(--amber); }
.mu-feed-hint { margin-top: .4rem; font-size: 0.75rem; line-height: 1.45; color: var(--muted); }
.mu-feed-msg { margin-top: .4rem; font-size: .84rem; min-height: 1em; }
.mu-feed-msg.mu-ok { color: var(--online); }
.mu-feed-msg.mu-err, .mu-err { color: #d9534f; }

/* ── Plugins section ─────────────────────────────────────────────────────── */
.pl-viewtabs { display: inline-flex; max-width: 100%; overflow-x: auto; scrollbar-width: none; gap: 2px; background: rgba(255,255,255,0.04); border: 1px solid var(--line); border-radius: 999px; padding: 2px; }
/* Also real links, same reason — see `.gs-tab`. */
.pl-vtab { display: inline-block; text-decoration: none; border: 0; background: transparent; color: var(--muted); font: inherit; font-size: 0.8rem; padding: 0.32rem 0.95rem; border-radius: 999px; cursor: pointer; }
.pl-vtab:hover { color: var(--text); text-decoration: none; }
.pl-vtab.active { background: var(--amber); color: var(--on-amber); font-weight: 600; }
.pl-installed { font-size: 0.76rem; color: #8fe0a8; display: inline-flex; align-items: center; gap: 0.3rem; }
.pl-filter { display: inline-flex; max-width: 100%; overflow-x: auto; scrollbar-width: none; gap: 2px; background: rgba(255,255,255,0.04); border: 1px solid var(--line); border-radius: 999px; padding: 2px; }
.pl-fbtn { border: 0; background: transparent; color: var(--muted); font: inherit; font-size: 0.8rem; padding: 0.3rem 0.8rem; border-radius: 999px; cursor: pointer; }
.pl-fbtn.active { background: var(--amber); color: var(--on-amber); font-weight: 600; }
#pl-lockwrap { margin: 1rem 1.5rem 2rem; }
/* drag-and-drop install overlay */
#plugins-screen { position: relative; }
.pl-drop { position: absolute; inset: 0; z-index: 60; display: none; align-items: center; justify-content: center;
  background: rgba(15, 16, 19,0.82); backdrop-filter: blur(2px); border: 2px dashed var(--amber); border-radius: var(--radius-lg); pointer-events: none; }
#plugins-screen.pl-dragging .pl-drop { display: flex; }
.pl-drop-in { display: flex; align-items: center; gap: 0.6rem; font-family: var(--ui, inherit); font-weight: 600; font-size: 1.1rem; color: var(--amber); }
.pl-drop-in .ico { width: 22px; height: 22px; }
.pl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 360px), 1fr)); gap: 1rem; padding: 0.2rem; }

/* Reusable section card for plugin workspaces (the SDK documents `.card`). A plain boxed panel
   with its own padding — plugins stack these inside their workspace. */
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-md); padding: 1.1rem 1.2rem; }
.card > h2:first-child, .card > h3:first-child { margin-top: 0; }

/* card: fixed-shape row (thumb + body); body is a column with the footer pinned to the bottom
   so every card lines up regardless of description length. */
.pl-card { display: flex; gap: 0.9rem; padding: 1rem; background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius-lg); transition: border-color 0.15s, box-shadow 0.15s; }
.pl-card.on { border-color: var(--line-amber); }
.pl-thumb { flex: 0 0 56px; width: 56px; height: 56px; border-radius: var(--radius); overflow: hidden;
  background: rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: center; }
.pl-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pl-ph { width: 26px; height: 26px; opacity: 0.4; }
.pl-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; }

/* header row: name grows & truncates, badge + toggle stay put on one line */
.pl-head { display: flex; align-items: center; gap: 0.5rem; }
.pl-name { font-weight: 600; color: var(--text); font-size: 0.98rem; min-width: 0; flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* …and on a phone that row cannot hold all three. Measured at 320px the body is 145px wide, of which
   the type badge takes ~62 and the toggle 40: the name was left with about 27px and every card was
   titled "R…", "SS…", "UE…". So the name takes the whole first line and the badge and the switch
   drop under it, which is the only arrangement in which a plugin's name is readable at all. */
@media (max-width: 600px) {
  .pl-head { flex-wrap: wrap; }
  .pl-name { flex: 1 1 100%; white-space: normal; overflow: visible; text-overflow: clip;
    overflow-wrap: anywhere; }
}
.pl-badge { font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.14rem 0.44rem;
  border-radius: 999px; border: 1px solid var(--line); color: var(--muted); flex: 0 0 auto; white-space: nowrap; }
.pl-b-ue { border-color: rgba(255, 106, 26,0.5); color: var(--amber); }
.pl-b-rt { border-color: rgba(120,160,255,0.5); color: #9db8ff; }
.pl-b-mgr { border-color: rgba(120,220,150,0.5); color: #8fe0a8; }
.pl-actions { display: flex; align-items: center; gap: 0.4rem; flex: 0 0 auto; }
.pl-switch { position: relative; display: inline-block; width: 40px; height: 22px; flex: 0 0 auto; }
.pl-switch input { opacity: 0; width: 0; height: 0; }
.pl-slider { position: absolute; inset: 0; background: rgba(255,255,255,0.14); border-radius: 999px; cursor: pointer; transition: 0.18s; }
.pl-slider::before { content: ""; position: absolute; height: 16px; width: 16px; left: 3px; top: 3px; background: #ddd; border-radius: 50%; transition: 0.18s; }
.pl-switch input:checked + .pl-slider { background: var(--amber); }
.pl-switch input:checked + .pl-slider::before { transform: translateX(18px); background: var(--on-amber); }
.pl-switch input:disabled + .pl-slider { opacity: 0.4; cursor: not-allowed; }

/* meta / description / deps — all clamp so cards keep an even height */
.pl-meta { color: var(--muted); font-size: 0.76rem; display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; min-width: 0; }
.pl-meta a { color: var(--amber); text-decoration: none; }
.pl-rt { font-size: 0.66rem; color: #e6b96a; border: 1px dashed rgba(230,185,106,0.5); border-radius: 4px; padding: 0.02rem 0.34rem; }
/* At most FOUR lines.

   ⚠ IT WAS TWO, AND TWO LINES CANNOT SAY WHAT A PLUGIN DOES. The card is `minmax(min(100%, 360px))`
   and the description sits beside the icon, so two lines is about eighty characters — every plugin
   in the library was cut mid-word before its first sentence ended, on the one screen where somebody
   decides whether to install it. There is no Readme button on a browse card, so those eighty
   characters were the whole of what a stranger got. Four lines is a first sentence.

   ⚠ AND IT CARRIED `min-height: 6em` FOR ONE RENDER, WHICH BOUGHT NOTHING AND COST A HOLE. The
   argument for it was that a short description pulls everything under it upward and leaves a row of
   cards ragged. It does not: the grid stretches every card in a row to the tallest of them, and
   `.pl-foot` is `margin-top: auto`, so the buttons are pinned to the BOTTOM of whatever height the
   row settled on. Measured rather than argued — with the reservation and without it, the installed
   grid came out byte-identical at 1424, 1920, 1366 and 320px, every card and every footer on the
   same pixel.
   What it did change is the screen where descriptions really are short. The BROWSE catalogue's
   are one line ("Claim-aware mining rules."), and four lines were reserved for each of them: about
   90px of nothing between the sentence and the Install button, on every card, on the one screen a
   stranger judges this product by. A reservation is only free while something fills it, and the
   half of the library that would have filled it is not the half this rule is about.
   `textFills` on the plugin views is what keeps the fixture honest about which half is which. */
.pl-desc { margin: 0; color: var(--muted); font-size: 0.82rem; line-height: 1.5; min-width: 0; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
.pl-deps { font-size: 0.74rem; color: var(--muted); display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; min-width: 0; }
.pl-deps-l { text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.66rem; }
.pl-dep { padding: 0.06rem 0.4rem; border-radius: 999px; border: 1px solid var(--line); }
.pl-dep.ok { border-color: rgba(120,220,150,0.5); color: #8fe0a8; }
.pl-dep.bad { border-color: rgba(220,120,120,0.5); color: #e39a9a; }
.pl-incompat { display: flex; align-items: center; gap: 0.4rem; font-size: 0.74rem; color: #e39a9a;
  background: rgba(220,120,120,0.1); border: 1px solid rgba(220,120,120,0.4); border-radius: 6px; padding: 0.35rem 0.5rem; overflow-wrap: anywhere; }
.pl-incompat .ico { width: 14px; height: 14px; flex: 0 0 auto; }

/* ── the card's bottom group ────────────────────────────────────────────────
   Three blocks belong together at the foot of the card: why it cannot run, what
   it still needs switched on, and its buttons. A grid stretches every cell in a
   row to the tallest, and the slack that leaves has to pool in ONE place or the
   cards read as ragged — which is what an owner reported. Whichever of the
   three comes first takes the slack; the ones after it sit tight against it.
   Written as adjacent-sibling rules rather than one auto margin each, because
   two `margin-top: auto` in a flex column SPLIT the free space between them
   and put a gap in the middle of the group. */
.pl-incompat, .pl-bneeds, .pl-foot { margin-top: auto; }
.pl-incompat + .pl-bneeds, .pl-incompat + .pl-foot, .pl-bneeds + .pl-foot { margin-top: 0; }

/* ── what a plugin needs switched on in the bridge ──────────────────────────
   A modal, and — only when something is actually off — one strip on the card
   that leads to it, so an owner is not left hunting for the switch among 43
   module cards four screens away.

   The strip IS the button. It used to be a box holding an icon, a sentence and
   a separate button, and at a card's real width (323px at 1440) those three
   could not share a line: every one of them was a 77px two-line panel nested
   inside the card, and 111px on a phone. One control, one line, one colour.

   And it draws for two of the four states only. `ok` and `info` mean there is
   nothing to do, which is what most cards say most of the time; they collapse
   to .pl-bn-quiet in the footer beside the ordinary buttons. A page whose every
   card carries a green box announcing that nothing is wrong is a page of text
   nobody reads, and it was the loudest thing on the screen.

   It sits in the card's bottom group (above), so it rides down against the
   footer rather than floating wherever the blocks above it happen to end. */
.pl-bneeds { display: flex; align-items: center; gap: 0.45rem;
  font: inherit; font-size: 0.74rem; text-align: left; width: 100%; cursor: pointer;
  box-sizing: border-box;
  border: 1px solid var(--line); border-radius: 6px; padding: 0.4rem 0.55rem; min-width: 0; }
.pl-bneeds .ico { width: 14px; height: 14px; flex: 0 0 auto; }
.pl-bn-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* Both halves of every hover live on the ELEMENT, so they travel together: the strip is a <button>
   and the panel's blanket `button:hover` would otherwise paint it amber with dark text. */
.pl-bneeds[data-state="pending"] { color: var(--amber); border-color: rgba(230,185,106,0.45); background: rgba(230,185,106,0.08);
  --hov-bg: rgba(230,185,106,0.18); --hov-fg: var(--amber-hi); --hov-line: rgba(230,185,106,0.7); }
.pl-bneeds[data-state="blocked"] { color: #e39a9a; border-color: rgba(220,120,120,0.4); background: rgba(220,120,120,0.08);
  --hov-bg: rgba(220,120,120,0.2); --hov-fg: #f0b3b3; --hov-line: rgba(220,120,120,0.7); }
/* The quiet half, in the footer: a tick and the same way in, tinted by state rather than boxed.
   Both halves of the hover are set in one rule — the sibling .pl-update does the same — because a
   :hover that restates only the fill or only the colour is the defect check-contrast exists for. */
.pl-btn.pl-bn-quiet[data-state="ok"] { color: #8fe0a8; border-color: rgba(120,220,150,0.45); }
.pl-btn.pl-bn-quiet[data-state="ok"]:hover { background: rgba(120,220,150,0.14);
  border-color: rgba(120,220,150,0.7); color: #a8ebbd; }
.pl-btn.pl-bn-quiet[data-state="info"] { color: var(--muted); }

.pl-bn-modal .pl-modal-body { overflow: auto; padding: 0.9rem 1.1rem; }
.pl-bn-lead { margin: 0 0 0.7rem; font-size: 0.8rem; color: var(--muted); line-height: 1.5; }
.pl-bn-warn { margin: 0 0 0.7rem; font-size: 0.78rem; color: var(--amber); line-height: 1.5; }
.pl-bn-h { margin: 0.9rem 0 0.4rem; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.pl-bn-group { border: 1px solid var(--line); border-radius: 8px; padding: 0.6rem 0.7rem; margin-bottom: 0.6rem; }
.pl-bn-mod { font-size: 0.88rem; color: var(--text); font-weight: 600; }
.pl-bn-why { margin: 0.25rem 0 0.6rem; font-size: 0.78rem; color: var(--muted); line-height: 1.5; }
.pl-bn-row { border-top: 1px solid var(--line); padding: 0.5rem 0 0.1rem; }
.pl-bn-rhead { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; min-width: 0; }
.pl-bn-name { flex: 1 1 12rem; min-width: 0; font-size: 0.82rem; color: var(--text); overflow-wrap: anywhere; }
.pl-bn-state { font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.04em;
  border-radius: 999px; padding: 0.04rem 0.42rem; border: 1px solid var(--line); color: var(--muted); }
.pl-bn-state.pl-bs-on { color: #8fe0a8; border-color: rgba(120,220,150,0.5); }
.pl-bn-state.pl-bs-off { color: var(--amber); border-color: rgba(230,185,106,0.5); }
.pl-bn-state.pl-bs-missing, .pl-bn-state.pl-bs-unknown { color: #e39a9a; border-color: rgba(220,120,120,0.5); }
/* The tier, in the shipped table's own words. Danger-family reads red, cost-family amber, the rest
   plain — the eleven tiers are named in full beside the badge, so colour only has to say "stop". */
.pl-bn-tier { font-size: 0.64rem; border-radius: 4px; padding: 0.04rem 0.4rem; border: 1px solid var(--line); color: var(--muted); }
.pl-bn-tier.pl-bt-danger { color: #e39a9a; border-color: rgba(220,120,120,0.5); }
.pl-bn-tier.pl-bt-cost { color: var(--amber); border-color: rgba(230,185,106,0.5); }
.pl-bn-hint { margin: 0.3rem 0 0; font-size: 0.75rem; color: var(--muted); line-height: 1.5; }
.pl-bn-block { margin: 0.3rem 0 0; font-size: 0.75rem; color: #e39a9a; line-height: 1.5; }
.pl-bn-where { margin: 0.25rem 0 0; font-size: 0.7rem; color: var(--faint, var(--muted)); }

.pl-empty { grid-column: 1 / -1; color: var(--muted); font-size: 0.85rem; padding: 1.4rem; text-align: center; }

/* footer: buttons on one aligned row, pinned to the card bottom; uninstall floats right */
/* `margin-top` is set by the bottom-group rules further up, not here: the footer takes the slack
   only when nothing else in that group came before it. */
.pl-foot { padding-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; }
.pl-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; font: inherit;
  font-size: 0.76rem; line-height: 1; height: 30px; box-sizing: border-box; white-space: nowrap;
  border: 1px solid var(--line); background: rgba(255,255,255,0.03); color: var(--text);
  padding: 0 0.7rem; border-radius: 7px; cursor: pointer; transition: background .12s, border-color .12s, color .12s; }
.pl-btn:hover { background: rgba(230,185,106,0.14); border-color: rgba(230,185,106,0.5); color: var(--amber); }
.pl-btn .ico { width: 15px; height: 15px; flex: 0 0 auto; }
.pl-btn.primary { background: var(--amber); border-color: var(--amber); color: var(--on-amber); font-weight: 600; }
.pl-btn.primary:hover { filter: brightness(1.08); color: var(--on-amber); }
.pl-btn.pl-update { border-color: rgba(120,160,255,0.55); color: #9db8ff; }
.pl-btn.pl-update:hover { background: rgba(120,160,255,0.14); border-color: rgba(120,160,255,0.7); color: #b9cbff; }
.pl-btn.pl-danger { color: #e39a9a; }
.pl-btn.pl-danger:hover { background: rgba(220,120,120,0.14); border-color: rgba(220,120,120,0.5); color: #e39a9a; }

/* plugin workspace (a plugin's own tabs, opened in-place from its card) */
#plugins-screen.pl-in-workspace > .screen-bar,
#plugins-screen.pl-in-workspace > .settings-note,
#plugins-screen.pl-in-workspace > .prem-lock-wrap { display: none; }
.pl-workspace { display: flex; flex-direction: column; margin: 0.2rem 1.5rem 2rem; }
.pl-workspace.hidden { display: none; }
/* header bar: prominent back button + plugin title, clearly separated from the content */
.pl-ws-bar { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: 0.2rem 0 1rem; margin-bottom: 1.2rem; border-bottom: 1px solid var(--line); }
.pl-ws-back { display: inline-flex; align-items: center; gap: 0.45rem; font: inherit; font-size: 0.85rem;
  border: 1px solid var(--line); background: rgba(255,255,255,0.04); color: var(--text);
  padding: 0.5rem 0.9rem; border-radius: 8px; cursor: pointer; transition: background .12s, border-color .12s, color .12s; }
.pl-ws-back:hover { background: rgba(230,185,106,0.14); border-color: rgba(230,185,106,0.5); color: var(--amber); }
.pl-ws-arrow { font-size: 1.1em; line-height: 1; }
.pl-ws-title { font-family: var(--ui, inherit); font-weight: 600; font-size: 1.1rem; color: var(--text);
  display: inline-flex; align-items: center; gap: 0.5rem; }
.pl-ws-title::before { content: ""; width: 4px; height: 1.1em; border-radius: 2px; background: var(--amber); }
/* sub-nav of the plugin's own tabs */
.pl-ws-nav { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1.2rem; }
.pl-ws-tab { display: inline-flex; align-items: center; gap: 0.4rem; font: inherit; font-size: 0.85rem;
  border: 1px solid var(--line); background: rgba(255,255,255,0.03); color: var(--muted); padding: 0.45rem 0.9rem;
  border-radius: 8px; cursor: pointer; transition: background .12s, color .12s, border-color .12s; }
.pl-ws-tab:hover { color: var(--text); background: rgba(255,255,255,0.06); }
.pl-ws-tab.active { color: var(--on-amber); background: var(--amber); border-color: var(--amber); font-weight: 600; }
.pl-ws-tab .fic { width: 16px; height: 16px; display: inline-flex; }
/* AN EMOJI IS NOT A 16px SPRITE. `.ssa-ic` is what the SDK wraps a plugin's icon in when that icon
   is a character rather than a `#i-` sprite id, and both spellings sat on one 16px×16px rule — so
   every emoji tab icon this product ships was clipped by 8px, drawn as half a glyph. Nothing could
   see it: the markup is right, the id resolves, the colour is fine, and the rule is correct for the
   sprite it was written for. Found by rendering a plugin's tab for the first time. `width: auto`
   with a floor keeps the sprite's alignment and lets a glyph be as wide as it is. */
.pl-ws-tab .ssa-ic { min-width: 16px; height: 16px; display: inline-flex;
  align-items: center; justify-content: center; flex: 0 0 auto; line-height: 1; }
/* content surface: a plain container — the plugin supplies its own `.card` sections as the boxes,
   so we never double-frame. */
.pl-ws-body { min-width: 0; overflow: visible; }
.pl-ws-empty { color: var(--muted); padding: 1.5rem; }

/* config editor modal */
.pl-modal-ov { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; z-index: 200; padding: 1.5rem; }
.pl-modal { background: var(--panel, #1b1b1e); border: 1px solid var(--line); border-radius: 12px;
  width: min(720px, 100%); max-height: 88vh; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
.pl-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.9rem 1.1rem; border-bottom: 1px solid var(--line); font-size: 0.9rem; }
.pl-modal-head code { color: var(--amber); font-size: 0.82rem; }
.pl-modal-x { border: 0; background: transparent; color: var(--muted); font-size: 1.4rem; line-height: 1; cursor: pointer; padding: 0 0.2rem; }
.pl-modal-x:hover { color: var(--text); }
.pl-modal-ta { margin: 0; border: 0; border-radius: 0; resize: none; flex: 1; min-height: 320px;
  background: #101012; color: #d7d7db; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.8rem;
  line-height: 1.5; padding: 0.9rem 1.1rem; outline: none; white-space: pre; overflow: auto; }
.pl-modal-err { color: #e39a9a; font-size: 0.76rem; min-height: 1rem; padding: 0 1.1rem; }
.pl-modal-err:not(:empty) { padding: 0.5rem 1.1rem; }
.pl-modal-foot { display: flex; justify-content: flex-end; gap: 0.6rem; padding: 0.8rem 1.1rem; border-top: 1px solid var(--line); }

/* trader-map editor (reuses .pl-modal shell) */
.pl-modal.trader-map { width: min(760px, 100%); }
.trmap-intro { margin: 0.8rem 1.1rem 0.4rem; }
.trmap-body { flex: 1; overflow: auto; padding: 0.4rem 1.1rem 0.8rem; }
.trmap-rows { display: flex; flex-direction: column; gap: 0.5rem; }
.trmap-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.55rem 0.7rem; border: 1px solid var(--line); border-radius: 8px; background: rgba(255,255,255,0.02); }
.trmap-meta { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; flex: 1; }
.trmap-idline { display: flex; align-items: baseline; gap: 0.7rem; flex-wrap: wrap; }
.trmap-id { color: var(--amber); font-weight: 600; font-size: 0.85rem; }
.trmap-guess { color: var(--muted); font-size: 0.74rem; }
.trmap-funds { color: var(--text); font-size: 0.74rem; font-family: var(--mono); }
.trmap-cur { color: var(--muted); }
/* Sample items wrap over a few lines (an identification aid) instead of truncating to one. */
.trmap-sells { color: var(--muted); font-size: 0.72rem; line-height: 1.4; max-height: 3.6em; overflow-y: auto; }
.trmap-nosell { color: var(--alarm); font-style: italic; }
.trmap-gactions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.trmap-applytypes { padding: 0.32rem 0.6rem; font-size: 0.76rem; border-radius: var(--radius); cursor: pointer; }
.trmap-inputs { display: flex; gap: 0.4rem; flex-shrink: 0; }
/* Match the admin settings-field controls (those styles are scoped to .settings-field, so the modal
   inputs need their own copy or they render as unstyled browser defaults). */
.trmap-type, .trmap-outpost { padding: 0.5rem 0.6rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); color: var(--text); font-family: var(--mono); font-size: 0.9rem; }
.trmap-type { min-width: min(150px, 100%); font-family: var(--ui); }
.trmap-outpost { min-width: min(110px, 100%); font-family: var(--ui); }
.trmap-type:focus, .trmap-outpost:focus { outline: none; border-color: var(--amber); box-shadow: var(--focus-ring); }
/* Outpost groups: one card per auto-grouped outpost with a bulk "set all to" sector picker. */
.trmap-group { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 0.8rem; overflow: hidden; }
/* Outpost gold block — same rows as the trader groups, with its own explanatory subtitle. */
.trmap-gold .trmap-ghead { align-items: flex-start; }
.trmap-ghint { flex-basis: 100%; font-size: 0.76rem; color: var(--muted); font-weight: 400; }
.trmap-goldrow .trmap-inputs { justify-content: flex-end; }
.trmap-ghead { display: flex; align-items: center; justify-content: space-between; gap: 0.8rem; flex-wrap: wrap;
  padding: 0.55rem 0.75rem; background: rgba(255, 255, 255, 0.03); border-bottom: 1px solid var(--line); }
.trmap-gtitle { color: var(--amber); font-weight: 700; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; }
.trmap-gcount { color: var(--muted); font-weight: 400; text-transform: none; }
.trmap-gset { display: flex; align-items: center; gap: 0.4rem; font-size: 0.78rem; color: var(--muted); }
.trmap-gsector { padding: 0.35rem 0.5rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset); color: var(--text); font-family: var(--ui); font-size: 0.85rem; }
.trmap-group .trmap-rows { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.6rem 0.75rem; }
.trmap-foot { display: flex; align-items: center; justify-content: flex-end; gap: 0.6rem;
  padding: 0.8rem 1.1rem; border-top: 1px solid var(--line); }
.trmap-msg { margin-right: auto; font-size: 0.8rem; }
.trmap-msg.success { color: var(--online); }
.trmap-msg.error { color: var(--alarm); }
@media (max-width: 640px) {
  .trmap-row { flex-direction: column; align-items: stretch; }
  .trmap-inputs { justify-content: stretch; }
  .trmap-type { flex: 1; }
}

/* readme viewer (rendered HTML) */
.pl-modal.readme { width: min(820px, 100%); }
.pl-readme-body { flex: 1; overflow: auto; padding: 1.1rem 1.3rem; line-height: 1.6; font-size: 0.88rem; color: var(--text); }
.pl-readme-body h1 { font-size: 1.3rem; margin: 0.2rem 0 0.8rem; }
.pl-readme-body h2 { font-size: 1.08rem; margin: 1.4rem 0 0.5rem; padding-bottom: 0.3rem; border-bottom: 1px solid var(--line); }
.pl-readme-body h3 { font-size: 0.96rem; margin: 1.1rem 0 0.4rem; color: var(--amber); }
.pl-readme-body p { margin: 0.5rem 0; }
.pl-readme-body ul, .pl-readme-body ol { margin: 0.5rem 0; padding-left: 1.4rem; }
.pl-readme-body li { margin: 0.25rem 0; }
.pl-readme-body a { color: var(--amber); }
.pl-readme-body code { background: rgba(255,255,255,0.08); padding: 0.08rem 0.34rem; border-radius: 4px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.84em; }
.pl-readme-body pre { background: #101012; border: 1px solid var(--line); border-radius: 8px; padding: 0.8rem 1rem; overflow: auto; margin: 0.6rem 0; }
.pl-readme-body pre code { background: none; padding: 0; }
.pl-readme-body table { border-collapse: collapse; width: 100%; margin: 0.6rem 0; font-size: 0.84rem; }
.pl-readme-body th, .pl-readme-body td { border: 1px solid var(--line); padding: 0.4rem 0.6rem; text-align: left; vertical-align: top; }
.pl-readme-body th { background: rgba(255,255,255,0.04); }
.pl-readme-body hr { border: 0; border-top: 1px solid var(--line); margin: 1.1rem 0; }
.pl-readme-body blockquote { margin: 0.6rem 0; padding: 0.4rem 0.9rem; border-left: 3px solid var(--amber); color: var(--muted); }

/* ── Overview admin console (SSA Bridge) ──────────────────────────────────── */
.cmd-console h2 { display: flex; align-items: center; gap: 0.5rem; }
/* the console is short, so keep the premium lock card near the top and compact */
.cmd-console .prem-lock-wrap { min-height: 0; }
.cmd-console .prem-lock-overlay { align-items: flex-start; padding: 0.5rem; }
.cmd-console .prem-lock-card { top: 0.4rem; max-width: 420px; padding: 1rem 1.2rem; }
.cmd-console .prem-lock-card .map-lock-ico { width: 26px; height: 26px; margin-bottom: 0.15rem; }
.cmd-console .prem-lock-card h3 { font-size: 0.92rem; margin-bottom: 0.35rem; }
.cmd-console .prem-lock-card p { font-size: 0.8rem; margin-bottom: 0.8rem; }
.cmd-state { margin-left: auto; font-size: 0.72rem; font-weight: 400; color: var(--muted);
  text-transform: none; letter-spacing: 0; border: 1px solid var(--line); border-radius: var(--pill); padding: 0.1rem 0.6rem; }
.cmd-state:empty { display: none; }
/* A designated place, and the reason it needed one.
   `.panel-eyebrow` is a flex row holding icon · title · chip · FEED id, and BOTH `.cmd-state` above
   and `.feed-id` carry `margin-left: auto`. Two auto margins in one flex line SPLIT the free space
   between them, so the chip was pushed to the middle of the row — a framed pill floating with
   nothing above it, nothing below it and nothing aligned to either side. Owners reported it twice,
   as "0 online · free version dropped into the middle of the console" and as the word "live" doing
   the same on the world card. Each rule is correct on its own, which is why nothing read it as a
   defect; only the pair is wrong, and only in a container that has both.
   So the chip belongs to its TITLE — right beside the words it qualifies — and `.feed-id` keeps the
   auto margin and the right edge to itself. `.cs-head` (the chat send box) has no feed id, so the
   bare rule above still does the right thing there and is left alone.
   `check-panel-render`'s `gap` rule measures the distance, because "in the middle" is a position
   and no amount of reading either rule says where the browser put it. */
.panel-eyebrow .cmd-state { margin-left: 0; }
.cmd-state.ok { color: #8fe0a8; border-color: rgba(120,220,150,0.45); }
.cmd-state.warn { color: #e6b96a; border-color: rgba(230,185,106,0.45); }
.cmd-state.off { color: #e39a9a; border-color: rgba(220,120,120,0.45); }
.cmd-inner { display: flex; flex-direction: column; gap: 0.9rem; }

/* toolbar (chat-style) */
.cmd-bar { margin-bottom: 0; flex-wrap: wrap; }
.cmd-mode.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.cmd-mode-btn { border: 0; background: var(--inset); color: var(--muted); font-family: var(--ui); font-size: 0.85rem;
  padding: 0.5rem 0.95rem; cursor: pointer; }
.cmd-mode-btn + .cmd-mode-btn { border-left: 1px solid var(--line); }
.cmd-mode-btn.active { background: var(--amber); color: var(--on-amber); font-weight: 600; }
.cmd-inline { display: inline-flex; align-items: center; gap: 0.45rem; min-width: 0; max-width: 100%; }
/* The "Run as" picker is an enhanced <select>, so its button is a real element sized by the longest
   option's text — "Auto — random online player" is 254px — and every box between that button and the
   toolbar was a flex item with the default `min-width: auto`, which cannot shrink below its content.
   On a 320px phone the whole DASHBOARD scrolled 21px sideways because of it. Each link in that chain
   is allowed to shrink here; the label inside the button already truncates with an ellipsis, so the
   result is a narrower control rather than a clipped one. */
.cmd-inline .cdd { min-width: 0; max-width: 100%; flex: 1 1 auto; }
.cmd-inline .cdd-btn { min-width: 0; max-width: 100%; }
.cmd-exec-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.cmd-check { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--muted); font-size: 0.85rem; cursor: pointer; }
.cmd-check input { accent-color: var(--amber); }

.cmd-pane { display: flex; flex-direction: column; gap: 0.7rem; }
.cmd-row { display: flex; gap: 0.6rem; align-items: stretch; flex-wrap: wrap; }
.cmd-search { position: relative; flex: 1; min-width: 12rem; }
.cmd-search input { font-family: var(--mono); }
.cmd-send { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; white-space: nowrap;
  background: var(--amber); border: 1px solid var(--amber); color: var(--on-amber); font-weight: 600;
  padding: 0.6rem 1.15rem; border-radius: var(--radius); cursor: pointer; }
.cmd-send:hover { filter: brightness(1.08); }
.cmd-send:disabled { opacity: 0.5; cursor: not-allowed; filter: none; }
.cmd-send .ico { width: 15px; height: 15px; }
/* Mobile: never let Send run off the box — the search takes the row, Send wraps to a full-width line below. */
@media (max-width: 640px) {
  .cmd-search { flex: 1 0 100%; min-width: 0; }
  .cmd-send { width: 100%; }
  .cmd-build-foot .cmd-preview { flex: 1 0 100%; }
}
/* The lock / bridge-offline overlay covers the WHOLE console box (header included), not just the inner
   content — so the section is the positioning context and keeps a fixed height while gated, so the
   overlay never spills over the live server log below it. */
.cmd-console { position: relative; }
.cmd-console.locked { min-height: 220px; }
.cmd-console.locked > h2 { opacity: 0; }               /* the overlay's card carries the title instead */
.cmd-console.locked .prem-lock-wrap .cmd-inner { min-height: 160px; }

/* autocomplete dropdown */
.cmd-suggest { position: absolute; z-index: 20; top: calc(100% + 6px); left: 0; right: 0;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  box-shadow: 0 14px 34px rgba(0,0,0,0.45); max-height: 320px; overflow-y: auto; }
.cmd-sg-item { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.5rem 0.75rem; cursor: pointer; }
.cmd-sg-item:hover, .cmd-sg-item.active { background: var(--hover-bg); color: var(--amber-hi); }
.cmd-sg-name { color: var(--amber); font-family: var(--mono); font-size: 0.82rem; }
.cmd-sg-args { color: var(--muted); font-size: 0.74rem; font-family: var(--mono); }
.cmd-sg-desc { color: var(--muted); font-size: 0.74rem; margin-left: auto; text-align: right; max-width: 45%; }
.cmd-sg-arg { gap: 0.55rem; }
.cmd-sg-img { width: 30px; height: 30px; flex: none; object-fit: contain; background: rgba(255,255,255,0.05); border-radius: 4px; }
.cmd-sg-arg .cmd-sg-name { font-family: var(--ui); color: var(--text); }
.cmd-sg-arg .cmd-sg-args { margin-left: auto; }

/* interactive builder */
/* searchable command picker — type-to-filter dropdown, alphabetical */
.cmd-cmdpick { position: relative; flex: 1; display: flex; align-items: center; }
.cmd-cmdpick-ico { position: absolute; left: 0.6rem; width: 15px; height: 15px; color: var(--muted); pointer-events: none; }
.cmd-cmdpick-q { width: 100%; padding: 0.5rem 0.75rem 0.5rem 2rem; background: var(--panel-2); border: 1px solid var(--line);
  color: var(--text); font-family: var(--ui); font-size: 0.85rem; border-radius: var(--radius-md); }
.cmd-cmdpick-q:hover, .cmd-cmdpick-q:focus { border-color: var(--line-amber); outline: none; }
.cmd-cmdpick-list { position: absolute; z-index: 20; top: calc(100% + 6px); left: 0; right: 0;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  box-shadow: 0 14px 34px rgba(0,0,0,0.45); max-height: 340px; overflow-y: auto; }
.cmd-cmdpick-item { display: flex; align-items: baseline; gap: 0.6rem; padding: 0.45rem 0.75rem; cursor: pointer; }
.cmd-cmdpick-item:hover, .cmd-cmdpick-item.active { background: var(--hover-bg); }
.cmd-cmdpick-n { color: var(--amber); font-family: var(--mono); font-size: 0.82rem; }
.cmd-cmdpick-cat { color: var(--muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; margin-left: auto; }
.cmd-cmdpick-empty { padding: 0.6rem 0.75rem; color: var(--muted); font-size: 0.8rem; }
.cmd-params { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.7rem; }
.cmd-params:empty { display: none; }
.cmd-field { display: flex; flex-direction: column; gap: 0.3rem; }
.cmd-field label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.cmd-field .req { color: var(--amber); }
.cmd-field input, .cmd-field select { padding: 0.5rem 0.6rem; font-family: var(--ui); font-size: 0.86rem;
  background: var(--inset); color: var(--text); border: 1px solid var(--line); border-radius: var(--radius); outline: none; }
.cmd-field input:focus, .cmd-field select:focus { border-color: var(--amber); }
.cmd-coords { display: flex; gap: 0.4rem; }
.cmd-coords input { width: 100%; }
.cmd-pick { display: flex; gap: 0.4rem; align-items: center; }
.cmd-pick input { flex: 1; min-width: 0; }
/* One rule, two names. `.cmd-cbtn` is the coordinate-fill button and looks identical on purpose;
   declaring it in its own block would be the class-name collision that wrecked the marker card
   twice, and it needs a different CLASS only so the .cmd-pick-btn click handler — which reaches for
   its sibling <input> — does not claim it. */
.cmd-pick-btn, .cmd-cbtn { display: inline-flex; align-items: center; gap: 0.3rem; white-space: nowrap; border: 1px solid var(--line);
  background: var(--inset); color: var(--text); font-size: 0.8rem; padding: 0.45rem 0.7rem; border-radius: var(--radius); cursor: pointer; }
.cmd-pick-btn:hover, .cmd-cbtn:hover { border-color: var(--line-amber); }
.cmd-pick-btn:disabled, .cmd-cbtn:disabled { opacity: 0.5; cursor: not-allowed; }
.cmd-pick-btn .ico, .cmd-cbtn .ico { width: 14px; height: 14px; flex: none; }
.cmd-pick-thumb { width: 26px; height: 26px; object-fit: contain; border-radius: 4px; background: rgba(255,255,255,0.05); flex: none; }
.cmd-build-foot { align-items: center; }
.cmd-preview { flex: 1; font-family: var(--mono); font-size: 0.82rem; color: var(--amber); background: var(--inset);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 0.55rem 0.75rem; overflow-x: auto; white-space: nowrap; }
.cmd-preview:empty::before { content: '—'; color: var(--muted); }
/* Why Send is dead. Outside #cmd-build-params, so nothing here re-renders a field being typed in. */
.cmd-problem { margin: 0; font-size: 0.78rem; color: #e6b96a; }
.cmd-problem:empty { display: none; }

/* A coords parameter is three boxes and two fills — it never fits a 220px grid cell, so it takes
   the whole row. `1 / -1` rather than a width: the row is as wide as the grid, whatever the grid is. */
.cmd-field-wide { grid-column: 1 / -1; }
.cmd-coords-wrap { display: flex; flex-direction: column; gap: 0.4rem; }
.cmd-coords-btns { display: flex; gap: 0.4rem; flex-wrap: wrap; }

/* ── the console's third pane: the bridge's own in-game mods ───────────────── */
/* minmax(min(100%, N), 1fr), never a bare length: the panel's root font is 124%, so a bare 18rem
   would be 357px and widen the whole page on a 320px phone instead of collapsing to one column. */
.cmd-mods-target { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 0.7rem; }
.cmd-mods-spot { gap: 0.4rem; }
.cmd-mods-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 0.7rem; }
.cmd-mods-groups.busy { pointer-events: none; opacity: 0.6; }
.cmd-mods-group { display: flex; flex-direction: column; gap: 0.45rem; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--inset); padding: 0.7rem 0.8rem; min-width: 0; }
.cmd-mods-gh { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--muted); }
.cmd-mods-gh .ico { width: 14px; height: 14px; color: var(--amber); flex: none; }
/* The sentence that says WHICH field aims this group. A disabled button with nothing explaining it
   is the dead end the whole in-game action surface was built to stop producing. */
.cmd-mods-need { margin: 0; font-size: 0.75rem; line-height: 1.4; color: var(--muted); }
.cmd-mods-btns { display: flex; flex-direction: column; gap: 0.35rem; }
.cmd-mact { display: flex; align-items: center; gap: 0.45rem; text-align: left; width: 100%; min-width: 0;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--text); font-family: var(--ui);
  font-size: 0.82rem; padding: 0.45rem 0.6rem; border-radius: var(--radius); cursor: pointer;
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); --hov-line: var(--line-amber); }
.cmd-mact:hover:not(:disabled) { background: var(--hov-bg); color: var(--hov-fg); border-color: var(--hov-line); }
.cmd-mact:disabled { opacity: 0.45; cursor: not-allowed; }
.cmd-mact .ico { width: 15px; height: 15px; flex: none; color: var(--amber); }
.cmd-mact span { min-width: 0; overflow-wrap: anywhere; }

/* output log */
.cmd-output { margin: 0; background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.7rem 0.9rem; min-height: 2.4rem; max-height: 220px; overflow-y: auto;
  font-family: var(--mono); font-size: 0.78rem; line-height: 1.55; color: #cfcfd4;
  white-space: pre-wrap; word-break: break-word; }
.cmd-output:empty { display: none; }
.cmd-out-ok { color: #8fe0a8; }
.cmd-out-err { color: #e39a9a; }
.cmd-out-cmd { color: #e6b96a; }
.cmd-out-res { color: #cfd6e4; padding-left: 1.1rem; }
.cmd-out-note { color: var(--muted); padding-left: 1.1rem; font-style: italic; }
.cmd-out-t { color: var(--muted); }

/* item picker modal (interactive spawn) — reuses .pl-modal-ov shell */
.cmd-ip { width: min(680px, 100%); }
.cmd-ip-search { margin: 0.9rem 1.1rem 0; }
.cmd-ip-filters { display: flex; gap: 0.5rem; margin: 0.6rem 1.1rem 0; }
.cmd-ip-filters select { flex: 1; min-width: 0; padding: 0.4rem 0.5rem; border: 1px solid var(--line);
  border-radius: 7px; background: var(--inset); color: var(--text); font-size: 0.8rem; }
.cmd-ip-filters select:focus { border-color: var(--amber); outline: none; }
.cmd-ip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 140px), 1fr)); gap: 0.6rem;
  padding: 0.9rem 1.1rem; overflow-y: auto; }
/* Pager sits below the scrolling grid and stays put — the grid scrolls, the controls don't. */
.cmd-ip-pager { flex: none; margin: 0; padding: 0.6rem 1.1rem 0.9rem; border-top: 1px solid var(--line); }
/* `position: relative` is here and not in the record-card block at the end of this file, where the
   corner button that needs it is declared. A second bare `.cmd-ip-card { … }` two thousand lines
   away is the exact shape that shrank the map card's portrait to 20px of artwork — benign today
   because the two rules name different properties, and one refactor from not being. */
.cmd-ip-card { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; text-align: center; position: relative;
  border: 1px solid var(--line); border-radius: var(--radius); padding: 0.6rem; cursor: pointer; background: var(--inset); }
.cmd-ip-card:hover { border-color: var(--amber); }
.cmd-ip-card img { width: 56px; height: 56px; object-fit: contain; }
.cmd-ip-ph { width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; color: var(--muted); }
.cmd-ip-ph .ico { width: 30px; height: 30px; }
.cmd-ip-card .n { font-size: 0.74rem; color: var(--text); line-height: 1.25; }
/* The family line. `min-width: 0` because the card is a flex COLUMN and a long subcategory would
   push the tile wider than its grid track rather than wrapping inside it. */
.cmd-ip-card .m { min-width: 0; font-size: 0.64rem; color: var(--muted); line-height: 1.2; overflow-wrap: anywhere; }
.cmd-ip-card .id { font-size: 0.66rem; color: var(--muted); font-family: var(--mono); word-break: break-all; }

/* ── The game database screen ───────────────────────────────────────────────────────────────────
   The picker's grid, unwrapped from a modal: same card shape, same 140px track, same `min()` so a
   phone collapses to one column instead of widening the page. It is a separate block rather than a
   second selector on `.cmd-ip-*` because the picker's rules are scoped to a scrolling modal body
   (`overflow-y`, a padded pager, a corner button) and none of that is true of a full screen —
   sharing the selector would mean either the screen inherits a modal's scroll box or the picker
   loses its own. The two are one DESIGN and two boxes. */
.db-toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin: 0 0 0.9rem; }
.db-search { flex: 1 1 14rem; min-width: 0; margin: 0; }
/* ⚠ **THESE THREE `<select>`s ARE NOT WHAT A READER PRESSES, and a whole round of "fixing" went
   into them before that turned up.** The panel upgrades every native select into its own `.cdd`
   dropdown and leaves the original behind as `.cdd-native`, pinned to the wrapper's top-left at
   `width: 1px !important` — so measured in the browser the select reads 24×20 at every viewport,
   which is exactly what a control crushed to nothing looks like and is nothing of the kind. The
   real control measured 150–205px wide and 44px tall at desktop and a full-width 44px row on a
   phone, which is correct and needed no help.
   The rule is kept, small, for the window BEFORE the upgrade runs (and for a select the upgrade
   does not reach): an unstyled native select on a dark panel is a white box. It is not a layout
   rule and must not grow into one. */
.db-toolbar select { padding: 0.45rem 0.55rem; border: 1px solid var(--line);
  border-radius: 7px; background: var(--inset); color: var(--text); font-size: 0.82rem; }
.db-toolbar select:focus { border-color: var(--amber); outline: none; }
/* The upgraded dropdown IS a flex item, and `.cdd` is a bare `inline-flex` with no `min-width: 0`
   — the shape that scrolled a phone sideways by 98px inside a plugin tab and by 312px inside the
   economy picker, both recorded above. The longest real label here is "Modular building elements
   (161)", which the render check draws at a true 310px viewport, so this is a proven guard rather
   than a precaution: `.cdd-lbl` already ellipsises and all it needs is permission to shrink.
   Scoped to this toolbar, like its two siblings, because `.cdd` is used in about thirty places. */
.db-toolbar .cdd, .db-toolbar .cdd-btn { max-width: 100%; min-width: 0; }
.db-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr)); gap: 0.6rem; }
.db-card {
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem; text-align: center;
  /* ⚠ **`justify-content: flex-start` IS LOAD-BEARING AND IT IS NOT THE DEFAULT HERE.** This card
     is a `<button>`, and the global `button` rule at the top of this file states
     `justify-content: center` for the ordinary horizontal case. Turning the axis vertical with
     `flex-direction: column` turns that into VERTICAL centring — and a grid stretches every cell in
     a row to the tallest, so each card centred its own contents inside a box sized by its noisiest
     neighbour. Measured by `check-panel-render`'s `align` rule on one row of eight: the picture sat
     **15px to 34px** from the top depending only on how many lines that card's name and family
     happened to wrap to, and the code line was the same distance off the bottom. Nothing was wrong
     in any rule, no text was clipped, and the row of thumbnails a reader scans down wandered.
     `.cmd-ip-card` — the picker's grid, the same design, a `<div>` — never had it, so one design
     rendered two ways. */
  justify-content: flex-start;
  border: 1px solid var(--line); border-radius: var(--radius); padding: 0.7rem 0.55rem;
  cursor: pointer; background: var(--inset); color: var(--text); font-family: var(--ui);
  /* `--hov-*` rather than a bare `:hover { background }`: the global button rule paints every
     button amber with dark text, so changing one half of the pair on hover makes a card nobody
     can read. `check-contrast` exists because that happened about a dozen times. */
  --hov-bg: var(--inset); --hov-fg: var(--text); --hov-line: var(--amber);
}
.db-card img { width: 60px; height: 60px; object-fit: contain; }
.db-card-ph { width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; color: var(--muted); }
.db-card-ph .ico { width: 32px; height: 32px; }
/* `min-width: 0` on every text line: the card is a flex COLUMN, so a long subcategory would push
   the tile wider than its grid track instead of wrapping inside it. */
.db-card-n { min-width: 0; font-size: 0.78rem; font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.db-card-m { min-width: 0; font-size: 0.66rem; line-height: 1.25; color: var(--muted); overflow-wrap: anywhere; }
/* The code is this card's FOOTER, and `margin-top: auto` is what gives the grid its second rail.
   A name wraps to one, two or three lines and the family line wraps under it, so the only way every
   card in a row can show its code on the same line is to pin it to the bottom — the same thing
   `.pl-foot` does on the plugin cards, for the same reason and measured by the same rule. Without
   it the picture rail holds and the codes sat 15px to 54px off the bottom across one row of eight. */
.db-card-c { min-width: 0; margin-top: auto; font-size: 0.62rem; color: var(--faint); font-family: var(--mono); word-break: break-all; }

/* count / amount picker (reuses .pl-modal shell) */
.cmd-count { width: min(360px, 100%); }
.cmd-count-body { padding: 1rem 1.1rem 1.2rem; display: flex; flex-direction: column; gap: 0.9rem; }
.cmd-count-chips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
.cmd-count-chip { border: 1px solid var(--line); background: var(--inset); color: var(--text); font-size: 0.9rem;
  padding: 0.5rem 0; border-radius: 8px; cursor: pointer; font-family: var(--mono); }
.cmd-count-chip:hover { border-color: var(--amber); background: var(--amber); color: var(--on-amber); }
.cmd-count-row { display: flex; align-items: center; gap: 0.6rem; }
.cmd-count-row span { font-size: 0.78rem; color: var(--muted); flex: none; }
.cmd-count-in { flex: 1; padding: 0.45rem 0.6rem; border: 1px solid var(--line); border-radius: 7px;
  background: var(--inset); color: var(--text); font-size: 0.95rem; }
.cmd-count-go { width: 100%; justify-content: center; }

/* ── SSA Bridge admin actions (map info-card + player tools) ───────────────── */
.mp-acts { margin-top: 0.7rem; padding-top: 0.7rem; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 0.4rem; }
.mp-acts-h { flex-basis: 100%; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 0.1rem; }
.mp-acts-lock .ico { width: 12px; height: 12px; color: var(--amber); vertical-align: middle; }
.mp-act { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.76rem; cursor: pointer;
  border: 1px solid var(--line); background: var(--inset); color: var(--text); padding: 0.35rem 0.6rem; border-radius: 7px; }
.mp-act:hover:not(:disabled) { border-color: var(--amber); background: var(--amber); color: var(--on-amber); }
.mp-act .ico { width: 14px; height: 14px; }
.mp-act.danger { color: #e39a9a; }
.mp-act.danger:hover:not(:disabled) { border-color: #d98a8a; background: #d98a8a; color: #1a0808; }
.mp-act:disabled { opacity: 0.45; cursor: not-allowed; }
/* The primary action, and the only one in the stack that is separated from what is above it. The
   step is 0.2rem ON TOP of the body's own 0.45rem gap, not 0.7rem: at 0.7 the total was 1.15rem
   against 0.45rem everywhere else — two and a half times the rhythm — and it read as a gap left by
   something that failed to render rather than as a deliberate break before the important button.
   It also takes the stack's type size rather than `.mp-act`'s 0.76rem. `.mp-act` is sized for the
   small chips in the action MENU; here it is the widest, most important button in a column of four,
   and it was the only one with smaller text than its neighbours — the primary action set in the
   secondary size. */
.mp-actions-btn {
  width: 100%; justify-content: center; margin-top: 0.2rem;
  padding: 0.45rem 0.6rem; font-family: var(--ui); font-size: 0.8rem; font-weight: 600;
}
.mp-actions-btn .mp-acts-lock { width: 12px; height: 12px; }
.map-ctx-item:disabled, .map-ctx-item.locked { opacity: 0.45; cursor: not-allowed; }

/* online-player picker (teleport here, …) */
.pp-list { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.8rem 1rem; max-height: 60vh; overflow-y: auto; }
.pp-row { display: flex; align-items: center; gap: 0.5rem; text-align: left; font-size: 0.86rem; cursor: pointer;
  border: 1px solid var(--line); background: var(--inset); color: var(--text); padding: 0.55rem 0.7rem; border-radius: 8px; }
.pp-row:hover { border-color: var(--amber); background: var(--amber); color: var(--on-amber); }
.pp-row .ico { width: 15px; height: 15px; }
/* A row that will always refuse — a switch the bridge reports as `retired`. Kept in the list with
   its reason beside it rather than dropped: the value it shows is real and a list that quietly
   omits three of its rows leaves somebody hunting for them.

   BOTH HALVES of the hover pair are retuned on the element, never one of them, because the rule
   above paints every row amber-on-dark and overriding a single half is the defect `check-contrast`
   exists for. Here the pair is simply the resting pair again — a disabled row must not light up. */
.pp-row-off { cursor: default; opacity: 0.55; }
.pp-row-off:hover { border-color: var(--line); background: var(--inset); color: var(--text); }
.pp-row-why { margin-left: auto; padding-left: 0.6rem; font-size: 0.72rem; color: var(--muted);
  white-space: nowrap; }
.pp-row-off:hover .pp-row-why { color: var(--muted); }

/* A bridge-module READ on screen (player state, medical record, what the repair mod resolved).
   Reuses the .pl-modal shell. No interactive rows on purpose — this is a reading, and the actions
   that follow from it are back in the menu it was opened from, so there is no hover pair to tune. */
.bri-modal { width: min(520px, 100%); }
.bri-body { padding: 0.9rem 1.1rem 1.1rem; overflow: auto; }
.bri-list { margin: 0; display: flex; flex-direction: column; gap: 0.45rem; }
/* minmax() with a bare length widens the page instead of collapsing; min() is the house rule. */
.bri-row { display: grid; grid-template-columns: minmax(min(100%, 11rem), 0.9fr) 1.3fr; gap: 0.4rem 0.9rem;
  align-items: baseline; padding: 0.5rem 0.65rem; border: 1px solid var(--line); border-radius: 8px;
  background: var(--inset); }
.bri-row dt { margin: 0; color: var(--muted); font-size: 0.78rem; min-width: 0; overflow-wrap: anywhere; }
.bri-row dd { margin: 0; color: var(--text); font-size: 0.86rem; min-width: 0; overflow-wrap: anywhere; }
.bri-note { margin: 0.8rem 0 0; color: var(--muted); font-size: 0.78rem; line-height: 1.5; }
@media (max-width: 560px) { .bri-row { grid-template-columns: 1fr; } }

/* ── A BRIDGE ANSWER THAT IS A LIST ───────────────────────────────────────────────────────────
   The reading above is label/value pairs and stays that shape. Almost every payload the bridge
   answers with is a LIST, though — doors, gardens, stations, vehicles, squads, containers — and one
   of those used to arrive as `[object Object],[object Object]` or as a line of JSON in a single
   cell. `bridgeRender()` in app.js draws them; this is what it draws with.

   Wide only where there is a table to be wide for: `.brv-w` is set by the renderer itself, so a
   reading of eight pairs never opens in a 900-pixel box. Nothing here declares a width without a
   min() guard, and no :hover rule moves one half of a pair without the other. */
.bri-modal.brv-wide { width: min(1040px, 100%); }
.brv { display: flex; flex-direction: column; gap: 0.7rem; min-width: 0; }
.brv-sec { min-width: 0; }
.brv-h { margin: 0 0 0.4rem; font-size: 0.82rem; color: var(--amber); letter-spacing: 0.02em; }
.brv-table { min-width: 0; }
.brv-table .roster-bar { margin-bottom: 0.5rem; }
.brv-table .data-table { font-size: 0.8rem; }
.brv-table .data-table td { vertical-align: top; padding: 0.45rem 0.6rem; }
/* The panel's tables have headings short enough to keep on one line; a module's own field names are
   not, and one `INTERACTIONS DISABLED` held nowrap squeezes every other column down to its own
   minimum. Here the heading wraps and the data gets the width. */
.brv-table .data-table th { white-space: normal; }
/* A position is one thing, not three. Broken over three lines it stops reading as a place, and the
   table already scrolls sideways inside its own wrap when it needs to. */
.brv-table .gl-loc { white-space: nowrap; }
/* `overflow-wrap: anywhere` is right for a label/value row and wrong inside a table: it makes a
   column's minimum width ONE CHARACTER, so a squeezed table breaks "Protection" down the page a
   letter at a time instead of overflowing into the scroll its wrapper already provides. A NAME
   therefore breaks at word boundaries only; an identifier keeps `anywhere` — it has no spaces to
   break at and a monospace run in chunks is how code reads — with a floor so it cannot collapse. */
.brv-table .brv-nm { overflow-wrap: break-word; word-break: normal; }
.brv-table .brv-ident { display: inline-block; min-width: 7rem; }
.brv-morecell { width: 1%; white-space: nowrap; min-width: 2.6rem; }

/* A game class, named by items.js, with its artwork where the catalogue has one. `brv-derived` is
   the honest half: the item database had no record, so the word beside it was tidied out of the
   identifier rather than published by the game. */
.brv-name { display: inline-flex; align-items: center; gap: 0.35rem; min-width: 0; }
.brv-ico { width: 20px; height: 20px; object-fit: contain; flex: 0 0 auto; border-radius: 3px; }
.brv-nm { min-width: 0; overflow-wrap: anywhere; }
/* Quiet on purpose: on a door list every single name is derived, so a badge on each row would be
   the loudest thing on the screen. One legend under the table says what the mark means. */
.brv-derived { flex: 0 0 auto; font-size: 0.6rem; line-height: 1; color: var(--muted);
  vertical-align: super; text-transform: uppercase; letter-spacing: 0.04em; cursor: help; }
/* The game's own handle for a thing nothing names — a level, an actor path, a station key. Drawn as
   code because that is what it is, and because an admin types it back in to address the thing. */
.brv-ident { display: inline-block; max-width: 100%; font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.72rem; color: var(--muted); background: var(--bg-2); border: 1px solid var(--line);
  border-radius: 5px; padding: 0.12rem 0.35rem; overflow-wrap: anywhere; }

/* A record small enough to read on one line — two numbers, a pair of bounds — kept on that line
   instead of being shut inside a drawer. */
.brv-flat { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.4rem; min-width: 0; }
.brv-flat-k { color: var(--muted); font-size: 0.72rem; }
.brv-flat-sep { color: var(--faint); }

.brv-yes, .brv-no { display: inline-flex; align-items: center; gap: 0.3rem; }
.brv-yes { color: var(--online); }
.brv-no { color: var(--faint); }
.brv-yes .ico, .brv-no .ico { width: 13px; height: 13px; }

.brv-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; min-width: 0; }
.brv-chip { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.74rem;
  padding: 0.18rem 0.45rem; border: 1px solid var(--line); border-radius: 999px;
  background: var(--inset); color: var(--text); overflow-wrap: anywhere; }
.brv-chip-ok { color: var(--online); border-color: rgba(84, 201, 138, 0.3); }
.brv-chip-warn { color: var(--standby); border-color: rgba(230, 178, 58, 0.3); }
.brv-chip-bad { color: var(--alarm); border-color: rgba(255, 92, 92, 0.32); }
.brv-more { color: var(--muted); }

/* Nesting stays nesting. A sub-record and a sub-list both become one of these. */
.brv-det { border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); min-width: 0; }
.brv-det > summary { cursor: pointer; padding: 0.45rem 0.65rem; font-size: 0.8rem; color: var(--text);
  background: transparent; list-style: none; display: flex; align-items: center; gap: 0.4rem; }
.brv-det > summary::-webkit-details-marker { display: none; }
.brv-det > summary::before { content: '▸'; color: var(--amber); font-size: 0.7rem; flex: 0 0 auto; }
.brv-det[open] > summary::before { content: '▾'; }
/* Both halves in one rule — a hover that moves only the fill or only the ink is the defect
   check-contrast exists for. */
.brv-det > summary:hover { background: var(--hover-bg); color: var(--text); }
.brv-count { margin-left: auto; font-size: 0.72rem; color: var(--muted); }
.brv-det > *:not(summary) { padding: 0 0.65rem 0.6rem; }

.brv-sub { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.brv-subrow { display: grid; grid-template-columns: minmax(min(100%, 8rem), 0.7fr) 1.6fr;
  gap: 0.3rem 0.6rem; font-size: 0.78rem; padding: 0.28rem 0; border-top: 1px solid var(--line-2); }
.brv-subrow:first-child { border-top: 0; }
.brv-subname { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.brv-subtext { color: var(--muted); min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 560px) { .brv-subrow { grid-template-columns: 1fr; } }

/* A card, where the payload has a shape worth drawing rather than tabulating. */
.brv-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: 0.6rem; min-width: 0; }
.brv-card { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.7rem 0.8rem;
  border: 1px solid var(--line); border-radius: 10px; background: var(--panel-2); min-width: 0; }
.brv-card-h { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; font-size: 0.9rem;
  color: var(--text); min-width: 0; }
.brv-badge { font-size: 0.7rem; color: var(--muted); background: var(--inset);
  border: 1px solid var(--line); border-radius: 5px; padding: 0.1rem 0.32rem; }
.brv-stats { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; min-width: 0; }
.brv-stats:empty, .brv-chips:empty { display: none; }
.brv-stat { display: flex; flex-direction: column; min-width: 0; }
.brv-stat-v { font-size: 0.95rem; color: var(--text); overflow-wrap: anywhere; }
.brv-stat-l { font-size: 0.68rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }

.brv-bar { height: 6px; border-radius: 999px; background: var(--inset); border: 1px solid var(--line);
  overflow: hidden; min-width: 0; }
.brv-bar-f { display: block; height: 100%; background: var(--amber); }
.brv-bar-grow .brv-bar-f { background: var(--online); }
.brv-bar-fuel .brv-bar-f { background: var(--standby); }

.brv-plants { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 9.5rem), 1fr));
  gap: 0.4rem; min-width: 0; }
.brv-plant { display: flex; flex-direction: column; gap: 0.22rem; padding: 0.4rem 0.5rem;
  border: 1px solid var(--line); border-radius: 8px; background: var(--inset); min-width: 0; }
.brv-plant-dead { border-color: rgba(255, 92, 92, 0.32); }
.brv-plant-h { display: flex; align-items: center; justify-content: space-between; gap: 0.3rem;
  font-size: 0.8rem; color: var(--text); min-width: 0; }
.brv-plant-s { font-size: 0.72rem; color: var(--amber); }
.brv-plant-m { font-size: 0.68rem; color: var(--muted); overflow-wrap: anywhere; }

/* The machine-facing twin of a value the module also computed for a person — kept, not deleted,
   because it is still an answer; just not the one the screen was opened for. */
.brv-rawrow { opacity: 0.72; }

/* hierarchical action menu (modal or cursor popover) */
.am-modal { width: min(460px, 100%); }
.am-anchor-ov { position: fixed; inset: 0; z-index: 210; }
.am-pop { position: fixed; width: 264px; max-width: calc(100vw - 16px); background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: 0 16px 44px rgba(0,0,0,0.5); overflow: hidden; }
.am-head { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.55rem; border-bottom: 1px solid var(--line); }
.am-title { flex: 1; font-size: 0.8rem; color: var(--muted); font-family: var(--mono); }
.am-back, .am-x { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 1.15rem; line-height: 1; padding: 0 0.35rem; }
.am-back:hover, .am-x:hover { color: var(--text); }
.am-search { margin: 0.5rem 0.5rem 0; }
.am-search input { font-size: 0.84rem; }
.am-list { max-height: min(62vh, 430px); overflow-y: auto; padding: 0.35rem; }
.am-modal .am-list { max-height: min(70vh, 560px); }
/* ── the zone editor ──────────────────────────────────────────────────────────────────────────
   One screen for one zone: where it is, which rules it uses, and what those rules say. It replaces
   four one-shot prompts that between them could not touch a single rule.

   `min(100%, 13rem)` and not a bare length, which is the panel-wide rule: `minmax(13rem, 1fr)`
   inside a modal that is itself `max-width: calc(100% - 24px)` puts two columns on a 320px phone
   and clips the right one, because the card is `overflow: hidden` and the right edge is not
   scrolled to — it is gone. With the min() it collapses to one column instead of widening. */
.zed-modal { width: min(680px, 100%); display: flex; flex-direction: column; max-height: 86vh; }
.zed-body { overflow-y: auto; padding: 0.9rem 1rem 1.1rem; }
/* A write here is a full rewrite of the whole zone set and takes a round trip plus a verdict wait.
   Dimming the screen for it is not decoration: without it the second click lands on controls whose
   values are about to be replaced by the re-read. */
.zed-body.zed-busy { opacity: 0.55; pointer-events: none; }
.zed-sec + .zed-sec { margin-top: 1.1rem; border-top: 1px solid var(--line); padding-top: 0.9rem; }
.zed-sec h4 {
  margin: 0 0 0.6rem; font-family: var(--ui); font-size: 0.78rem; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--amber);
}
.zed-sec h5 {
  margin: 1rem 0 0.4rem; font-family: var(--ui); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted);
}
.zed-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); gap: 0.55rem; }
.zed-f { display: flex; flex-direction: column; gap: 0.22rem; min-width: 0; }
.zed-f > span { font-size: 0.72rem; color: var(--muted); }
/* The base rule above gives these their COLOURS; this gives them their shape, so a zone field is
   the same object as a bridge field or a catalogue field rather than a differently-proportioned box
   that happens to be the same colour. Same numbers as `.bpf-row input`, which is the closest
   neighbour — a modal form built from data — so the two cannot drift apart by being tuned
   separately. */
.zed-f input, .zed-f select {
  width: 100%; min-width: 0; padding: 0.34rem 0.5rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  font-family: var(--ui); font-size: 0.82rem; line-height: 1.5;
}
.zed-f input:focus, .zed-f select:focus { border-color: var(--amber); outline: none; }
/* Read-only, with nothing running. It has to still be READABLE — the whole argument for keeping the
   screen up with the server stopped is that an owner goes through their zones then — so it loses
   its edge and its ink, not its contents. */
.zed-f input:disabled, .zed-f select:disabled { color: var(--muted); border-color: var(--line-2); cursor: default; }
.zed-f-wide { margin-top: 0.2rem; }
.zed-note { margin: 0.5rem 0 0; font-size: 0.72rem; line-height: 1.5; color: var(--muted); }
.zed-acts { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.8rem; }
/* A row is a label and its three-way control. `min-width: 0` on the label is what stops a long
   event name pushing the select off the right edge instead of ellipsing — a flex child's default
   min-width is its content, which is the single commonest cause of the sideways scroll this
   panel's mobile rule exists for. */
.zed-rows { display: flex; flex-direction: column; gap: 0.3rem; }
.zed-row { display: flex; align-items: center; gap: 0.6rem; }
.zed-row .zed-rl { flex: 1; min-width: 0; font-size: 0.78rem; overflow-wrap: anywhere; }
.zed-row select { flex: 0 0 auto; width: 7.5rem; max-width: 45%; }
/* STACKED on a narrow screen, and this was measured rather than guessed. At a true 310px viewport
   the control takes 7.5rem and the label is left with 57px — "Fallschaden bei Spielern" in 57px is
   an ellipsis and two letters. Nothing else in this rig can see that: an ellipsis is a deliberate
   mechanism, so the overflow rules and the page-level clipped rule are all silent about it by
   design, and it was found by the `fits` rule in German at 320px.

   The breakpoint is where the label stops being readable rather than a round number: 7.5rem of
   control plus 0.6rem of gap plus the modal's own padding leaves under 90px for a sentence below
   about 420px, and a sentence is what these rows are. */
@media (max-width: 460px) {
  .zed-row { flex-direction: column; align-items: stretch; gap: 0.2rem; }
  .zed-row select { width: 100%; max-width: 100%; }
}
/* The switch the whole request was written around, given the weight it earns rather than being
   rule seven of forty-six. */
.zed-row-lead {
  padding: 0.5rem 0.6rem; border: 1px solid var(--line-2); border-radius: var(--radius-md);
  background: var(--inset);
}
.zed-row-lead .zed-rl { font-weight: 600; }
.zed-bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.5rem 0 0.2rem; }
.zed-bulk > span { font-size: 0.72rem; color: var(--muted); }
.zed-bulk select { width: 8rem; max-width: 100%; }
.zed-inst { margin-top: 0.45rem; border: 1px solid var(--line); border-radius: var(--radius-md); padding: 0.4rem 0.6rem; }
.zed-inst > summary { cursor: pointer; font-size: 0.78rem; font-weight: 600; padding: 0.15rem 0; }
/* Retuned as a PAIR on the element, never one half in a :hover rule — the stylesheet's own closing
   rule, and what `check-contrast` exists to enforce. This one is a *ghost*: no fill, the colour
   change is the whole affordance. */
.zed-inst > summary { --hov-bg: transparent; --hov-fg: var(--amber); --hov-line: currentColor; }
.zed-inst > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }

/* The shape of the menu, said out loud.
   These menus were a flat list sorted by one rule — red things last — and thirteen entries with no
   shape means reading all thirteen every time. The heading is a LABEL and never a control: no hover,
   no pointer, nothing that invites a click, because a section that looks pressable is a section
   somebody presses. It is drawn only when its section has something visible in it (`amFilter`), so
   typing in the search box never leaves a word with nothing under it. */
.am-sec {
  padding: 0.5rem 0.6rem 0.18rem; font-family: var(--ui); font-size: 0.64rem;
  font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
  pointer-events: none; user-select: none;
}
/* No rule above the first one — it would read as a divider under the title bar rather than as a
   heading over the group it names. */
.am-sec + * { margin-top: 0; }
.am-list > .am-sec:not(:first-child) { border-top: 1px solid var(--line); margin-top: 0.3rem; }
.am-item { display: flex; align-items: center; gap: 0.55rem; width: 100%; text-align: left; cursor: pointer;
  border: 0; background: transparent; color: var(--text); padding: 0.55rem 0.6rem; border-radius: 7px; font-size: 0.86rem; }
.am-item:hover:not(:disabled) { background: var(--amber); color: var(--on-amber); }
.am-item:disabled { opacity: 0.45; cursor: not-allowed; }
.am-item .ico { width: 15px; height: 15px; flex: none; }
.am-l { flex: 1; }
.am-arrow { color: var(--muted); font-size: 1rem; }
.am-item:hover:not(:disabled) .am-arrow { color: #4a3a12; }
.am-item.danger { color: #e39a9a; }
.am-item.danger:hover:not(:disabled) { background: #d98a8a; color: #1a0808; }
#map-admin:disabled { opacity: 0.5; cursor: not-allowed; }
.map-updated-badge { position: absolute; right: 8px; top: 8px; z-index: 4; font-size: 0.72rem; font-family: var(--mono);
  color: var(--muted); background: rgba(15, 16, 19, 0.72); border: 1px solid var(--line); border-radius: 6px; padding: 0.15rem 0.5rem; pointer-events: none; }

/* ── what an action did, in a form somebody can read ──────────────────────────────────────────
   Four things were wrong with one element on a timer, and every one of them lost the message: a
   second action overwrote the first (the map fires them in pairs), the timer could not be stopped,
   clicking to select text dismissed it, and there was nothing to copy with. So: a STACK, newest at
   the bottom near the map, capped at four; a failure with no timer at all; every timer paused while
   the pointer is over it; selectable text; a copy button and a close button. */
#bridge-toast.bridge-toasts {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 300;
  display: flex; flex-direction: column; align-items: stretch; gap: 0.4rem;
  width: min(560px, 92vw); pointer-events: none;
}
.bridge-toast {
  padding: 0.65rem 0.8rem 0.65rem 1rem; border-radius: 10px; font-size: 0.92rem;
  background: #1b1b1f; color: var(--text); border: 1px solid var(--line);
  box-shadow: 0 14px 40px rgba(0,0,0,0.5);
  opacity: 0; pointer-events: none; transition: opacity .18s, transform .18s;
  transform: translateY(10px); white-space: pre-wrap;
  /* Selectable on purpose: the whole point of keeping it on screen is that somebody wants what is
     in it. Dismissing used to be bound to a click anywhere on it, which is the same gesture. */
  user-select: text;
}
.bridge-toast.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
/* A toast that has timed out is FADED rather than removed — what the panel last said stays in the
   page — but it must not go on holding a gap in the stack above the map. Collapsed rather than
   `display: none`, so the text is still in the document for anything reading it back. */
.bridge-toast:not(.show) {
  max-height: 0; padding-top: 0; padding-bottom: 0; border-width: 0; overflow: hidden;
  transition: opacity .18s, transform .18s, max-height .18s .18s, padding .18s .18s;
}
.bridge-toast.ok { border-color: rgba(120,220,150,0.5); }
.bridge-toast.err { border-color: rgba(220,120,120,0.55); color: #f0c0c0; }
.bridge-toast.warn { border-color: rgba(230,200,120,0.6); color: #efdcae; }
.bt-head { display: flex; align-items: flex-start; gap: 0.5rem; }
.bt-title { font-weight: 600; flex: 1 1 auto; min-width: 0; font-size: 0.95rem; line-height: 1.35; }
.bt-tools { flex: 0 0 auto; display: flex; align-items: center; gap: 0.25rem; }
/* Ghost buttons: the pair is retuned on the ELEMENT so the hover cannot be half-applied — the rule
   the whole stylesheet follows, and `npm run check-contrast` fails on any that state one half. */
.bt-copy, .bt-x {
  --hov-bg: rgba(255,255,255,0.08); --hov-fg: var(--text); --hov-line: var(--line-amber);
  background: transparent; border: 1px solid var(--line); color: var(--muted);
  font-family: var(--ui); font-size: 0.72rem; font-weight: 600;
  padding: 0.12rem 0.45rem; border-radius: 6px; cursor: pointer; line-height: 1.5;
}
.bt-x { font-size: 0.95rem; padding: 0 0.35rem; }
/* One press for the lot, and it only appears when there IS a lot: with a single result the cross
   beside it is already the one press. Sits at the top of the stack so it does not move under the
   hand as results arrive underneath it. */
.bt-clear-all {
  --hov-bg: rgba(255,255,255,0.08); --hov-fg: var(--text); --hov-line: var(--line-amber);
  align-self: flex-end; pointer-events: auto;
  background: #1b1b1f; border: 1px solid var(--line); color: var(--muted);
  font-family: var(--ui); font-size: 0.72rem; font-weight: 600;
  padding: 0.2rem 0.6rem; border-radius: var(--pill); cursor: pointer;
}
.bt-detail {
  margin-top: 0.35rem; font-family: var(--mono); font-size: 0.83rem; color: var(--muted);
  white-space: pre-wrap; max-height: 40vh; overflow-y: auto; overflow-wrap: anywhere;
}
.bridge-toast.ok .bt-title { color: #8fe0a8; }
.bridge-toast.err .bt-title { color: #f0a8a8; }
.bridge-toast.warn .bt-title { color: #f0cf8a; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 1 (additive polish)
   Appended last so it LAYERS over the base without touching existing rules.
   Every selector targets ONLY classes/elements that already exist. Nothing is
   renamed, removed or restructured — this is pure visual polish + a11y.
   ════════════════════════════════════════════════════════════════════════ */
:root {
  /* Layered elevation for floating surfaces (subtle — preserves the flat tactical look). */
  --elev-1: 0 1px 2px rgba(0,0,0,0.4);
  --elev-2: 0 8px 24px -8px rgba(0,0,0,0.6), 0 2px 6px rgba(0,0,0,0.45);
  --elev-3: 0 22px 55px -14px rgba(0,0,0,0.72), 0 6px 16px rgba(0,0,0,0.5);
  /* Focus halo — was a 3px amber ring layered on top of a focused field's amber border, which read
     as a DOUBLE border on every text box. Neutralised: focused inputs now show only their single
     amber border, and buttons/links/tabs keep the 2px :focus-visible outline, so nothing loses its
     focus indicator. */
  --ring:   none;
}

/* Modern, consistent focus RING everywhere — keeps the high-contrast outline for
   a11y and adds a soft amber halo so focus reads as deliberate, not a default. */
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible,
[tabindex]:focus-visible,
.gs-tab:focus-visible,
.anav-btn:focus-visible,
.tab:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
  box-shadow: var(--ring);
}

/* Button busy/loading affordance — OPT-IN via aria-busy / .loading, so it can
   never affect an existing button unless code sets that state. */
button[aria-busy="true"], button.loading {
  color: transparent !important;
  pointer-events: none;
  position: relative;
}
button[aria-busy="true"]::after, button.loading::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 1em; height: 1em; border-radius: 50%;
  border: 2px solid var(--on-amber); border-top-color: transparent;
  animation: ssa-spin 0.6s linear infinite;
}
button.secondary[aria-busy="true"]::after, button.secondary.loading::after,
button.danger[aria-busy="true"]::after, button.danger.loading::after { border-color: currentColor; border-top-color: transparent; }
@keyframes ssa-spin { to { transform: rotate(360deg); } }

/* Real elevation on floating surfaces — the Grafana/Proxmox "layer that lifts". */
.amodal-box { box-shadow: var(--elev-3); }
.cdd-menu, .roster-country-menu, .lang-dd-list { box-shadow: var(--elev-2); }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 2 — navigation interactivity
   Additive only; targets confirmed classes (.admin-nav / .anav-btn). The tab
   nav previously changed only text colour on hover — add a subtle hover fill +
   a faint underline hint + smooth transitions so tabs read as interactive
   (GitHub/Grafana style). Active tab stays amber, unchanged.
   ════════════════════════════════════════════════════════════════════════ */
.admin-nav { scrollbar-width: thin; scrollbar-color: var(--line-amber) transparent; }
.anav-btn {
  transition: color .14s ease, background .14s ease, border-color .14s ease;
  border-top-left-radius: var(--radius); border-top-right-radius: var(--radius);
}
.anav-btn:hover:not(.active) {
  color: var(--text);
  background: var(--hover-bg);
  border-bottom-color: var(--line-amber);
}

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 3 — data tables (roster & co.)
   Additive; targets the existing .roster-table. Adds a readable header, a clear
   row-hover surface (rows are clickable → open a player), and a whisper-subtle
   zebra for scannability. No columns/markup/behaviour touched.
   ════════════════════════════════════════════════════════════════════════ */
.roster-table thead th {
  background: var(--bg-2);
  color: var(--muted);
  border-bottom: 1px solid var(--line-amber);
  position: sticky; top: 0; z-index: 1;
}
.roster-table tbody tr { transition: background .12s ease; }
.roster-table tbody tr:nth-child(even) { background: rgba(255,255,255,0.014); }
.roster-table tbody tr:hover { background: var(--hover-bg); }
/* keep the action button readable on a hovered row */
.roster-table tbody tr:hover .rr-admin-btn { border-color: var(--line-amber); }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 4 — form-field clarity
   The settings inputs sat on --inset (≈ page black) with a 9% border, so they
   barely read as editable. Lift them with a whisper of fill + a clearer border
   + a hover cue + the modern focus halo. Same selectors as the base rule, so it
   overrides by source order; behaviour/markup untouched. Helps every settings
   screen AND the setup wizard read as "these are fields you can edit".
   ════════════════════════════════════════════════════════════════════════ */
:root { --input-bg: rgba(255,255,255,0.025); --input-line: rgba(241, 243, 245,0.18); }
.settings-field input[type="text"],
.settings-field input[type="number"],
.settings-field input[type="password"],
.settings-field textarea,
.settings-field select,
.setup-box .settings-field input {
  background: var(--input-bg);
  border-color: var(--input-line);
  transition: border-color .14s ease, box-shadow .14s ease, background .14s ease;
}
.settings-field input:hover:not(:focus),
.settings-field textarea:hover:not(:focus),
.settings-field select:hover:not(:focus),
.setup-box .settings-field input:hover:not(:focus) { border-color: var(--line-amber); }
.settings-field input:focus,
.settings-field textarea:focus,
.settings-field select:focus,
.setup-box .settings-field input:focus,
.login-box input:focus { border-color: var(--amber); box-shadow: var(--ring); outline: none; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 5 — modals & empty/loading/error states
   Additive; targets confirmed classes only. (a) Modal: dim+blur the backdrop so
   the dialog is the clear focus, and give the header (.amodal-box h2) a hairline
   divider so title/body read as distinct zones. (b) Empty/loading/error
   placeholders (.pl-empty / .rc-empty / .cmd-cmdpick-empty) get one consistent,
   calm, centred treatment instead of three slightly different looks. No markup,
   copy, sizing or behaviour touched.
   ════════════════════════════════════════════════════════════════════════ */
.amodal { background: rgba(0,0,0,0.62); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
.amodal-box h2 {
  padding-bottom: 0.7rem;
  margin-bottom: 0.9rem;
  border-bottom: 1px solid var(--line);
  letter-spacing: 0.01em;
}
/* consistent, quiet placeholder for the three empty/loading/error slots */
.pl-empty, .rc-empty, .cmd-cmdpick-empty {
  color: var(--muted);
  text-align: center;
  letter-spacing: 0.01em;
}
.pl-empty { min-height: 4.5rem; display: flex; align-items: center; justify-content: center; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 6 — interactive card affordance
   The plugin cards already DECLARE a border/shadow transition but never had a
   :hover target, so they sat inert. Give them (and the clickable squad-member
   chips in the player detail) a real hover: amber-hinted border + a whisper of
   lift — the "cards that respond" feel. Purely visual; .pl-card.on already owns
   the amber border for active plugins, so this just adds the shadow on hover.
   ════════════════════════════════════════════════════════════════════════ */
.pl-card:hover { border-color: var(--line-amber); box-shadow: var(--elev-1); }
.pd-member { transition: border-color .14s ease, background .14s ease; }
.pd-member:hover { border-color: var(--line-amber); background: var(--hover-bg); }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 7 — cohesive themed scrollbars
   The one un-themed element left across the whole panel was the default chunky
   grey OS scrollbar (visible on the page, modals, textareas, the roster wrapper,
   dropdowns). Give every scroll area a thin, dark, amber-on-hover scrollbar so
   scrolling reads as part of the same tactical surface — the Grafana/Proxmox
   "custom scrollbars everywhere" cohesion. Firefox via scrollbar-*, Chrome (the
   primary target) via ::-webkit-scrollbar. Purely cosmetic; .admin-nav keeps its
   own amber track (more specific rule wins).
   ════════════════════════════════════════════════════════════════════════ */
* { scrollbar-width: thin; scrollbar-color: rgba(241, 243, 245,0.22) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(241, 243, 245,0.16);
  border-radius: 6px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 106, 26,0.5);
  background-clip: padding-box;
}
::-webkit-scrollbar-corner { background: transparent; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 8 — mobile header menu (hamburger)
   On phones/small tablets the header's manager buttons (Update/Restart/Shutdown
   + language + Field Console + Logout) wrapped onto multiple rows and ate a lot
   of vertical space. Collapse them behind a hamburger that opens a proper drawer
   under the header. Desktop is 100% unchanged (the hamburger is display:none and
   .header-actions keeps its normal inline flex row). Wired by a tiny isolated
   inline script (toggles header.menu-open); markup added: #mobile-menu-btn + an
   id on the existing .header-actions. No app.js/API/behaviour touched — the same
   buttons, just relocated into a drawer on small screens.
   ════════════════════════════════════════════════════════════════════════ */
.mobile-menu-btn { display: none; }              /* desktop: hidden */
.mobile-menu-btn .ico { width: 22px; height: 22px; }
.mobile-menu-btn .mm-close { display: none; }    /* show X only when open */

@media (max-width: 760px) {
  /* brand + hamburger share one row; the actions drop into an absolute drawer */
  header { flex-wrap: nowrap; position: relative; }
  .mobile-menu-btn {
    display: inline-flex; align-items: center; justify-content: center;
    /* 44, not the 42 this said until the touch floor was driven — it is the control every other
       screen on this panel is reached through on a phone, and it was two pixels short. */
    flex: 0 0 auto; padding: 0.5rem; width: 44px; height: 44px;
  }
  header.menu-open .mobile-menu-btn { border-color: var(--line-amber); color: var(--amber-hi); }
  header.menu-open .mobile-menu-btn .mm-open { display: none; }
  header.menu-open .mobile-menu-btn .mm-close { display: inline-flex; }

  .header-actions {
    display: none;                 /* closed by default */
    position: absolute;
    top: calc(100% + 6px);
    right: 0.75rem; left: 0.75rem;
    flex-direction: column;
    align-items: stretch;
    gap: 0.45rem;
    padding: 0.7rem;
    background: var(--panel);
    border: 1px solid var(--line-amber);
    border-radius: var(--radius);
    box-shadow: var(--elev-3);
    z-index: 200;
  }
  header.menu-open .header-actions { display: flex; animation: modalIn 0.16s ease; }
  .header-actions > button,
  .header-actions > a.secondary {
    width: 100%; justify-content: flex-start;
    padding-top: 0.6rem; padding-bottom: 0.6rem;
  }
  /* the thin vertical divider becomes a horizontal rule when stacked */
  .header-actions .header-sep { width: 100%; height: 1px; align-self: auto; margin: 0.2rem 0; }
  /* language picker + its trigger fill the drawer width */
  #admin-lang-wrap, #admin-lang-wrap .cdd, #admin-lang-wrap .cdd-btn,
  #admin-lang-wrap .lang-dd, #admin-lang-wrap .lang-dd-btn { width: 100%; }

  /* ══ …AND THE MENU OPENS THE NAVIGATION, WHICH IS WHAT THE WORD MEANS ═══════════════════════════
     The block above collapsed the manager buttons behind a hamburger and left the TABS where they
     were: a 1020px strip inside a 310px viewport, swipe-only, with no scrollbar (phones paint none
     until you drag) and nothing on screen saying the other three screens of it exist. So the main
     navigation of a paid product was a blind gesture, while the one control labelled "Menu" opened
     Update / Restart / Shut down / Logout — four manager actions and not one destination.

     ⚠ THE FIELD CONSOLE'S ANSWER IS NOT THE ANSWER HERE, and the difference is arithmetic rather
     than taste. That bar is eleven peers and about one screen of scrolling either way, so a cue
     that it scrolls plus the chosen tab scrolled into view is enough to find any of them. This one
     is nine entries in 1020px — over three screens — and two of those nine are DROPDOWNS carrying
     Settings' six screens and the plugin list, which no amount of scrolling reveals to somebody who
     does not already know they open. A cue tells a reader there is more; it does not get them to
     Premium from Dashboard in one gesture, and that is what an owner on a phone needs.

     So the strip gets the cue AND the hamburger gets the tabs: one tap opens every destination at
     once, in a two-column grid that fits on the screen without scrolling — nine entries in five
     rows rather than nine — with the manager actions kept below them, because the drawer's top is
     where a thumb lands and Shut down is not what should be there. The strip stays exactly as it
     is for a quick sideways flick between neighbours.

     ⚠ THE CONTAINING BLOCK MOVES FROM <header> TO .topbar, and that one line is what puts the
     actions BELOW the tabs rather than over them. `.header-actions` is inside <header> and the nav
     is its SIBLING, so no ordering property can put one after the other — they are in different
     parents. `.topbar` is `position: sticky`, which is a positioned element, so dropping <header>
     to static while the menu is open re-resolves the drawer's `top: 100%` against the whole topbar,
     which by then includes the expanded nav. Nothing inside <header> is absolutely positioned, so
     that costs nothing else. */
  header.menu-open { position: static; }
  /* ⚠ AND THE DRAWER HAS TO BE ABLE TO END ON THE SCREEN. Once the tabs are above it the actions
     start about 310px down, and six of them plus the language picker is another 320 — which is off
     the bottom of a 667px phone, on a panel whose topbar is STICKY, so scrolling the page does not
     bring Logout back. A percentage max-height resolves against the containing block's height, and
     the containing block is now the whole topbar, so `100vh - 100% - 12px` is exactly "the rest of
     the screen" while the topbar is stuck at the top, which is when this is open. `dvh` second, so
     a browser that has it uses the height a phone really has and one that does not ignores the
     line. */
  header.menu-open .header-actions {
    max-height: calc(100vh - 100% - 12px);
    max-height: calc(100dvh - 100% - 12px);
    overflow-y: auto;
  }
  header.menu-open ~ .admin-nav {
    display: grid;
    /* Never a bare length in minmax — it collapses to one column instead of widening the page. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 128px), 1fr));
    gap: 0.3rem;
    overflow: visible;
    padding: 0.55rem 0.75rem;
    border-bottom-width: 2px;
  }
  header.menu-open ~ .admin-nav .anav-btn {
    justify-content: flex-start;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel-2);
    padding: 0.6rem 0.7rem;
  }
  header.menu-open ~ .admin-nav .anav-btn.active {
    border-color: var(--line-amber); color: var(--amber);
  }
  /* The two dropdown entries are <span> wrappers round their link. As grid items they have to hand
     the whole cell to the link inside, or Settings and Plugins are half-width cells beside
     full-width siblings and the row stops reading as a list. */
  header.menu-open ~ .admin-nav .anav-dd { display: flex; }
  header.menu-open ~ .admin-nav .anav-dd > .anav-btn { flex: 1 1 auto; }

  /* ── and the closed strip says it scrolls ─────────────────────────────────────────────────────
     The same two-layer trick the field console uses, 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; the moment the content moves the shadow is
     uncovered. Both are BACKGROUND layers, so they paint behind the tabs — `elementFromPoint` still
     answers with the tab, which is what the rig's `ontop` rule reads. Switched off while the menu
     is open, where there is nothing to scroll. */
  .admin-nav {
    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;
  }
  header.menu-open ~ .admin-nav { background-image: none; }
}

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 9 — styled toast notifications
   Replaces the ugly native browser alert() (the "Windows/Google" bar at the top
   of the page) with an on-brand toast that stacks top-right. Wired by overriding
   window.alert in the isolated inline script — every existing alert() call site
   is unchanged, they just render as a proper toast now. Error-ish messages get a
   red rail, notices get the amber rail. z-index above modals/dropdowns.
   ════════════════════════════════════════════════════════════════════════ */
.ssa-toast-wrap {
  position: fixed; top: 1rem; right: 1rem; z-index: 4000;
  display: flex; flex-direction: column; gap: 0.5rem;
  width: min(380px, calc(100vw - 2rem));
  pointer-events: none;
}
.ssa-toast {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: 0.6rem;
  background: var(--panel);
  border: 1px solid var(--line-amber);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius);
  padding: 0.7rem 0.8rem;
  box-shadow: var(--elev-3);
  color: var(--text); font-size: 0.88rem; line-height: 1.45;
  animation: ssaToastIn 0.2s ease;
}
.ssa-toast.err { border-left-color: var(--alarm); }
.ssa-toast.err .ssa-toast-ic { color: var(--alarm); }
.ssa-toast.ok { border-left-color: var(--online); }
.ssa-toast.ok .ssa-toast-ic { color: var(--online); }
.ssa-toast-ic { flex: 0 0 auto; width: 18px; height: 18px; color: var(--amber); margin-top: 1px; }
.ssa-toast-msg { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
.ssa-toast-x {
  flex: 0 0 auto; background: none; border: 0; color: var(--muted);
  cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.1rem;
}
.ssa-toast-x:hover { color: var(--text); }
.ssa-toast.hide { animation: ssaToastOut 0.2s ease forwards; }
@keyframes ssaToastIn { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
@keyframes ssaToastOut { to { opacity: 0; transform: translateX(18px); } }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 10 — Live Map marker hover
   The marker hover was a single hard 2px outline sitting right on the dot edge,
   with no transition — barely legible against the busy map and abrupt. Give it a
   floating ring (outline-offset) + a soft amber glow halo + a smooth transition,
   and a slightly larger hover hit-area, so a hovered marker clearly "lifts" off
   the map. Colours/positions/behaviour otherwise unchanged.
   ════════════════════════════════════════════════════════════════════════ */
.map-mk .mk-dot { transition: box-shadow .12s ease; }
.map-mk .mk-dot::before { inset: -10px; }            /* a touch more forgiving hover target */
.map-mk:hover, .map-mk.mk-selected { z-index: 6; }
/* Tight amber ring HUGGING the icon (no gap) + soft glow. !important so it wins over
   the per-status green/red rings (which have higher specificity). Applies on hover AND
   while selected — the clicked marker stays lit so you can see what the open popover is for. */
.map-mk:hover .mk-dot,
.map-mk.mk-selected .mk-dot {
  outline: none !important;
  box-shadow: 0 0 0 2px var(--amber-hi), 0 0 9px 2px rgba(255, 106, 26,0.6), 0 1px 4px rgba(0,0,0,0.7) !important;
}
/* keep the selected marker's name label visible while its popover is open */
.map-mk.mk-selected .mk-label { display: block; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 11 — Live Map LEFT SIDEBAR polish
   The layer cards + entity-list rows hovered to a flat, washed grey
   (rgba(255,255,255,.06)) with no accent — off-identity and bland against the
   amber tactical panel. Give every interactive sidebar row a crisp on-brand
   hover: an inset amber left-rail + the name going amber-hi + a smooth
   transition, so hovering reads clearly and matches the rest of the panel.
   Colours/counts/behaviour otherwise unchanged.
   ════════════════════════════════════════════════════════════════════════ */
.map-layer, .ml-row, .map-filters-head, .ml-sec-h {
  transition: background .12s ease, box-shadow .12s ease, color .12s ease;
}
.map-layer:hover, .ml-row:hover {
  background: var(--hover-bg);
  box-shadow: inset 2px 0 0 var(--amber);
}
.ml-row:hover .mlr-name { color: var(--amber-hi); }
.map-layer:hover .ml-name { color: var(--amber-hi); }
.map-filters-head:hover { color: var(--amber-hi); }
.map-filters-head:hover .ico { color: var(--amber-hi); }
/* offline rows still get the amber name on hover (override the muted default) */
.ml-row.off:hover .mlr-name { color: var(--amber); }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 12 — dropdown hover + collapsible map lists
   (a) .cdd-item hover was amber TEXT on a barely-lighter grey → low contrast,
   reads as muddy/"dark". Make it a clear solid-amber highlight bar with dark
   text, like a proper dropdown selection. Applies to every custom dropdown
   (map layer sub-filters, language, executor, country…) for consistency.
   (b) Live Map entity lists (Players/Vehicles/Chests/Storage/Bases) become
   collapsible by clicking their section header — same affordance as the
   "Layers & filters" panel. Chevron + hidden rows when collapsed; wired by the
   isolated inline script (state survives the map's periodic re-render).
   ════════════════════════════════════════════════════════════════════════ */
/* Both halves from the element's own pair, so a more specific rule cannot take one and leave the
   other — which is exactly what `.cdd-item.cdd-check.on` did, giving near-black text on the dark
   panel for any row you had ticked. The defaults live on `.cdd-item` itself. */
.cdd-item:hover, .rc-item:hover {
  background: var(--hov-bg, var(--amber));
  color: var(--hov-fg, var(--on-amber));
}

/* collapsible entity-list sections */
.ml-sec-h { cursor: pointer; user-select: none; }
.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-h .ml-sec-n { margin-left: auto; }        /* keep count pinned right, chevron after it */
.ml-sec.collapsed .ml-sec-h::after { transform: rotate(-90deg); }
.ml-sec.collapsed .ml-row,
.ml-sec.collapsed .ml-empty { display: none; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 13 — custom-dropdown TRIGGER hover fix
   The global `button:hover` (amber bg + dark on-amber text) was hijacking the
   .cdd-btn dropdown triggers (e.g. the map layer "All (85)"). Context rules like
   `.map-layers .cdd-btn{background:panel-2}` kept the bg DARK but the dark text
   from button:hover still applied → dark-on-dark, unreadable on mere hover.
   Give .cdd triggers a proper dropdown hover instead: keep the dark surface,
   light label, amber border + amber caret as the cue. Fixes every custom
   dropdown trigger (map filters, language, executor, country…).
   ════════════════════════════════════════════════════════════════════════ */
.cdd-btn:hover {
  background: var(--panel-2);
  border-color: var(--line-amber);
  color: var(--text);
}
.cdd-btn:hover .dd-caret,
.cdd-btn:hover .cdd-lbl { color: var(--amber-hi); }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 14 — styled confirm / prompt dialogs
   Replaces the native browser confirm()/prompt() (the "ugly window" on header
   Restart/Shutdown and the player admin actions) with an on-brand modal. Driven
   by window.ssaConfirm()/ssaPrompt() (Promise-based) in the inline script; the
   app.js call sites are converted to `await` these. Reuses the existing button
   system (primary / .danger / .secondary) so it matches everything else.
   ════════════════════════════════════════════════════════════════════════ */
.ssa-dialog-back {
  position: fixed; inset: 0; z-index: 5000;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.62); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  padding: 1rem;
}
.ssa-dialog {
  width: min(440px, 100%);
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius);
  box-shadow: var(--elev-3); padding: 1.3rem 1.4rem; animation: modalIn 0.18s ease;
}
.ssa-dialog h3 {
  margin: 0 0 0.8rem; font-size: 1.05rem; color: var(--text); letter-spacing: 0.01em;
  padding-bottom: 0.7rem; border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: 0.5rem;
}
.ssa-dialog h3 .ico { width: 18px; height: 18px; color: var(--amber); flex: none; }
.ssa-dialog.danger h3 .ico { color: var(--alarm); }
.ssa-dialog-msg { margin: 0 0 1rem; color: var(--text); font-size: 0.9rem; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
.ssa-dialog-input {
  width: 100%; margin: 0 0 1rem; padding: 0.55rem 0.7rem; box-sizing: border-box;
  background: var(--input-bg, rgba(255,255,255,0.025)); border: 1px solid var(--input-line, rgba(241, 243, 245,0.18));
  border-radius: var(--radius); color: var(--text); font-family: var(--ui); font-size: 0.9rem;
}
.ssa-dialog-input:focus { border-color: var(--amber); box-shadow: var(--ring); outline: none; }
.ssa-dialog-btns { display: flex; justify-content: flex-end; gap: 0.5rem; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 15 — Plugins: search + restart clarity
   Adds a compact search box to the Plugins toolbar (reuses .player-search) and
   turns the section note into a scannable "what to restart" callout so it's
   obvious that UE4SS mods apply on a SERVER restart while manager plugins apply
   instantly (and why a just-disabled mod can still read as online).
   ════════════════════════════════════════════════════════════════════════ */
/* In the toolbar the search must not devour the row like it does on the roster
   bar — cap it, and let it collapse to full width only when the bar wraps. */
.pl-search { flex: 0 1 220px; min-width: 150px; padding: 0.4rem 0.6rem; transition: border-color 0.12s ease, box-shadow 0.12s ease; }
.pl-search input { font-size: 0.9rem; }
/* Focus the whole search pill — not the inner <input>. The global input:focus-visible rule
   otherwise paints a second amber frame INSIDE the pill (2px outline + a 3px --ring box-shadow
   halo) on top of the pill's own amber border — a double border. Kill BOTH on the inner input so
   only the single pill border shows. Applies to every .player-search box. */
.player-search:focus-within { border-color: var(--amber); box-shadow: var(--focus-ring); }
.player-search input:focus-visible { outline: none; box-shadow: none; }
/* The restart-guidance note reads as a proper callout box, not a bare rule. */
.pl-note {
  background: rgba(255, 106, 26, 0.06);
  border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  border-radius: var(--radius);
  padding: 0.75rem 0.95rem;
  line-height: 1.6;
}
.pl-note b { color: var(--text); font-weight: 600; }
@media (max-width: 720px) { .pl-search { flex-basis: 100%; } }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 17 — click-to-sort table headers
   Column headers on the Players / Chat / Game-log tables sort by that column
   (replacing the old "Sort" dropdowns). A faint ↕ marks a sortable column; the
   active one shows an amber ▲/▼.
   ════════════════════════════════════════════════════════════════════════ */
.data-table th.th-sort { cursor: pointer; user-select: none; transition: color 0.12s ease, background 0.12s ease; }
.th-sort .th-sort-in { display: inline-flex; align-items: center; gap: 0.3rem; }
.th-sort .th-arrow { font-size: 0.74em; line-height: 1; color: var(--amber); }
.data-table th.th-sort:not(.active) .th-arrow::after { content: '↕'; color: var(--muted); opacity: 0.4; }
.data-table th.th-sort:hover { color: var(--text); background: rgba(255, 106, 26, 0.06); }
.data-table th.th-sort:not(.active):hover .th-arrow::after { opacity: 0.75; }
.data-table th.th-sort.active { color: var(--amber-hi, var(--amber)); }
.data-table th.th-sort:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 18 — stable table columns
   Fixed layout so column widths stay put across sorting and paging (auto layout
   re-measured each page's content, which made columns jump/resize). Long cells
   truncate with an ellipsis; message/log cells wrap. Widths are scoped per table
   (roster / chat / game-log all share the .roster-table class).
   ════════════════════════════════════════════════════════════════════════ */
.data-table.roster-table { table-layout: fixed; width: 100%; }
.roster-table td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ⚠ **A HEADER IS THE ONE CELL THAT MUST NEVER BE CUT, AND THIS RULE CUT THEM.** A truncated VALUE
   is a value you can go and read somewhere else; a truncated CAPTION is a column nobody can
   identify, so every figure under it is unreadable too. Measured on a phone: `Result` came back as
   72% of itself in 46px, `Admin` as 89% in 51px, `When` as 94% — three columns of a table about who
   did what, none of them named.
   Captions are one or two short words, so wrapping costs a line and never more, and the row of them
   is one row per table rather than one per record. The VALUES keep their ellipsis: a dense table is
   what that is for, and the answer for a column that cannot fit its values is to drop the column,
   which is what every table here now does at the widths where it stops fitting. */
.roster-table th {
  white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere;
  vertical-align: bottom;
}
.roster-table td.chat-msg, .roster-table td.gamelog-line { white-space: normal; overflow: visible; text-overflow: clip; }
/* ...and the timestamp beside them, which needed the same specificity to win. A bare `.chat-ts`
   rule loses to `.roster-table td`, so the nowrap it was meant to lift stayed on and the time
   went on being cut to 89% of itself. Specificity is why a fix can change nothing on screen. */
/* ⚠ A `.roster-table td.chat-ts` rule that lifted the nowrap USED TO BE HERE and it made things
   worse. The chat table sizes its first three columns with `width: 1%` PLUS `white-space:
   nowrap`, which is the shrink-to-fit idiom: the nowrap is what gives the column a content
   width to shrink to. Without it the column really is 1% and the timestamp spills out of it.
   The cut it was aimed at is a table squeezed too narrow to be a table, and that is fixed by
   the breakpoint below rather than by a property on the cell. */
.audit-table td.c-details.audit-details { white-space: normal; overflow: visible; text-overflow: clip; }

/* Players — widths by CLASS, never :nth-child. The old rules were positional, so each column kept
   the width of whatever used to sit in its slot once columns were inserted; that is how a
   three-digit ID ended up claiming 18% of the table.
   The layout is `fixed`, so content does not size anything: EVERY column must state a width, or it
   collapses to nothing while the rest divide the space up between them. The numbers below were
   measured in a browser against this server's real roster (longest name, longest squad, 17-digit
   Steam IDs, six-figure balances) rather than estimated. Squad / Discord / Name are the ones
   allowed to ellipsis — everything else fits its widest real value.
   Percentages, NOT rem: the panel scales its root font size (1rem measures ~19.8px here, not 16),
   so rem widths came out about a quarter too wide and pushed the table into a scrollbar. A
   percentage resolves against the table itself and is immune to that. */
/* Tighter cells than the other tables: at 0.7rem a side, padding alone ate 392px of a 1400px table
   across 14 columns — a quarter of the width before a single value was drawn. 0.45rem still spent
   249px of a 1313px table on nothing; 0.3rem spends 166px. */
.roster-table:not(.chat-table) th,
.roster-table:not(.chat-table) td { padding-left: 0.3rem; padding-right: 0.3rem; }
/* …and the same argument applied to the HEADERS, which are the binding constraint on half these
   columns: `Playtime`, `Money`, `Gold`, `Fame`, `Discord` and `ID` each need more room for their
   own caption than for any value under it. At 0.06em an eleven-character header spends 9px on
   letter-spacing alone. The shared `.data-table th` keeps its 0.06em; only this table, which has
   fourteen columns to fit rather than three, tightens it. */
.roster-table:not(.chat-table) th { letter-spacing: 0.02em; }

/* THE PERCENTAGES ARE A BUDGET, AND A BUDGET IS ONLY MEANINGFUL AGAINST A TABLE WIDTH.
   That is what went stale: they were measured on a 1920 monitor, where all but one of them held to
   this day, and a percentage that fits at 1817px of table does not fit at 1313px. On a 1440 monitor the
   ID column resolved to 42px and printed `1…` for `100` on every row, both timestamps printed the
   date and lost the time, the Steam ID lost its last digit, and six headers ellipsised their own
   sort arrow. Nothing was wrong with any rule; the width they were measured against had gone.
   So the widths below are derived at the NARROWEST viewport of each column set (the media queries
   under .roster-table further up decide which columns are on screen at all), and the ladder there
   is derived from these numbers rather than picked. `npm run check-panel-render` measures the real
   boxes at seven widths and fails on a value that had to ellipsis to fit — see the `players · …`
   views, which is the only place this can be checked at all. */
.roster-table:not(.chat-table) th.rr-status-h { width: 2.4%; }   /* just a dot — and it was clipping THAT */
.roster-table:not(.chat-table) th.c-name     { width: 9.4%; }
.roster-table:not(.chat-table) th.c-uid      { width: 3.6%; }    /* in-game id: a few digits */
.roster-table:not(.chat-table) th.c-steam    { width: 12.9%; }   /* 17 digits, monospace — must not clip */
.roster-table:not(.chat-table) th.c-playtime { width: 7.6%; }
.roster-table:not(.chat-table) th.c-fame     { width: 5.3%; }
.roster-table:not(.chat-table) th.c-money    { width: 6.2%; }    /* "163,314" */
.roster-table:not(.chat-table) th.c-gold     { width: 5.4%; }
.roster-table:not(.chat-table) th.c-squad    { width: 6.9%; }    /* long names ellipsis */
.roster-table:not(.chat-table) th.c-ip       { width: 11.3%; }   /* "255.255.255.255" is 15 monospace digits */
.roster-table:not(.chat-table) th.c-discord  { width: 7.2%; }    /* long names ellipsis */
/* The admin-actions button inherits the generic button padding (~21px a side), which made it 44px
   wide inside a 41px cell — it overflowed and read as shoved off to the right. It is an icon
   button; size it like one. */
.roster-table td.rr-act .rr-admin-btn { padding: 0.25rem 0.35rem; min-width: 0; }
/* That cell holds a button, not text — the shared ellipsis rule was appending dots next to it. */
.roster-table td.rr-act { overflow: visible; text-overflow: clip; }
/* Both were 1.6% narrower and both CUT their own text, measured by `check-panel-render`'s `fits`
   rule the moment the panel stopped formatting dates in the OPERATING SYSTEM's locale. That is
   worth writing down because the widths were not wrong before and are not wrong now: an English
   `7/5/26, 10:08 AM` is genuinely wider than the Czech `05.07.26 10:08` the OS was handing back,
   so a column sized against whatever locale the developer's machine happened to be in was sized
   against the wrong string. The 3.2% comes out of NAME, which is the only column here with no width of its own — it takes
   whatever is left, so it is where a widened neighbour is paid for, and it is first on the list of
   the three the block above allows to ellipsis. */
.roster-table:not(.chat-table) th.c-login    { width: 11.2%; }
.roster-table:not(.chat-table) th.c-logout   { width: 10.9%; }
.roster-table:not(.chat-table) th.rr-act-h   { width: 2.9%; }

/* Chat — 4 columns (Time, Channel, Player, Message) */
.chat-table:not(.gamelog-table) th:nth-child(1) { width: 11.5rem; }
.chat-table:not(.gamelog-table) th:nth-child(2) { width: 6.5rem; }
.chat-table:not(.gamelog-table) th:nth-child(3) { width: 18%; }

/* Game log — 3 columns (Time, Category, Log entry). Fit Time to its content (the full timestamp is
   only ~150px wide) and trim the oversized right padding, so Category sits right next to it instead
   of after a dead gap; the log entry then gets all the remaining room. */
.gamelog-table th:nth-child(1), .gamelog-table td:nth-child(1) { width: 174px; white-space: nowrap; padding-right: 0.35rem; }
.gamelog-table th:nth-child(2) { width: 8rem; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 19 — Log Viewer: raw-line reveal
   A per-entry "raw" toggle reveals the ORIGINAL log line (nothing is lost when
   the enriched view folds coordinates/blobs away), plus styling for the session
   picker. Used in the Log Viewer and the player card's Logs tab.
   ════════════════════════════════════════════════════════════════════════ */
.gl-raw-btn {
  font: inherit; font-size: 0.72em; cursor: pointer; margin-left: 0.4rem; vertical-align: middle;
  padding: 0.05rem 0.4rem; border-radius: 4px;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.gl-raw-btn:hover, .gl-raw-btn.on { border-color: var(--line-amber); color: var(--amber); }
.gl-raw {
  margin-top: 0.35rem; padding: 0.45rem 0.6rem;
  background: #101012; border: 1px solid var(--line); border-radius: 6px;
  font-family: var(--mono); font-size: 0.8rem; color: var(--muted);
  white-space: pre-wrap; word-break: break-word;
}
/* Player card → Logs tab: a compact filter bar + a scrollable list, matching the Log Viewer. */
.pd-logs-body { min-height: 4rem; }
.pdl-bar { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; flex-wrap: wrap; }
.pdl-bar .pdl-search-wrap { flex: 1 1 180px; min-width: 140px; }
.pdl-bar .pdl-count { margin-left: auto; color: var(--muted); font-size: 0.82rem; white-space: nowrap; }
.pdl-list { max-height: 46vh; overflow: auto; }
.pdl-list .table-wrap { margin-top: 0; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 20 — Log Viewer: entity icons + item
   preview. Small SVG markers show WHAT each clickable thing is (user / location
   / weapon / box / vehicle) without recolouring the line, and item/vehicle codes
   open a game-DB preview (icon + name). Also fixes the player-card Logs table so
   its time column isn't collapsed.
   ════════════════════════════════════════════════════════════════════════ */
.gl-ic { width: 0.95em; height: 0.95em; vertical-align: -0.14em; margin-right: 0.22rem; color: var(--muted); opacity: 0.7; flex: none; }
.gl-player:hover .gl-ic, .gl-loc:hover .gl-ic, .gl-item:hover .gl-ic { color: var(--amber); opacity: 1; }
/* clickable item/vehicle/weapon code → dashed underline (distinct from players' solid) */
.gl-item { color: inherit; cursor: pointer; text-decoration: underline dashed; text-decoration-color: rgba(255, 106, 26, 0.4); text-underline-offset: 2px; }
.gl-item:hover { color: var(--amber-hi); text-decoration-color: var(--amber); }
/* item preview popover */
.gl-preview { position: absolute; z-index: 5000; max-width: 250px; padding: 0.6rem 0.7rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: 8px; box-shadow: var(--elev-3); }
.gl-preview-body { display: flex; gap: 0.65rem; align-items: center; }
.gl-preview img { width: 56px; height: 56px; object-fit: contain; background: var(--inset); border-radius: 6px; flex: none; }
.gl-preview-t { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.gl-preview-t b { color: var(--text); font-size: 0.9rem; }
.gl-preview-t .mono { font-size: 0.72rem; word-break: break-all; }
/* player-card Logs tab: content-sized columns so the time isn't crushed to 10px */
.pdl-list .data-table { table-layout: auto; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 21 — Log Viewer + player modal polish
   Fixes: icons were too dark to see; Steam IDs rendered a size smaller than the
   rest of the line; the item preview was cramped/dark; the player modal was
   tight for reading the Logs tab.
   ════════════════════════════════════════════════════════════════════════ */
/* Entity icons — bright enough to read, on-brand amber; shape signals the type. */
.gl-ic { width: 0.92em; height: 0.92em; vertical-align: -0.13em; margin-right: 0.26rem; color: var(--amber); opacity: 0.95; flex: none; }
/* a touch more breathing room between log rows so the icons + text don't feel cramped */
.gamelog-line { padding-top: 0.5rem; padding-bottom: 0.5rem; line-height: 1.5; }
.gl-player:hover .gl-ic, .gl-loc:hover .gl-ic, .gl-item:hover .gl-ic { color: var(--amber-hi); opacity: 1; }
/* One consistent type size across the whole log line — no shrunk Steam IDs. */
.gamelog-line, .gamelog-line a, .gamelog-line .gl-sid { font-size: 0.85rem; }
.gl-sid { font-size: inherit; }
/* Item preview — larger image on a light plate, clearer text. */
.gl-preview { max-width: 290px; padding: 0.85rem 0.9rem; }
.gl-preview-body { gap: 0.85rem; }
.gl-preview img { width: 84px; height: 84px; padding: 5px; background: rgba(255, 255, 255, 0.07); border: 1px solid var(--line); }
.gl-preview-t b { font-size: 1rem; line-height: 1.2; }
.gl-preview-t .mono { font-size: 0.74rem; }
/* Player modal — bigger, and the Logs list gets real room to read. */
.amodal.wide .amodal-box { max-width: 1120px; max-height: 92vh; }
.pdl-list { max-height: 58vh; }
.pdl-bar { margin-bottom: 0.7rem; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 22 — Log Viewer: per-type category chip
   Every row's category chip carries its log-type icon (skull=kills, coins=economy,
   shield=raid, car=vehicles, lock=bunkers…) + a friendly label / sub-category, so
   each category reads at a glance in the viewer and the player card's Logs tab.
   ════════════════════════════════════════════════════════════════════════ */
.gl-cat { display: inline-flex; align-items: center; gap: 0.3rem; max-width: 100%; vertical-align: middle; }
.gl-type-ic { width: 0.95em; height: 0.95em; flex: none; color: currentColor; opacity: 0.9; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 23 — Log Viewer: category never overflows
   The category column is a fixed 9rem; long tags (VehicleInactiveTimerReached,
   OutOfInteractionRange…) must ellipsize inside the pill, not spill into the entry
   column. Full text stays available via the cell's title tooltip.
   ════════════════════════════════════════════════════════════════════════ */
.gamelog-table td:nth-child(2), .pdl-list .data-table td:nth-child(1) { overflow: hidden; }
.gl-cat-t { display: inline-block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
/* Unify category casing — every tag reads with a capital first letter (login → Login,
   out of range → Out of range) while leaving command / UE-category camel case intact. */
.gl-cat-t::first-letter { text-transform: uppercase; }
/* The player-card Logs table is auto-layout, so cap its category so a long label
   ("Vehicle Destruction") can't crowd out the message. */
.pdl-list .data-table td:nth-child(1) { max-width: 12rem; }

/* ════════════════════════════════════════════════════════════════════════
   DESIGN-SYSTEM REFINEMENTS · increment 24 — Live Map: location ping
   Clicking a log location opens the map centred on the spot AND drops a pulsing
   pin there, so the admin sees exactly where the event happened. Lives inside the
   transformed #map-canvas and counter-scales (--mz) so it stays a constant size.
   ════════════════════════════════════════════════════════════════════════ */
.map-ping { position: absolute; z-index: 30; pointer-events: none; }
.map-ping.hidden { display: none; }
/* Bold target marker centred ON the spot: a bright ring + an X, with a dark halo so it reads on any
   terrain, plus an expanding pulse to catch the eye. All three counter-scale (--mz) to a constant
   on-screen size regardless of map zoom. */
.map-ping-ring, .map-ping-x, .map-ping-pulse {
  position: absolute; left: 0; top: 0; transform-origin: center;
}
.map-ping-ring {
  width: 38px; height: 38px; border-radius: 50%;
  border: 3px solid var(--amber-hi); background: rgba(255, 106, 26, 0.14);
  transform: translate(-50%, -50%) scale(var(--mz, 1));
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.75), 0 0 10px 2px rgba(255, 106, 26, 0.55), inset 0 0 6px rgba(0, 0, 0, 0.5);
}
.map-ping-x { width: 20px; height: 20px; transform: translate(-50%, -50%) scale(var(--mz, 1)); }
.map-ping-x::before, .map-ping-x::after {
  content: ''; position: absolute; left: 50%; top: 0; width: 3px; height: 100%;
  background: var(--amber-hi); border-radius: 2px;
  box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.7);
}
.map-ping-x::before { transform: translateX(-50%) rotate(45deg); }
.map-ping-x::after { transform: translateX(-50%) rotate(-45deg); }
.map-ping-pulse {
  width: 38px; height: 38px; border-radius: 50%;
  transform: translate(-50%, -50%) scale(var(--mz, 1));
}
.map-ping.ping-anim .map-ping-pulse { animation: mapPingPulse 1.5s ease-out infinite; }
.map-ping.ping-anim .map-ping-ring { animation: mapPingPop 0.4s cubic-bezier(0.2, 0.8, 0.3, 1.5); }
@keyframes mapPingPulse {
  0% { box-shadow: 0 0 0 0 rgba(255, 106, 26, 0.55); }
  70% { box-shadow: 0 0 0 calc(26px * var(--mz, 1)) rgba(255, 106, 26, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 106, 26, 0); }
}
@keyframes mapPingPop {
  0% { transform: translate(-50%, -50%) scale(calc(var(--mz, 1) * 2.2)); opacity: 0; }
  100% { transform: translate(-50%, -50%) scale(var(--mz, 1)); opacity: 1; }
}

/* Numbered pages in the native table pager (same shape as the Field Console). */
.roster-pager-nums { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.roster-pager .pg-n { min-width: 2rem; padding: .3rem .5rem; font-variant-numeric: tabular-nums; }
.roster-pager .pg-n.cur { border-color: var(--accent, var(--amber, #ff6a1a)); color: var(--accent, var(--amber, #ff6a1a)); cursor: default; }
.roster-pager-gap { color: var(--muted, #8b9099); padding: 0 2px; user-select: none; }

/* Table pagination on a phone: drop the outer page numbers, keep Prev/Next, first, last and the
   pages next to the current one. The row also wraps instead of pushing the table sideways. */
.roster-pager { flex-wrap: wrap; }
@media (max-width: 560px) {
  .roster-pager .pg-far { display: none; }
  .roster-pager .pg-n { min-width: 1.8rem; padding: .3rem .4rem; }
}

/* 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; }

/* ── roster: numeric columns + in-game id ────────────────────────────────────
   Playtime / Fame / Money / Gold read as a column of figures, so they are right-aligned with
   tabular numerals — proportional digits make columns of numbers impossible to compare. */
.roster-table td.num, .roster-table th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.roster-table th.num .th-sort-in { justify-content: flex-end; }
/* The in-game profile id, now its own column instead of a suffix on the Steam ID. */
.roster-table td.rr-uid-col { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }

/* Country filter items are multi-select, so they carry the same tick box as every other
   checkable menu (.cdd-check) rather than looking like single-choice rows. */
.rc-item.rc-check .cdd-box {
  flex: none; width: 14px; height: 14px; border-radius: 3px;
  border: 1px solid var(--line-amber, var(--line)); background: var(--inset);
}
.rc-item.rc-check.on { background: none; color: var(--amber-hi); }
.rc-item.rc-check.on .cdd-box {
  background: var(--amber); border-color: var(--amber);
  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: 12px 12px; background-position: center; background-repeat: no-repeat;
}

/* ── admin audit trail ───────────────────────────────────────────────────────
   Built to match the Players roster, because it is the same kind of object — a long list of rows
   scanned top to bottom — and two tables in one panel that behave differently read as a bug.
   It used to be a bare .data-table: auto layout, so the columns were sized by whatever happened to
   be in them and the table ran to the edges of the page; no zebra, no sticky header, no hover.

   Same rules as .roster-table: layout is FIXED, so every column must state a width or it collapses
   while the others divide the space. Percentages, not rem — the panel scales its root font size,
   and a rem width comes out about a quarter too wide against it. */
.data-table.audit-table { table-layout: fixed; width: 100%; }
.audit-table th, .audit-table td { padding-left: 0.5rem; padding-right: 0.5rem; }
.audit-table td { vertical-align: top; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.audit-table th { overflow: hidden; text-overflow: ellipsis; }

/* Re-balanced when ACTION and RESULT stopped being a dotted key and a three-digit number.
   "Server control › Restart skip" and "Not permitted" are words, and every cell here is
   `nowrap` + `ellipsis`, so a column too narrow for its own content does not wrap — it CUTS,
   which is the defect `check-panel-render`'s `fits` rule is about. */
/* The same, at every width rather than only on a phone: the columns are percentages, so the cell
   is narrow on a desktop too. */
.audit-table td.c-when, .audit-table td.c-when.dim { white-space: normal; overflow: visible; text-overflow: clip; }
.audit-table th.c-when   { width: 12%; }
.audit-table th.c-who    { width: 12%; }
.audit-table th.c-action { width: 20%; }   /* "Server control › Restart", "Players › Intel note" */
.audit-table th.c-target { width: 13%; }
.audit-table th.c-status { width: 11%; }   /* "Not permitted", "Server unreachable" */
.audit-table th.c-ip     { width: 10%; }
.audit-table th.c-details{ width: 19%; }   /* a preview — the full body is in the expanded row */
.audit-table th.c-exp    { width: 2.4%; }  /* just the chevron */

/* Readable header + scannable rows, the same treatment increment 3 gave the roster. */
.audit-table thead th {
  background: var(--bg-2);
  border-bottom: 1px solid var(--line-amber);
  position: sticky; top: 0; z-index: 1;
}
.audit-table tbody tr.audit-row { transition: background .12s ease; cursor: pointer; }
.audit-table tbody tr.audit-row:nth-of-type(4n+3) { background: rgba(255, 255, 255, 0.014); }
.audit-table tbody tr.audit-row:hover { background: var(--hover-bg); }
.audit-table tbody tr.audit-row:focus-visible { outline: 2px solid var(--amber); outline-offset: -2px; }
/* A failed action is the one worth spotting, so it is marked rather than just listed. It has to
   win over the zebra and the hover, hence the later position and the explicit hover rule. */
.audit-table tbody tr.audit-row.audit-bad,
.audit-table tbody tr.audit-row.audit-bad:nth-of-type(4n+3) { background: rgba(255, 92, 92, 0.07); }
.audit-table tbody tr.audit-row.audit-bad:hover { background: rgba(255, 92, 92, 0.12); }
.audit-status-bad { color: #ff8b80; }
.audit-table code { font-family: var(--mono); font-size: 0.78rem; color: var(--amber-hi, var(--amber)); }
/* The preview of what was done. It is a `key: value · key: value` line, so it breaks at its own
   separators; cut, it was 86% of itself and the last field was the one that went. */
.audit-details { font-family: var(--mono); font-size: 0.75rem; color: var(--muted); white-space: normal; overflow-wrap: anywhere; }
.audit-table td.c-exp { position: relative; overflow: visible; }

/* The chevron is drawn, not fetched: the panel's sprite has no chevron symbol, and the alternative
   is adding one to the sheet for a single 6px mark. */
.audit-chev {
  position: absolute; right: 0.35rem; top: 50%;
  width: 0.42em; height: 0.42em; margin-top: -0.3em;
  border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
  transform: rotate(45deg); transform-origin: center;
  transition: transform .15s ease, border-color .15s ease;
}
.audit-row:hover .audit-chev { border-color: var(--amber); }
.audit-row.audit-open .audit-chev { transform: rotate(-135deg); margin-top: -0.12em; }
.audit-table tbody tr.audit-row.audit-open { background: var(--hover-bg); }

/* Shown only when the recorded IP cannot be the admin's own — a permanent hint is a hint nobody
   reads. Amber, not red: nothing is broken, the value just isn't what it looks like. */
.audit-ip-hint {
  border-left: 2px solid var(--amber);
  padding-left: 0.7rem;
  color: var(--text);
}

/* ── the expanded row ──────────────────────────────────────────────────────
   The details column used to be the whole recorded body truncated to 70 characters, with the rest
   only in a `title` tooltip — unreadable for JSON and impossible to copy. */
.audit-detail-row > td {
  white-space: normal;
  background: var(--inset, rgba(0, 0, 0, 0.18));
  border-bottom: 1px solid var(--line-amber);
  padding: 0.85rem 1rem 1rem;
}
.audit-dd-grid {
  display: grid; gap: 0.5rem 1.4rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  margin-bottom: 0.75rem;
}
.audit-dd-f { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.audit-dd-k {
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600;
}
.audit-dd-v { font-size: 0.84rem; color: var(--text); word-break: break-word; }
.audit-dd-v.mono { font-family: var(--mono); font-size: 0.78rem; }
.audit-dd-body { margin-top: 0.2rem; }
.audit-json {
  margin: 0.3rem 0 0; padding: 0.6rem 0.75rem; max-height: 18rem; overflow: auto;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: calc(var(--radius) / 2);
  font-family: var(--mono); font-size: 0.76rem; line-height: 1.5; color: var(--text);
  white-space: pre-wrap; word-break: break-word;
}

/* Narrow screens drop columns rather than squeezing them — the same order Players uses, least
   useful first. Nothing is lost: every dropped value is in the expanded row. */
@media (max-width: 1100px) {
  .audit-table th.c-ip, .audit-table td.c-ip { display: none; }
  .audit-table th.c-details { width: 35%; }
}
@media (max-width: 820px) {
  .audit-table th.c-target, .audit-table td.c-target { display: none; }
  .audit-table th.c-details, .audit-table td.c-details { display: none; }
  .audit-table th.c-when { width: 22%; }
  .audit-table th.c-who { width: 22%; }
  .audit-table th.c-action { width: 28%; }
  .audit-table th.c-status { width: 20%; }
  .audit-table th.c-exp { width: 6%; }
}
/* ⚠ **AND THE DROPPING STOPPED AT 820px, WHICH IS NOT A PHONE.** Five columns were still on screen
   at 320px, so the timestamp came back as 31% of itself in 51px — `Jul 5, 2026, 8:00…`, a date with
   no time on a log whose whole subject is when something happened — and the action as 23% in 65px.
   The same ladder, carried down to the widths people actually hold: the outcome is a chip that the
   row's own colour already carries, and the date gives up its year and seconds rather than its
   hour. Everything dropped is in the expanded row, which is what the arrow on the right opens. */
@media (max-width: 560px) {
  .audit-table th.c-status, .audit-table td.c-status { display: none; }
  .audit-table th.c-when { width: 30%; }
  .audit-table th.c-who { width: 26%; }
  .audit-table th.c-action { width: 36%; }
  .audit-table th.c-exp { width: 8%; }
  /* Two short words on a caption wrap; the action is a breadcrumb with its own separators and is
     allowed to use them. */
  .audit-table td.c-action { white-space: normal; overflow: visible; text-overflow: clip; }
  .audit-table td.c-when.dim { white-space: normal; overflow: visible; text-overflow: clip; }
  /* An admin's own name, which is the column the whole log is read BY. It was 52% of itself in
     59px, so it wraps here too — a name can be one unbroken word, which is what `overflow-wrap`
     is for. */
  .audit-table td.c-who.rr-name { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere; }
  .audit-table td.c-details.audit-details { white-space: normal; overflow: visible; text-overflow: clip; }
}

/* ── In-game mods (SSA Bridge modules) ─────────────────────────────────────
   One card per module: kill-switch and live state in the header, what it did in the middle, its
   settings below. The forms are drawn from a description the module ships, so nothing here is
   specific to any one feature. */

.mod-list { display: flex; flex-direction: column; gap: 1rem; }
.mod-note { margin: 0; }

.mod-offline {
  display: flex; gap: 0.9rem; align-items: flex-start;
  padding: 1.1rem 1.25rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-md);
}
.mod-offline .ico { width: 20px; height: 20px; flex: none; color: var(--standby); }
.mod-offline b { display: block; margin-bottom: 0.25rem; }
.mod-offline p { margin: 0 0 0.5rem; color: var(--muted); font-size: 0.88rem; line-height: 1.5; }
.mod-offline code { color: var(--faint); font-size: 0.78rem; }

/* The in-game kill-switch. One quiet line when it is ON — it is a precondition, not news — and the
   `.mod-offline` panel when it is OFF, which is the state that has to be readable at a glance.
   `flex-wrap` plus `min(100%, …)` on the text so the row folds on a phone instead of widening the
   page; the button is a `.pl-btn` and inherits the panel's control floor and hover pair. */
.mod-killswitch {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 0.9rem;
  padding: 0.55rem 0.9rem; margin-bottom: 0.25rem;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-md);
  color: var(--muted); font-size: 0.84rem;
}
.mod-killswitch > span { flex: 1 1 min(100%, 18rem); min-width: 0; }
.mod-killswitch-off .ico { color: var(--standby); }

/* The bridge's own cost meter. A diagnostic, so it is quiet by default and only ever as loud as
   what it found. `min(100%, …)` on the table so a long mod id folds instead of widening the page. */
.mod-perf {
  margin-top: 1rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-md);
}
.mod-perf > summary { display: flex; align-items: center; gap: 0.55rem; padding: 0.8rem 1.1rem; cursor: pointer; font-weight: 600; }
.mod-perf > summary .ico { width: 18px; height: 18px; color: var(--amber); }
.mod-perf > div { padding: 0 1.1rem 1.1rem; }
.perf-lead { margin: 0 0 0.8rem; color: var(--muted); font-size: 0.86rem; line-height: 1.55; }
.perf-nums { display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; list-style: none; margin: 0 0 0.9rem; padding: 0; }
.perf-nums li { color: var(--muted); font-size: 0.82rem; }
.perf-nums b { color: var(--text); font-family: var(--mono); font-size: 0.95rem; margin-right: 0.3rem; }
.perf-tbl { width: 100%; max-width: min(100%, 48rem); border-collapse: collapse; font-size: 0.82rem; }
.perf-tbl th { text-align: left; color: var(--faint); font-weight: 500; padding: 0.3rem 0.6rem 0.3rem 0; }
.perf-tbl td { padding: 0.28rem 0.6rem 0.28rem 0; color: var(--muted); border-top: 1px solid var(--line-2); word-break: break-word; }
.perf-tbl td:first-child { color: var(--text); }
.perf-h { margin: 1.1rem 0 0.4rem; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--faint); }
.perf-none { margin: 1rem 0 0; color: var(--online); font-size: 0.84rem; }
#mod-perf-reset { margin-top: 1rem; }
.mod-ks-msg { margin: 0.5rem 0 0 !important; color: var(--alarm); }
.mod-ks-msg:empty { display: none; }

.mod-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-md);
  overflow: hidden;
}
.mod-card.on { border-color: var(--line-amber); }

.mod-head {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.85rem 1.1rem;
  background: var(--panel-2); border-bottom: 1px solid var(--line);
}
.mod-title { display: flex; align-items: center; gap: 0.6rem; flex: 1; min-width: 0; flex-wrap: wrap; }
.mod-name { font-weight: 600; }
.mod-head-actions { display: flex; gap: 0.5rem; align-items: center; }

.mod-state {
  padding: 0.1rem 0.45rem; border-radius: 999px;
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em;
  border: 1px solid var(--line); color: var(--muted); background: var(--inset);
}
.mod-s-ready { color: var(--online); border-color: rgba(84, 201, 138, 0.35); }
.mod-s-loading { color: var(--standby); border-color: rgba(230, 178, 58, 0.35); }
.mod-s-dry { color: var(--amber-hi); border-color: var(--line-amber); }

/* what it did */
.mod-status { padding: 0.9rem 1.1rem; border-bottom: 1px solid var(--line-2); }
.mod-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr)); gap: 0.7rem; }
.mod-stat { text-align: center; }
.mod-stat-v { display: block; font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.mod-stat-k { display: block; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.mod-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.8rem; }
.mod-chip {
  padding: 0.1rem 0.45rem; border-radius: 999px; font-size: 0.73rem;
  background: var(--inset); border: 1px solid var(--line); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.mod-chip b { color: var(--text); }

/* settings */
.mod-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); gap: 0 1.6rem; padding: 1rem 1.1rem 1.2rem; }
.mod-group { min-width: 0; }
.mod-group-t {
  margin: 0.8rem 0 0.5rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--muted); font-weight: 600;
}

.mf { display: block; margin-bottom: 0.85rem; min-width: 0; }
/* A field whose condition isn't met stays visible but inert — hiding it makes a setting look lost. */
.mf-off { opacity: 0.4; pointer-events: none; }
.mf-label { display: block; font-size: 0.86rem; font-weight: 500; }
.mf-hint { margin: 0.15rem 0 0.35rem; color: var(--muted); font-size: 0.78rem; line-height: 1.45; }

.mf-check { display: flex; gap: 0.6rem; align-items: flex-start; cursor: pointer; }
.mf-check input { margin-top: 0.2rem; flex: none; }
.mf-check .mf-body { min-width: 0; }
.mf-check .mf-hint { margin-bottom: 0; }

.mf-opts { display: grid; gap: 0.45rem; margin-top: 0.35rem; }
.mf-opt {
  display: block; width: 100%; text-align: left; cursor: pointer; font: inherit; color: var(--text);
  padding: 0.6rem 0.75rem; background: var(--inset);
  border: 1px solid var(--line); border-radius: var(--radius);
  transition: border-color .12s ease, background .12s ease;
}
.mf-opt:hover { background: var(--hover-bg); }
.mf-opt.on { border-color: var(--amber); background: rgba(255, 106, 26, 0.07); }
.mf-opt-t { display: block; font-weight: 600; font-size: 0.88rem; }
.mf-opt-h { display: block; margin-top: 0.15rem; color: var(--muted); font-size: 0.78rem; line-height: 1.45; }

.mf-rangerow { display: flex; align-items: center; gap: 0.8rem; }
.mf-rangerow input { flex: 1; min-width: 0; }
.mf-out { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 3rem; text-align: right; }

.mf-int input, .mf-text input {
  width: 100%; padding: 0.4rem 0.6rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font: inherit;
}
.mf-int input:focus, .mf-text input:focus { border-color: var(--amber); outline: none; }

.mf-places {
  margin-top: 0.35rem; padding: 0; background: none; border: 0;
  color: var(--amber-hi); font: inherit; font-size: 0.78rem; cursor: pointer; text-decoration: underline;
}
.mf-places-list {
  /* The list is complete rather than capped, so the window has to be worth scrolling: 14rem showed
     about six of 848 rows at a time. The search header is sticky, so it stays reachable however far
     down you are. */
  margin-top: 0.4rem; max-height: min(26rem, 55vh); overflow: auto; overscroll-behavior: contain;
  border: 1px solid var(--line); border-radius: var(--radius);
}
.mf-place {
  display: flex; width: 100%; gap: 0.6rem; align-items: center;
  padding: 0.3rem 0.55rem; background: none; border: 0; border-bottom: 1px solid var(--line-2);
  color: var(--text); font: inherit; font-size: 0.8rem; cursor: pointer; text-align: left;
}
.mf-place:last-child { border-bottom: 0; }
.mf-place:hover { background: var(--hover-bg); }
.mf-place.picked { color: var(--amber-hi); }
.mf-place-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, monospace; }
.mf-place-c { color: var(--muted); font-variant-numeric: tabular-nums; }

/* module state chips beyond the module's own reported state */
.mod-s-offline { color: var(--muted); }
.mod-s-restart { color: var(--standby); border-color: rgba(230, 178, 58, 0.35); }

/* places picker — 848 rows, so it needs a filter and the chosen ones on top */
.mf-place-search {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.4rem 0.55rem; border-bottom: 1px solid var(--line);
  background: var(--panel-2); position: sticky; top: 0; z-index: 1;
}
.mf-place-q {
  flex: 1; min-width: 0; padding: 0.25rem 0.5rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font: inherit; font-size: 0.8rem;
}
.mf-place-q:focus { border-color: var(--amber); outline: none; }
.mf-place-n-of { color: var(--muted); font-size: 0.72rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.mf-place.picked { color: var(--amber-hi); background: rgba(255, 106, 26, 0.07); }
.mf-place.picked .mf-place-n::before { content: "✓ "; }

/* a stat that actually has a value — a zero stays plain, so what matters stands out */
.mod-stat-v.mod-hi { color: var(--amber-hi, var(--amber)); }

/* the bridge core card: no kill-switch, so an icon holds the switch's place and the row stays aligned */
.mod-card.core { border-color: var(--line-amber); }
.mod-core-ico { width: 20px; height: 20px; color: var(--amber); flex: none; }

/* "nothing was found" — a real outcome, not a wait */
.mod-s-none { color: var(--alarm); border-color: rgba(255, 92, 92, 0.35); }
.mod-clash { border-color: rgba(255, 92, 92, 0.45); }
.mod-clash .ico { color: var(--alarm); }

/* multi-select: a compact grid, because a dozen entity kinds as a stacked list is a scroll */
.mf-multi-opts {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  gap: 0.15rem 0.6rem; margin-top: 0.35rem;
}
.mf-multi-opt { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; font-size: 0.85rem; }
.mf-multi-opt input { flex: none; }

/* ── live map layers ───────────────────────────────────────────────────────
   Read out of the running game rather than the last save. */
.mk-live { background: var(--c-player, #35d07f); }
.mk-livebase { background: var(--amber); }

/* condition bar — banded, not a gradient: "wrecked / damaged / fine" is the decision */
.hpb {
  display: inline-block; vertical-align: middle;
  width: 5.5rem; height: 0.5rem; margin-right: 0.4rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: 999px; overflow: hidden;
}
.hpb.sm { width: 3.5rem; height: 0.4rem; }
.hpb-t { display: block; height: 100%; }
.hpb-ok { background: var(--online); }
.hpb-warn { background: var(--standby); }
.hpb-bad { background: var(--alarm); }
.hpb-n { font-variant-numeric: tabular-nums; font-size: 0.8rem; }

/* Wrapper for the parts box in a card — the database-backed one and the live one alike. */
/* The parts box is ALSO the scroll container, so it must carry no padding at the top: a sticky
   header sticks to the container's PADDING edge, which leaves that strip above it for rows to scroll
   through — the heading reads as sitting slightly low with a gap above it that items slide past.
   The header supplies its own spacing instead. */
.mp-parts { margin-top: 0.5rem; border-top: 1px solid var(--line); padding-top: 0; }
/* A live part row is a content row with a condition bar on the end. The bar must not be squeezed by
   a long name, so it keeps its width and the NAME gives way — a half-drawn bar reads as a wrong
   number, while a clipped name is still obviously a clipped name. */
.mp-parts .mp-c-row .hpb { flex: none; margin-right: 0.25rem; }
.mp-parts .mp-c-row .hpb-n { flex: none; min-width: 2.6em; text-align: right; font-size: 0.72rem; }
/* Why a column of bars is missing — stated once at the top of the panel rather than left blank for
   the reader to interpret as "everything is fine". */
.mp-c-note { padding: 0.35rem 0.1rem 0.15rem; color: var(--standby); font-size: 0.74rem; line-height: 1.4; }

/* how many markers a dense layer left out — sits under the map, never in the way of it */
.map-trunc {
  position: absolute; left: 50%; bottom: 0.6rem; transform: translateX(-50%);
  padding: 0.25rem 0.7rem; border-radius: 999px;
  background: rgba(11, 12, 14, 0.85); border: 1px solid var(--line-amber);
  color: var(--amber-hi, var(--amber)); font-size: 0.78rem; white-space: nowrap;
  pointer-events: none; z-index: 5;
}
.ml-more { padding: 0.35rem 0.6rem; color: var(--muted); font-size: 0.78rem; font-style: italic; }

/* ── live-layer kinds ──────────────────────────────────────────────────────
   Sixteen different things share one layer, so the colour has to carry the meaning: hostile things
   read red, wildlife green, machines blue, structures amber. Drawn in one colour they are a sea of
   identical dots and the layer answers nothing.

   THESE ARE NOT PALETTE TOKENS AND THE 2026-09-04 SWAP DELIBERATELY LEFT ALL TWELVE ALONE.
   Three of them were the same literal as a theme colour — puppet was --alarm, animal was --online,
   base was --amber — which reads like an invitation to re-point them at the new values. Measured,
   it is the opposite. This set is tuned for MUTUAL separation, and the closest pair anywhere in it
   was puppet/boss at 71 (weighted RGB distance). Re-pointing animal to the new green #54c98a puts
   it 52 from --c-player, which would make the two most important dots on a live map — a wild animal
   and a person — the hardest pair to tell apart. Re-pointing puppet to #ff5c5c collapses
   puppet/sentry from 117 to 68, and base to #ff6a1a collapses sentry/base from 87 to 69.

   Against the new ground every one of them measures the SAME or BETTER than it did against the old,
   because #0b0c0e is marginally darker than the olive #0d0e0a: the worst of the twelve is boss at
   5.37:1 (was 5.31), and nothing drops. So there was nothing to fix and three distinctions to lose.
   Judge a marker colour by what it has to be told apart FROM, not by which token it once matched. */
:root {
  --lk-puppet:  #e5443f;   /* hostile */
  --lk-boss:    #ff2d55;
  /* Razor is not Brenner. Both are bosses and both keep the skull, so the colour is what
     separates them — a lighter pink of the same family, because they belong on one layer. */
  --lk-boss2:   #ff7ab0;
  --lk-sentry:  #ff7a45;   /* dangerous, but static */
  --lk-npc:     #c9a24b;   /* people */
  --lk-animal:  #6cbf3f;   /* wildlife */
  --lk-shark:   #29a3c9;
  --lk-drone:   #a06cff;   /* machines */
  /* A dropship is not a big drone — it delivers a cargo drop, which is an event players converge on,
     while a patrol drone is ambient. They get their own layers, so they get their own colour. */
  --lk-dropship: #00d3c2;
  --lk-vehicle: #5aa0ff;
  --lk-base:    #f0820c;
  /* A body. Bone rather than another saturated hue, and that is the reading as well as the taste:
     every colour above says "something is happening here" and a corpse is the one marker on this
     map that says the opposite — it is over, and what is left is where the gear is. Judged the way
     the note above this block says to judge one, by what it has to be told apart FROM: it is the
     only desaturated marker on the map, so it cannot be confused with the gold NPC or the blue
     vehicle at a glance, and it is light enough on #0b0c0e to clear the contrast floor comfortably. */
  --lk-corpse:  #b9b2a6;
}
.mk-live .mk-dot, .mk-livebase .mk-dot { border-color: rgba(0, 0, 0, 0.35); }
.lk-player  .mk-dot, .ml-dot.lk-player  { background: var(--c-player); }
.lk-puppet  .mk-dot, .ml-dot.lk-puppet  { background: var(--lk-puppet); }
.lk-animal  .mk-dot, .ml-dot.lk-animal  { background: var(--lk-animal); }
.lk-shark   .mk-dot, .ml-dot.lk-shark   { background: var(--lk-shark); }
.lk-sentry  .mk-dot, .ml-dot.lk-sentry  { background: var(--lk-sentry); }
.lk-sentryd .mk-dot, .ml-dot.lk-sentryd { background: var(--lk-sentry); }
.lk-npc     .mk-dot, .ml-dot.lk-npc     { background: var(--lk-npc); }
.lk-drone   .mk-dot, .ml-dot.lk-drone   { background: var(--lk-drone); }
.lk-dropship .mk-dot, .ml-dot.lk-dropship { background: var(--lk-dropship); }
.lk-boss    .mk-dot, .ml-dot.lk-boss    { background: var(--lk-boss); }
.lk-boss2   .mk-dot, .ml-dot.lk-boss2   { background: var(--lk-boss2); }
.lk-vehicle .mk-dot, .ml-dot.lk-vehicle { background: var(--lk-vehicle); }
.lk-base    .mk-dot, .ml-dot.lk-base    { background: var(--lk-base); }
/* A deployed sentry is somebody's, so it gets a ring the world ones don't have. */
.lk-sentryd .mk-dot { box-shadow: 0 0 0 2px rgba(255, 106, 26, 0.55); }
/* Dead things stay on the map — where a body fell is information — but must not read as alive. */
.map-mk.dead .mk-dot { opacity: 0.35; filter: grayscale(1); }
/* a note inside a module card — why what you are looking at is not the whole story */
.mod-note-inline {
  margin: 0; padding: 0.6rem 1.1rem;
  border-bottom: 1px solid var(--line-2, var(--line));
  color: var(--standby); font-size: 0.83rem; line-height: 1.5;
}



/* a marker whose position came from the running game, not the last save */
.mp-live { color: var(--online); }
/* …and its opposite number, which for a whole release did not exist. A live position said so and a
   SAVED one said nothing, so the two were told apart by the absence of a chip — which is not a
   distinction anybody reads, and on a stopped server it was the state of every card on the map.
   Muted rather than alarming: a save-file position is a normal and useful thing to be looking at.
   What is not acceptable is it looking like where somebody is standing. */
.mp-saved { color: var(--muted); font-weight: 600; }
/* A missing frame is a finding, not a blank — it reads in the same register as the trap colour,
   which is the palette's existing "this one is a problem". */
.mp-bad { color: var(--c-trap); font-weight: 600; }
.map-mk.livepos .mk-dot { box-shadow: 0 0 0 2px rgba(84, 201, 138, 0.55); }

/* live layers — one colour per layer, mirroring the per-kind marker colours */
.mk-puppet .mk-dot, .ml-dot.mk-puppet     { background: var(--lk-puppet); }
.mk-animal .mk-dot, .ml-dot.mk-animal     { background: var(--lk-animal); }
/* A sentry EMPLACEMENT, which is a place — the same hot orange as the sentry that stands on it,
   because it is the same thing's post, plus the inset ring this panel has always used to mark a
   place rather than a thing. Both carry `i-turret` and the same colour token, so without the
   ring the two are one picture and only the class name differs: see `mapAdoptSaved`, where the
   live-kind stamp is withheld from a row that carries a `place`. */
.mk-sentry .mk-dot, .ml-dot.mk-sentry     { background: var(--lk-sentry);
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.45); }
.mk-npc .mk-dot, .ml-dot.mk-npc           { background: var(--lk-npc); }
.mk-drone .mk-dot, .ml-dot.mk-drone       { background: var(--lk-drone); }
.mk-dropship .mk-dot, .ml-dot.mk-dropship { background: var(--lk-dropship); }
.mk-boss .mk-dot, .ml-dot.mk-boss         { background: var(--lk-boss); }
.mk-corpse .mk-dot, .ml-dot.mk-corpse     { background: var(--lk-corpse); }

/* ════════════════════════════════════════════════════════════════════════
   HOVER PAIRING · the one place a button opts out of the amber hover

   `button:hover` paints the amber fill with dark text. A button that wants a
   different hover has to change BOTH halves, and the way to do that is here —
   set the trio on the element, never override `background` or `color` alone in
   a `:hover` rule. Splitting the pair is what produced dark-on-dark and
   light-on-light labels all over the panel, one patch at a time.

   Two shapes, and everything falls into one of them:
     ghost   — an icon or text button that should never take a fill at all
     tinted  — a row or tile that takes its own soft fill and keeps normal text

   `npm run check-contrast` folds the cascade and fails on any button whose
   hover states one half without the other, so this cannot drift back.
   ════════════════════════════════════════════════════════════════════════ */

/* ghost: no fill, no border flash — the colour change alone is the affordance */
.sched-x, .su-btn-ghost, .pl-modal-x, .ssa-toast-x, .am-back, .am-x, .foot-update,
.amodal-close, .mp-close, .ul-del, .link-btn, .map-filters-head, .pd-stuff-head,
.foot-link, .mos-item, .cs-chip-x, .cs-saved-x {
  --hov-bg: transparent;
  --hov-line: currentColor;
  --hov-fg: var(--text);
}
/* …except these carry a real border of their own, which must not turn amber */
.su-btn-ghost { --hov-line: var(--faint); }
.link-btn, .foot-link, .foot-update { --hov-line: transparent; }

/* tinted: a soft fill of its own, with the panel's normal text on top */
.pd-member, .intel-tile, .intel-cc, .map-zoom-b, .mf-opt, .mf-place,
.map-layer, .ml-row, .anav-dd-item, .adm-perm {
  --hov-fg: var(--text);
}

/* Base damage, in the same three bands the game's own base screen uses. Colour carries the severity
   so the row reads at a glance; the numbers are cumulative, as the game counts them. */
/* Three equal cells, not three pills sharing a line. As pills they wrapped — two on one row and the
   third dropped below — which read as a broken row rather than as three bands of one measurement.
   A grid cannot wrap, and the counts line up, which is the only reason to show the three together. */
.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(--inset); border: 1px solid var(--line-2);
  line-height: 1.25; white-space: nowrap; color: var(--muted);
}
.mp-band b { font-size: 0.95rem; font-weight: 700; }
.mp-band i { font-style: normal; font-size: 0.62rem; letter-spacing: 0.02em; }
.mp-band b { font-variant-numeric: tabular-nums; }
.mp-band.b75 b { color: var(--standby); }
.mp-band.b50 b { color: var(--amber-hi); }
.mp-band.b25 b { color: var(--alarm); }

/* A container found inside another one: the same row shape as an item, but it opens. */
.mp-c-open {
  width: 100%; text-align: left; font: inherit; letter-spacing: 0; cursor: pointer;
  background: none; border: 0; border-radius: var(--radius); padding: 0.18rem 0.2rem;
  color: var(--text);
  /* Tinted, not amber-filled: it sits among plain rows and must still read as one of them. */
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: transparent;
}
.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. The first version indented it by 1.4rem, which
   read as the panel jumping sideways rather than as something unfolding — the rule was full width and
   suddenly its contents were not. A rule down the left says "these belong to that" without moving
   anything. */
.mp-c-sub {
  margin: 0.15rem 0 0.35rem; padding: 0.2rem 0 0.2rem 0.55rem;
  /* No cap here either — the card is the one thing that scrolls. */
  border-left: 2px solid var(--line-amber);
  background: rgba(0, 0, 0, 0.14); border-radius: 0 var(--radius) var(--radius) 0;
}
/* Boxes and loose items are different answers to "what is in here", so they do not look alike: a
   container is a row you can open, and it sits above the plain contents under its own heading. */
.mp-c-open { border: 1px solid var(--line-2); background: var(--inset); margin-bottom: 0.15rem; }
.mp-c-open:last-of-type { margin-bottom: 0.3rem; }

/* "Everything on this card is what the game says right now" — a mark on the artwork, not a chip.
   It qualifies the whole card, so it belongs on the thing itself rather than in a row of properties
   where it competed with Online and Dead and was the longest of the three. */
.mp-ico.mp-live-at { position: relative; overflow: visible; }
.mp-ico.mp-live-at::after {
  content: ''; position: absolute; right: -3px; bottom: -3px;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--online); border: 2px solid var(--panel);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
}
/* A slow pulse says "still current" without being a moving object on a card people read. */
@media (prefers-reduced-motion: no-preference) {
  .mp-ico.mp-live-at::after { animation: mp-live-pulse 2.4s ease-in-out infinite; }
}
@keyframes mp-live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
/* The image plate clips its art, so the mark has to live on a wrapper that does not clip. */
.mp-ico.mp-img.mp-live-at { overflow: visible; }
.mp-ico.mp-img.mp-live-at img { border-radius: calc(var(--radius-md) - 3px); }

/* Options that belong to the panel they head, sitting directly under its title rather than stranded
   below a long list. Quiet ground so they read as settings, not as content. */
.map-opts-top { background: rgba(0, 0, 0, 0.12); }
/* Exports are actions, not filters, so they get their own strip at the foot of the panel. Hidden
   entirely when neither has anything to write. */
/* The export used to be a sticky strip at the foot of the list panel. It is one menu in the
   screen's action bar now — a menu had to open over the lists it belongs to, and as a child of that
   scrollable column it widened the column and pushed the page into a sideways scrollbar. */

/* Average and worst, at the head of the parts they summarise — shown whether the numbers came from
   the running game or from the save. */
.mp-c-sum {
  display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
  padding: 0.35rem 0.1rem 0.4rem; border-bottom: 1px solid var(--line-2);
  font-size: 0.78rem; color: var(--muted);
}
.mp-c-sum-w { margin-left: 0.35rem; }

/* A spawn point with nothing standing on it: where a sentry WILL be, not where one is. Hollow rather
   than merely faded — "not there yet" is a different statement from "dead", and a reader must be able
   to tell them apart without hovering. */
/* The layer's own colour is kept — that is what says WHAT it is — but faded, with a dashed edge.
   A fully hollow dot was the first attempt and it hid the icon: the glyph is coloured for a solid
   fill, so on transparent it disappears. Faded keeps both readable. */
.map-mk.ghost .mk-dot { opacity: 0.45; border-style: dashed; border-width: 2px; }
.map-mk.ghost .mk-label { font-style: italic; opacity: 0.75; }
/* Never on top of a real one: if both are drawn, the thing that is actually there wins the click. */
.map-mk.ghost { z-index: 0; }

/* ── Translations editor ─────────────────────────────────────────────────────
   Laid out like the settings screens, because it is the same shape of problem: something to read on
   the left, the box that changes it on the right. The first version led with the key in a monospace
   column as wide as the text, which turned three thousand strings into a list of developer
   identifiers with the meaning tucked in beside them. */
/* The screen's own margins — 1.5rem each side, the same as every other screen's title bar — rather
   than a capped slab pinned to the left edge. Capping it left two thirds of a wide monitor empty
   while the rows themselves sat hard against the window; the content should use the page and simply
   not touch its edges. */
.tr-bar, .tr-stats, .tr-rows { margin-left: 1.5rem; margin-right: 1.5rem; }

/* One row, one job: what to edit (surface + language), what to show (filter + search). Grouped, not
   spread across the whole width with a search box swallowing the middle. */
.tr-bar { display: flex; flex-wrap: wrap; gap: 0.5rem 0.8rem; align-items: center; }
.tr-pick { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; }
.tr-pick select { min-width: min(11rem, 100%); max-width: 100%; text-transform: none; letter-spacing: 0; }
/* Pushed to the end of the row rather than filling it: it is a filter, not the subject. */
.tr-bar .settings-search { flex: 1 1 12rem; min-width: 9rem; max-width: 20rem; }
.tr-only { --hov-bg: var(--panel-2); --hov-fg: var(--text); white-space: nowrap; }
.tr-bar .settings-toolbar-actions { margin-left: auto; }

/* How far along this language is, as a bar — "2814 of 3241" is a number to work out, a bar is a
   glance, and that is the first question this screen exists to answer. */
.tr-stats { display: flex; gap: 0.9rem; flex-wrap: wrap; align-items: center;
  padding: 0.5rem 0.1rem 0.7rem; color: var(--muted); font-size: 0.8rem; }
.tr-prog { display: flex; align-items: center; gap: 0.45rem; }
.tr-prog-bar { width: 9rem; height: 6px; border-radius: var(--pill); background: var(--inset);
  border: 1px solid var(--line-2); overflow: hidden; }
.tr-prog-bar span { display: block; height: 100%; background: var(--online); }
.tr-prog-n { font-variant-numeric: tabular-nums; color: var(--text); font-weight: 700; }

/* Grouped by key prefix and collapsible — the file's own structure, not one invented for the screen. */
.tr-grp { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 0.5rem;
  background: var(--panel); overflow: hidden; }
.tr-grp-h { --hov-bg: var(--panel-2); --hov-fg: var(--amber-hi);
  display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.5rem 0.7rem;
  background: var(--panel-2); border: none; border-bottom: 1px solid var(--line);
  color: var(--text); font: inherit; font-size: 0.76rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; text-align: left; cursor: pointer; }
.tr-grp-h:hover { background: var(--hov-bg); color: var(--hov-fg); }
.tr-grp-n { flex: 1; }
.tr-grp-c { 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.72rem; font-weight: 400;
  font-variant-numeric: tabular-nums; }
/* Untranslated in this group — the number that decides where to start. */
.tr-grp-m { min-width: 1.6em; padding: 0.05rem 0.45em; text-align: center; border-radius: var(--pill);
  background: var(--standby); color: var(--bg); font-size: 0.72rem; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.tr-grp-chev { color: var(--muted); transition: transform 0.12s ease; }
.tr-grp.collapsed .tr-grp-chev { transform: rotate(-90deg); }
.tr-grp.collapsed .tr-grp-b { display: none; }
.tr-grp.collapsed .tr-grp-h { border-bottom: none; }

.tr-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1.2rem; align-items: start; padding: 0.5rem 0.7rem; border-top: 1px solid var(--line-2); }
.tr-row:first-child { border-top: none; }
/* The English is the label: it is what the row is about and what a translator reads. */
.tr-row .tr-en { color: var(--text); font-size: 0.88rem; line-height: 1.4; overflow-wrap: anywhere; }
/* The key is secondary — it matters when hunting one down, not while translating. */
/* The panel gives every <code> a chip: border, fill, padding. On three thousand of them that made
   the key look like a second input box under the text, which is the opposite of secondary. */
.tr-row .tr-k {
  display: block; margin-top: 0.15rem; padding: 0;
  background: none; border: none; border-radius: 0;
  font-family: var(--mono); font-size: 0.68rem; color: var(--muted); overflow-wrap: anywhere;
}
.tr-row textarea { width: 100%; resize: vertical; min-height: 1.9rem; padding: 0.3rem 0.4rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font: inherit; font-size: 0.85rem; line-height: 1.4; }
.tr-row textarea:focus { outline: none; border-color: var(--amber); }
/* Three states worth telling apart at a glance: never translated, changed by this owner, and changed
   in this session but not yet saved. A left edge, so it reads down the column without shouting. */
.tr-row.tr-missing { box-shadow: inset 3px 0 0 var(--standby); }
.tr-row.tr-mine    { box-shadow: inset 3px 0 0 var(--c-vehicle); }
.tr-row.tr-dirty   { box-shadow: inset 3px 0 0 var(--amber); background: rgba(255, 154, 60, 0.06); }
/* Editing English itself has no second column of meaning — the label would be the box's own text. */
/* Editing English itself: the left column is only the key, and a key does not need half the width —
   the text being edited does. */
.tr-rows.tr-base .tr-row { grid-template-columns: minmax(9rem, 22rem) minmax(0, 1fr); }
.tr-rows.tr-base .tr-row .tr-k { margin-top: 0; font-size: 0.76rem; }
.tr-empty { padding: 1.2rem 0.6rem; color: var(--muted); font-size: 0.86rem; }
/* Unsaved work is announced on the button that clears it. */
#tr-save.tr-has { box-shadow: 0 0 0 1px var(--amber-hi) inset; }
/* One column on a narrow screen — two columns of 12rem each is unreadable well before it wraps.
   ⚠ BOTH SHAPES, and the second one is not redundant. `.tr-rows.tr-base .tr-row` above weighs
   (0,3,0) against this rule's (0,1,0), so a media query could never take it: on the English screen
   the key column kept its `minmax(9rem, 22rem)` at every width, and at 310px that left the box being
   edited **16 pixels wide** — a textarea nobody can type into, on the one screen whose whole job is
   typing. A media query does not raise specificity; the selector has to. */
@media (max-width: 1100px) {
  .tr-row, .tr-rows.tr-base .tr-row { grid-template-columns: 1fr; gap: 0.35rem; }
}

/* ── Discord channel / role picker ───────────────────────────────────────────
   The SAME control the rest of the panel uses for a dropdown, so a setting that holds "which
   channel" looks like a choice rather than a box for an 18-digit number with a button bolted on. The
   id box is not gone, just behind the ID toggle — pasting is still the fastest route when somebody
   has one on the clipboard, and the only route when the bot is offline. */
.dp-box { grid-area: control; display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
/* The panel drops the background arrow with `.cdd .cdd-btn`, which needs a .cdd ancestor this
   control does not have — so it kept the painted arrow AND got the caret, and every field showed
   two. Same treatment, applied directly. */
.dp-pick {
  flex: 1 1 auto; min-width: 0;
  justify-content: flex-start; text-align: left;
  background-image: none; padding-right: 0.7rem;
}
.dp-pick .dd-caret { margin-left: auto; padding-left: 0.4rem; }
.dp-pick .cdd-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* An id that names nothing is usually a deleted channel, and the feed on it has been silently dead
   ever since — the control says so rather than looking like any other setting. */
.dp-pick.dp-bad { border-color: var(--c-trap); }
.dp-pick.dp-bad .cdd-lbl { color: var(--c-trap); }
.dp-raw {
  flex: none; padding: 0.24rem 0.5rem; font-family: var(--mono); font-size: 0.7rem; letter-spacing: 0.04em;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted);
  --hov-bg: var(--panel-2); --hov-fg: var(--text);
}
.dp-raw:hover { background: var(--hov-bg); color: var(--hov-fg); }
.dp-raw.on { border-color: var(--amber); color: var(--amber-hi); }
.dp-id { flex-basis: 100%; }
.dp-id[hidden] { display: none; }

.dp-menu {
  position: fixed; z-index: 4000; width: 22rem; max-width: calc(100vw - 16px);
  display: flex; flex-direction: column; padding: 0.35rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
}
.dp-search {
  width: 100%; padding: 0.32rem 0.45rem; margin-bottom: 0.3rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font: inherit; font-size: 0.85rem;
}
.dp-search:focus { outline: none; border-color: var(--amber); }
.dp-list { max-height: 18rem; overflow-y: auto; display: flex; flex-direction: column; }
.dp-item {
  --hov-bg: var(--amber); --hov-fg: var(--on-amber);
  display: flex; align-items: center; gap: 0.4rem; width: 100%;
  padding: 0.32rem 0.45rem; border: none; border-radius: var(--radius);
  background: none; color: var(--text); font: inherit; font-size: 0.85rem; text-align: left; cursor: pointer;
}
.dp-item:hover { background: var(--hov-bg); color: var(--hov-fg); }
/* A ticked row keeps its own quiet ground and retunes the PAIR, so a hover cannot take one half and
   leave the other — the mistake that made a selected dropdown row unreadable elsewhere. */
.dp-item.on { background: rgba(255, 106, 26, 0.18); color: var(--amber-hi);
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); }
.dp-hash { color: var(--muted); }
.dp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dp-parent { color: var(--muted); font-size: 0.72rem; }
.dp-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.dp-warn {
  flex: none; width: 15px; height: 15px; line-height: 15px; text-align: center; border-radius: 50%;
  background: var(--c-trap); color: var(--on-amber); font-size: 0.68rem; font-weight: 700; cursor: help;
}
.dp-empty, .dp-loading { padding: 0.6rem 0.45rem; color: var(--muted); font-size: 0.82rem; line-height: 1.4; }
.dp-foot { display: flex; justify-content: flex-end; padding-top: 0.3rem; }

/* The Settings nav entry uses the same dropdown Plugins does — including its arrow, which
   `.anav-dd.has-dd > .anav-btn::after` already draws. Adding a chevron of my own gave every one of
   them two. */
#settings-nav[hidden] { display: none; }

/* Menu entries are links, so they keep middle-click and "open in new tab" — they must still look and
   behave like the buttons beside them. */
.anav-dd-menu a.anav-dd-item { text-decoration: none; }

/* ── Header language menu ──────────────────────────────────────────────────
   A bare list of flags did not say WHAT it switches — the panel, the game server and Discord all
   have a language and all three are a few clicks apart. The menu names itself, marks the current
   choice with a tick rather than colour alone, and states coverage where a language is partial. */
.lang-dd-list { min-width: 15rem; }
.lang-dd-head {
  padding: 0.4rem 0.6rem 0.45rem; margin-bottom: 0.15rem;
  border-bottom: 1px solid var(--line);
  color: var(--muted); font-family: var(--ui); font-size: 0.68rem;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.lang-dd-item .lang-dd-name { flex: 1 1 auto; min-width: 0; }
/* Not a warning — a language at 60% simply falls back to English for the rest. */
.lang-dd-pct {
  flex: none; color: var(--muted); font-family: var(--mono); font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
}
.lang-dd-tick { flex: none; width: 0.95rem; height: 0.95rem; opacity: 0; }
.lang-dd-item.active .lang-dd-tick { opacity: 1; }
.lang-dd-item.active { color: var(--amber-hi); font-weight: 600; }
/* Both halves of the hover pair travel together — set on the element, never one side in :hover. */
.lang-dd-item { --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); }
.lang-dd-item:hover .lang-dd-pct { color: inherit; }
.lang-dd-foot {
  margin-top: 0.25rem; padding: 0.45rem 0.6rem 0.15rem;
  border-top: 1px solid var(--line);
}
.lang-dd-foot a {
  color: var(--muted); font-family: var(--ui); font-size: 0.76rem; text-decoration: none;
}
.lang-dd-foot a:hover { color: var(--amber-hi); text-decoration: underline; }

/* ── Which languages a surface offers ──────────────────────────────────────
   Under each language dropdown. The list is not fixed — a language appears once it has strings for
   that surface — and without saying so, a missing one reads as a bug. */
/* Column 1, under the help text — this is explanation, and the control column is a narrow strip that
   wrapped the chips into a ragged block while two thirds of the row sat empty. */
.lang-offer { grid-column: 1; display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.45rem; }
.lang-offer-lead { color: var(--muted); font-family: var(--ui); font-size: 0.76rem; line-height: 1.45; }
.lang-offer-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.lang-offer-chip {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.16rem 0.42rem; border: 1px solid var(--line); border-radius: 999px;
  background: var(--panel-2); color: var(--text);
  font-family: var(--ui); font-size: 0.72rem; white-space: nowrap;
}
.lang-offer-chip .lang-flag { width: 1.1em; height: 0.825em; }
.lang-offer-chip.partial { border-style: dashed; }
.lang-offer-pct { color: var(--muted); font-family: var(--mono); font-variant-numeric: tabular-nums; }
.lang-offer-link { align-self: start; color: var(--muted); font-family: var(--ui); font-size: 0.76rem; text-decoration: none; }
.lang-offer-link:hover { color: var(--amber-hi); text-decoration: underline; }

/* ── Add / edit a language ─────────────────────────────────────────────────
   This was three native prompt() boxes in a row. The flag was the field that most needed showing and
   was the one you could not see — a two-letter country code typed blind. */
.ssa-dialog.tr-dlg { max-width: 32rem; text-align: left; }
.ssa-dialog.tr-dlg.wide { max-width: 46rem; }
.tr-dlg-bd { margin: 0.35rem 0 1rem; }
.tr-dlg-grid { display: grid; grid-template-columns: minmax(4rem, auto) minmax(0, 1fr); gap: 0.55rem 0.9rem; align-items: start; }
.tr-dlg-grid > label { padding-top: 0.45rem; color: var(--muted); font-family: var(--ui); font-size: 0.8rem; }
.tr-dlg-grid input[type="text"] { width: 100%; }
.tr-dlg-help { margin: 0.3rem 0 0; color: var(--muted); font-family: var(--ui); font-size: 0.74rem; line-height: 1.45; }
.tr-dlg-err { margin: 0.6rem 0 0; color: var(--bad); font-family: var(--ui); font-size: 0.8rem; }
.tr-dlg-err[hidden] { display: none; }

.tr-f-flagrow { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.tr-f-flagrow .tr-dlg-help { flex: 1 0 100%; }
/* A fixed box so the row does not jump as flags of different widths come and go. */
.tr-f-flagprev {
  display: inline-flex; align-items: center; gap: 0.45rem; min-width: 8rem;
  padding: 0.34rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel-2); font-family: var(--ui); font-size: 0.8rem;
}
.tr-f-flagprev .lang-flag { width: 1.5em; height: 1.125em; }
.tr-f-flagname { color: var(--text); }
.tr-f-none, .tr-f-flagprev .tr-f-flagname:only-child { color: var(--muted); font-style: italic; }

.tr-flag-search { width: 100%; margin-bottom: 0.6rem; }
.tr-flag-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-bottom: 0.7rem; }
.tr-flag-hint { flex: 1 1 14rem; color: var(--muted); font-family: var(--ui); font-size: 0.74rem; line-height: 1.4; }
.tr-flag-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: 0.35rem;
  max-height: 24rem; overflow-y: auto; padding: 0.2rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel-2);
}
/* A section title spans the row rather than sitting in a cell of its own. */
.tr-flag-head {
  grid-column: 1 / -1; padding: 0.5rem 0.4rem 0.25rem;
  color: var(--muted); font-family: var(--ui); font-size: 0.68rem;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.tr-flag-empty { grid-column: 1 / -1; padding: 1.2rem; color: var(--muted); font-family: var(--ui); font-size: 0.82rem; text-align: center; }
.tr-flag-cell {
  display: flex; align-items: center; gap: 0.45rem; min-width: 0;
  padding: 0.34rem 0.45rem; border: 1px solid transparent; border-radius: var(--radius);
  background: none; color: var(--text); font-family: var(--ui); font-size: 0.76rem;
  text-align: left; cursor: pointer;
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi);
}
.tr-flag-cell img { width: 1.5rem; height: 1.125rem; border-radius: 2px; object-fit: cover; flex: none; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
.tr-flag-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tr-flag-cell.on { border-color: var(--amber); background: var(--hover-bg); color: var(--amber-hi); }
.tr-flag-cell.custom img { box-shadow: 0 0 0 1px var(--amber); }

/* The dialog's own fields, painted like every other input in the panel. Left to the browser they
   came out white-on-white in a dark modal. */
.tr-dlg input[type="text"] {
  width: 100%; padding: 0.5rem 0.65rem; box-sizing: border-box;
  background: var(--input-bg, rgba(255, 255, 255, 0.025));
  border: 1px solid var(--input-line, rgba(241, 243, 245, 0.18));
  border-radius: var(--radius); color: var(--text); font-family: var(--ui); font-size: 0.88rem;
}
.tr-dlg input[type="text"]:focus { border-color: var(--amber); box-shadow: var(--ring); outline: none; }
.tr-dlg input[type="text"]:disabled { color: var(--muted); cursor: not-allowed; }
/* Room for the help text to be a sentence rather than a column of two-word lines. */
.ssa-dialog.tr-dlg { max-width: 38rem; }
.tr-dlg-grid { grid-template-columns: 5.5rem minmax(0, 1fr); }
/* `.secondary` sets its own display, which beats the [hidden] attribute's — the Edit button stayed
   on screen for every shipped language, offering to edit something that cannot be edited. */
#tr-edit-lang[hidden] { display: none; }

/* `.ssa-dialog` pins width to 440px, so max-width alone did nothing — the flag grid came out two
   columns wide with a scrollbar. Width follows the max here, and both stay under the viewport. */
.ssa-dialog.tr-dlg { width: min(38rem, calc(100vw - 2rem)); }
.ssa-dialog.tr-dlg.wide { width: min(46rem, calc(100vw - 2rem)); }

/* ── Translation paging ────────────────────────────────────────────────────
   Replaces a hard ceiling of 300 with "narrow the search" under it — which told the reader the rest
   existed and gave them no way to reach it. Search finds a known string; translating a language is
   walking it end to end. */
.tr-pager {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
  font-family: var(--ui); font-size: 0.78rem; color: var(--muted);
}
/* The one under the list gets a rule above it and room to breathe. */
.tr-rows > .tr-pager { margin: 0.9rem 1.5rem 0.4rem; padding-top: 0.7rem; border-top: 1px solid var(--line); }
.tr-pager-n { font-variant-numeric: tabular-nums; }
.tr-pager-nav { display: inline-flex; align-items: center; gap: 0.25rem; }
.tr-pager-pos { padding: 0 0.4rem; color: var(--text); font-variant-numeric: tabular-nums; }
.tr-pager .tr-pg {
  min-width: 1.9rem; padding: 0.2rem 0.45rem; font-size: 0.85rem; line-height: 1.2;
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi);
}
.tr-pager .tr-pg:disabled { opacity: 0.35; cursor: default; }
.tr-pager-size { display: inline-flex; align-items: center; gap: 0.35rem; }
.tr-pager-size select { padding: 0.18rem 0.4rem; font-size: 0.78rem; }

/* ── Map export menu ───────────────────────────────────────────────────────
   Two hard-coded buttons became one menu built from what the map actually holds — so it can never
   offer a file that comes back empty, and never miss a layer either. */
.map-exp { position: relative; display: inline-flex; }
.map-exp.hidden { display: none; }
.map-exp-chev { margin-left: 0.3rem; color: var(--muted); font-size: 0.7rem; }
/* FIXED, not absolute, and placed from the button's own rectangle.
   Absolute inside the side column made the menu part of that column's scrollable area: it pushed
   the column wider and opened off to the left. Fixed takes it out of the layout altogether, which
   is what the Discord channel picker already does for the same reason. */
.map-exp-menu {
  position: fixed; z-index: 90;
  width: 16rem; max-width: calc(100vw - 16px); max-height: 22rem; overflow-y: auto;
  display: flex; flex-direction: column; gap: 1px; padding: 0.3rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius);
  box-shadow: var(--elev-2);
}
.map-exp-menu[hidden] { display: none; }
.map-exp-sep { height: 1px; margin: 0.25rem 0.2rem; background: var(--line); }
.map-exp-item {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.36rem 0.5rem; border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.8rem; text-decoration: none;
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi);
}
.map-exp-c { color: var(--muted); font-family: var(--mono); font-size: 0.74rem; font-variant-numeric: tabular-nums; }
.map-exp-item:hover .map-exp-c { color: inherit; }

/* ── Condition as a dot ────────────────────────────────────────────────────
   How the game itself shows it. In a list a bar is the wrong shape: it takes a column of its own and
   pushes the name and the count about, and forty rows of bars answer "how damaged is each one" when
   the question is "which of these is damaged". The exact figure is the tooltip. */
.hpd {
  flex: none; width: 0.5rem; height: 0.5rem; border-radius: 50%;
  /* A ring, so the dot reads on a light row and a dark one alike, and so a pale band still has an
     edge rather than dissolving into the row it sits on. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);
}
/* The SAME three tokens the bar uses. Invented names would have silently fallen back to literals
   and the dot and the bar would have drifted into two different greens. */
.hpd-ok { background: var(--online); }
.hpd-warn { background: var(--standby); }
.hpd-bad { background: var(--alarm); }
/* The row lays out around it: picture, dot, name, count. The dot sits with the picture rather than
   trailing the name, so the column of dots is scannable straight down the list. */
/* Dot AND figure. The dot is what the eye reads down a column of forty rows; the number is what an
   admin acts on — a colour alone means hovering every row to learn whether "warn" is 58% or 27%.
   Together they take about the width the bar took by itself. */
.hpd-w {
  flex: none; display: inline-flex; align-items: center; gap: 0.3rem;
  min-width: 3.1rem;                 /* so the names line up whatever the figure is */
  font-family: var(--mono); font-size: 0.7rem; font-variant-numeric: tabular-nums;
}
.hpd-n { color: var(--muted); }
/* The figure takes the band colour too, but muted — at a glance it is the dot that carries the
   state; the number is there to be read, not to shout. */
.hpd-ok-t { color: var(--online); opacity: 0.85; }
.hpd-warn-t { color: var(--standby); }
.hpd-bad-t { color: var(--alarm); }
.mp-c-row .hpd-w { margin-right: 0.1rem; }

/* Find an item inside a container. Offered once there is enough to need one — looking for one thing
   on somebody ("does this player have a lockpick") is a real question, and forty rows is where the
   eye stops being the right tool. */
.mp-c-find {
  width: calc(100% - 1rem); margin: 0.4rem 0.5rem 0.35rem; padding: 0.34rem 0.55rem; box-sizing: border-box;
  background: var(--input-bg, rgba(255, 255, 255, 0.025));
  border: 1px solid var(--input-line, rgba(241, 243, 245, 0.18));
  border-radius: var(--radius); color: var(--text); font-family: var(--ui); font-size: 0.8rem;
}
.mp-c-find:focus { border-color: var(--amber); box-shadow: var(--ring); outline: none; }
.mp-c-hide { display: none !important; }

/* ── Server history charts ────────────────────────────────────────────────
   Plain SVG, no charting library: the panel ships inside the exe behind a strict CSP, so a CDN
   dependency would have to be vendored and maintained for a line, a fill and a hover readout.

   Styled to sit with the REST of the panel rather than inventing a look: the range picker is the
   same pill filter the Plugins tab uses (it selects a view, it does not act, so full amber blocks
   next to Start/Stop were wrong), and the chart surface is the same soft inset the cards use.
   Every colour is a theme token, so both themes are covered by construction. */
.hist-bar { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; margin: 0.1rem 0 0.9rem; }
.hist-span { font-size: 0.75rem; color: var(--faint); }

.hist-charts { display: grid; gap: 1.1rem; }
.hist-chart { min-width: 0; }
.hist-head {
  display: flex; align-items: baseline; gap: 0.7rem; flex-wrap: wrap;
  margin: 0 0 0.35rem; font-size: 0.78rem;
}
.hist-title { font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
/* WRAPS. Six keys in one unbreakable row measured 446px against a 360px phone and dragged the
   whole page sideways — the chart legend was the "graf a jeho popisky jedou mimo stranku".
   `.hist-head` wraps its own children, which does nothing for the buttons inside this one. */
.hist-legend { display: inline-flex; flex-wrap: wrap; gap: 0.7rem; min-width: 0; }
/* The legend IS the control — each key is a button that drops or restores its line. A switched-off
   key stays listed and dimmed rather than vanishing: a control you cannot see is one you cannot undo. */
.hist-key {
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: transparent; border: 0; padding: 0.1rem 0.2rem; border-radius: var(--radius);
  color: var(--muted); font: inherit; font-size: 0.75rem; cursor: pointer;
  --hov-bg: transparent; --hov-fg: var(--text); --hov-line: currentColor;
}
.hist-key.off { color: var(--faint); }
.hist-key.off i { opacity: 0.25; }
.hist-key.off { text-decoration: line-through; text-decoration-thickness: 1px; }
.hist-key[disabled] { cursor: default; opacity: 0.45; }
.hist-key i { width: 0.85rem; height: 0.16rem; border-radius: 1px; display: inline-block; }
/* The readout sits at the right of its own line, so numbers appearing on hover never reflow the
   title or the legend — the chart must not twitch while the pointer moves across it. */
.hist-read { margin-left: auto; font-family: var(--mono); font-size: 0.75rem; color: var(--text); white-space: nowrap; }
.hist-read b { font-weight: 600; }
.hist-when { color: var(--faint); margin-right: 0.35rem; }

.hist-svg {
  width: 100%; height: 360px; display: block;
  background: rgba(255, 255, 255, 0.018);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
}
.hist-grid { stroke: var(--line-2); stroke-width: 1; }
.hist-axis { fill: var(--muted); font-family: var(--mono); font-size: 11px; }
.hist-axis-r { text-anchor: start; }
/* The axis NAMES, so "14" on the left and "60" on the right are not two anonymous ladders. */
.hist-unit { fill: var(--faint); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; }
.hist-time { font-size: 11px; }
/* The clock under each date — the detail, not the row. */
.hist-clock { fill: var(--faint); font-size: 10px; }
.hist-line { fill: none; stroke-width: 1.75; stroke-linejoin: round; stroke-linecap: round; }
.hist-fill { opacity: 0.10; stroke: none; }
.hist-cursor { stroke: var(--amber); stroke-width: 1; opacity: 0.55; }
/* The span being dragged out. Drawn under the pointer, so it must not swallow the pointer events
   that are drawing it. */
.hist-sel { fill: var(--amber); opacity: 0.13; pointer-events: none; }
/* `touch-action: none` is what lets the drag-to-select-a-time-range gesture finish on a phone:
   without it the browser claims a vertical swipe for page scrolling and fires `pointercancel`,
   so the drag never ends and the readout never appears. The chart is short enough that the
   page can still be scrolled from either side of it. */
.hist-svg { cursor: crosshair; touch-action: none; }
.hist-reset {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.28rem 0.7rem; border-radius: 999px;
  border: 1px solid var(--line-amber); background: transparent;
  color: var(--amber); font: inherit; font-size: 0.75rem; cursor: pointer;
  --hov-bg: transparent; --hov-fg: var(--amber-hi); --hov-line: currentColor;
}
.hist-reset .ico { width: 11px; height: 11px; }
.hist-reset[hidden] { display: none; }
.hist-empty { color: var(--faint); font-size: 0.8rem; margin: 0.4rem 0 0; }

/* Where a searched-for item was FOUND. The point of searching inside boxes is knowing which box, so
   the location travels with the hit rather than being something you go and look for. */
.mp-c-where {
  margin-left: 0.4rem; padding: 0.05rem 0.4rem;
  border: 1px solid var(--line); border-radius: 999px;
  font-size: 0.68rem; color: var(--muted); white-space: nowrap;
}
.mp-c-hit .mp-c-name { flex: 0 1 auto; }

/* ── touch: nothing smaller than a fingertip ─────────────────────────────────────────────
   44px is the accessibility floor and roughly the width of an adult index finger. Everything
   below was between 11 and 36px — including several DESTRUCTIVE controls (remove a plan,
   delete a chip, close a modal), which is the worst possible thing to make hard to hit
   accurately. The glyph inside keeps its size; only the hit area grows. Plugin stylesheets
   load into this same document, so their icon buttons are covered here too. */
@media (max-width: 760px) {
  /* BOTH pager selectors, and the second one is not redundant. `.roster-pager .pg-n` alone covered
     the page NUMBERS and not the ‹ Prev / Next › beside them, which are bare `.secondary` — so half
     of every pager on this panel (five of them, one `pagerHtml()`) sat at 39px under a block
     promising 44. Widening it to `.roster-pager button` alone then made the numbers WORSE, 43×39 to
     31×44: that selector weighs (0,1,1) and the component's own `.roster-pager .pg-n { min-width:
     2rem }` weighs (0,2,0), so the 32px floor won the width back. So the pager is covered by TWO
     rules — `.pg-n` here for the width, `.roster-pager button` below for the height — and the first
     has to keep naming the class or the component wins that axis again.

     ⚠ AND ONLY THE NUMBERS GET THE WIDTH. `min-width: <length>` on a FLEX ITEM replaces its
     automatic content floor — the `min-width: auto` that is the only thing stopping a flex item
     shrinking below its own text — so 44px on ‹ Prev / Next › lets the browser cut the word in half
     at a narrow width or in a longer language. A page number is one or two digits and has nothing
     to lose; a word does. The arrows are in the min-height rule below instead. */
  /* ⚠ `.map-zoom-btn` IS NOT `.map-zoom-b`, AND THE THREE LETTERS ARE WHY IT WAS MISSED. The
     second is the island's own zoom stack and has been on this list since it was written; the
     first is the four icon-only buttons around the map — reset the view, clear the player filter,
     clear the search, close the game chat — which take their size from `padding: 0.3rem` around a
     15px glyph and come out 27px wide. The blanket floor below gives them 44px of HEIGHT and never
     a width, so they were 27×44 on every phone, and `#map-zoom-reset` is on screen whenever the
     map is. All four are icon-only, which is exactly the case the warning above says min-width is
     safe for: there is no word here to cut in half. */
  button.btn-icon, .gs-tab, .gs-mode-btn, .map-zoom-b, .map-zoom-btn, .roster-pager .pg-n,
  .trmap-gsector, .amodal-close, .pd-close, .ul-del, .rr-admin-btn,
  .ck-x, .ck-del, .ck-chip-x, .mp-chip-x, .bs-chip-x, .vr-x, .ee-pop-x, .ee-mini {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  /* The map card's ✕ reaches the same floor through the card's own variable rather than through a
     `min-width` here. It is the same 44px, but the card computes two other things from that number
     — where the button sits so the glyph lands on the card's inner edge, and how much room the
     title has to leave so it cannot run underneath — and a `min-width` that silently enlarged the
     box left both of those solving for a 2rem button. Which is what happened: the title's clearance
     was a hardcoded 1.4rem and the ✕ overlapped it by ~9px on every phone. */
  .map-popover { --mp-close-box: 44px; }
  /* ‹ Prev / Next › — see the width warning above. Height only, because these carry a word. */
  .roster-pager button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
  /* ⚠ `.tr-pager-size select` USED TO BE HERE AND IT WAS A DEAD RULE, the same shape as
     `.roster-pager .pg-n` above and found the same way — by driving it. `enhanceSelect()` replaces
     EVERY `<select>` in this panel with a `.cdd` wrapper: the real select becomes
     `.cdd-native`, `position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none`,
     and what a reader presses is a `button.roster-select.cdd-btn` beside it. So this floor moved a
     control nobody can see by 43 pixels and left the visible one at 37, for as long as it existed —
     and the label around both measured 37, which is how it was noticed at all. The button is
     covered by the floor below, where every other button on this panel is. */
  /* The rail that collapses the map's side panels was 16px tall when laid flat, then 28 — which is
     over WCAG's 24px minimum and under this panel's own 44, and it stood outside the floor below as
     "deliberate" until somebody drove it. Slop is not available here, because what is on the other
     side of this rail is the MAP: eight invisible pixels of rail over the island is a pan gesture
     that does not pan. So it is the box that grows. */
  .map-rail { flex-basis: 44px; height: 44px; }
  /* The shared footer, which is on EVERY screen this panel has — so it was 25px tall on every one
     of them. "App status" is a real button, the three social entries are real links, and all four
     sat at the line-height of 0.8rem text with no padding at all. Height only: they carry words. */
  .foot-link, .foot-update, .af-social a {
    min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem;
  }
}

/* ══ THE PANEL'S OWN TOUCH FLOOR ═════════════════════════════════════════════════════════════════
   The block above names controls one at a time, and a named list only ever covers what somebody
   remembered to name. Driven at a phone width on 2026-09-18 the panel came back with **438 controls
   under 44px across 106 distinct shapes** — the map's two rails 15 times each, its two arm buttons
   15, `.secondary` 34, the plugin manager's ✕ 12, the zone editor's order arrows at 19×10. Every one
   of them had a stylesheet rule that was right about how the control LOOKS and silent about how big
   it is under a thumb, which is why a list cannot be the answer: the next control is written the
   same way and joins the backlog the day it ships.

   So the floor is stated ONCE, for the kinds of thing a finger has to find, and the named list above
   is left alone — it carries `min-width` for icon buttons, which this cannot (see the warning below).
   `check-panel-render`'s `tap` rule is ON for this surface now and has no `allow` list, so a control
   that cannot reach 44 has to say why in CSS rather than in a config key.

   ⚠ `:where()` WEIGHS NOTHING, and that is what makes a blanket rule safe here. At its own
   specificity this would beat components it knows nothing about; at zero it only reaches a control
   whose own stylesheet never mentioned a height, and a component that means its size says so in one
   declaration. The same reasoning, and the same shape, as THE PLUGIN WORKSPACE FLOOR below.

   ⚠ IT IS A `min-height`, NEVER A `height` OR A BLANKET `min-width`. A min-height only ever moves a
   control that was already too small. A `min-width` on a FLEX ITEM replaces the automatic content
   floor that is the only thing stopping a word being cut in half — the pager two blocks up pays for
   that lesson — so width is floored only on controls that carry a GLYPH rather than a word, named
   below. And `height` would flatten a dense form, which is the normal shape of this panel.

   ⚠ A TRACK WITH AN ABSOLUTELY POSITIONED SLIDER IN IT CANNOT TAKE A HEIGHT FLOOR. `.pl-switch` and
   `.eco-switch` are 40×22 labels whose visible part is a `position: absolute; inset: 0` pill — a
   min-height of 44 does not grow the target, it draws a fat 44px pill. Those two are excluded here
   and take transparent slop on a pseudo-element instead, which is the idiom this project already
   uses for a map marker and which the rig reads (it resolves a positioned pseudo's used size). */
@media (max-width: 760px) {
  /* `button` is already `inline-flex; align-items: center; justify-content: center` at the top of
     this file, so a height floor lands on a centred control without a display rule here. A
     `<summary>` must never be given one at all — `display: list-item` is what draws the disclosure
     triangle — so it is centred with `align-content`, which does not touch layout mode. */
  :where(button:not(.mp-owner), summary, [role="button"], [role="tab"], select, textarea,
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="hidden"]),
    label:has(input:not([type="hidden"])):not(.pl-switch):not(.eco-switch),
    label:has(select), label:has(textarea),
    a.pl-vtab, a.prem-manage, a.lang-offer-link, a.secondary) {
    min-height: 44px;
  }
  :where(summary) { align-content: center; }
  /* A tick box's caption is its target — tapping the word toggles the box — so the label is what
     grows, and it has to be laid out as something that can HAVE a height. Every such label in this
     panel already is; this is for the one somebody writes next year. */
  :where(label:has(input[type="checkbox"]), label:has(input[type="radio"]),
    a.pl-vtab, a.prem-manage, a.lang-offer-link) {
    display: inline-flex; align-items: center;
  }
  /* A TABLE CELL IGNORES `min-height`, and its `height` means a minimum — so this is the same floor
     spelled the way the one element that cannot take `min-height` understands it. It must NOT be
     given a `display`: that takes the cell out of the table's layout and stacks the headers over an
     unwrapped row, which is exactly what the plugin floor below did once and no rule could report. */
  .data-table th.th-sort { height: 44px; }

  /* ── the glyph controls, where a width floor is safe because there is no word to cut ──────────
     Each of these is an icon, an arrow or a single character. The list above covers their height;
     these were 17 to 33px WIDE. */
  .pl-modal-x, .sched-x, .zw-x, .zw-add, .zw-arrow, .zw-step button, .tr-pager .tr-pg,
  .eco-item-info, .mp-c-info, .eco-arow-info, .cmd-ip-i, .gl-preview-more,
  .eco-pick, .eco-selall, .dp-raw, .cs-chip-x, .cs-saved-x {
    min-width: 44px;
  }
  /* The history range chips are the one row where a width floor is NOT safe — `.pl-filter` is an
     `inline-flex` with `overflow-x: auto`, so its children really can shrink, and `min-width` on a
     flex item replaces the `min-width: auto` that is the only thing stopping "Bridge itself 1"
     being cut in half. "7d" was 43px: the missing pixel is padding, so padding is what it gets. */
  .pl-fbtn { padding-inline: 0.95rem; }
  /* A `<tr role="button" tabindex="0">` — the audit log's rows open their own detail — and a table
     row takes `height` as a minimum, exactly like the header cell above. Never a `display`. */
  .audit-table tbody tr.audit-row { height: 44px; }
  /* `.tr-row textarea { min-height: 1.9rem }` is (0,1,1) and the floor is (0,0,0), so the component
     wins and every box on the translations screen stayed 32px. This is the same floor at a
     specificity that can be heard. */
  .tr-row textarea { min-height: 44px; }
  /* The two order arrows are stacked in a 20px column, and no two targets 10px apart can both be
     44px — the honest answer is to lay them side by side on a phone rather than to pretend.
     ⚠ `.zw-row` IS IN THE SELECTOR BECAUSE `.zw-ord { flex-direction: column }` IS DECLARED 1,900
     LINES BELOW THIS ONE. Equal specificity is settled by order, and a media query does not raise
     it — so the first version of this rule changed nothing at all and the row simply became 88px
     tall with one arrow at each end of it. */
  .zw-row .zw-ord { flex-direction: row; }
  /* …and the name gets the width back by WRAPPING rather than by ellipsis. Three 44px glyphs plus a
     262px panel leaves about 110px for the name, and `text-overflow: ellipsis` spent it badly: the
     regions read "Admin Ba…" and "Outpost …", which is the picker losing exactly the part that
     tells two outposts apart. Two lines of 0.78rem fit inside the 44px the row already has, so this
     costs no height at all for any name short enough to have fitted before. */
  .zw-row .zw-nm {
    white-space: normal; overflow-wrap: anywhere; line-height: 1.15;
    overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  }

  /* ── the three that must stay the size they are, and take transparent slop instead ────────────
     A switch's track is its picture; an inline link and the map card's owner button sit INSIDE a
     sentence, where a 44px box would push the line apart. `getComputedStyle` resolves a positioned
     pseudo's used width and height, so the rig measures the hit area rather than the paint. */
  .pl-switch, .eco-switch, .mp-owner, .sq-member-link, #prem-portal-link { position: relative; }
  .pl-switch::after, .eco-switch::after { content: ''; position: absolute; inset: -11px -2px; }
  /* ⚠ THE SLOP REACHES LEFT ONLY, AND THE FIRST VERSION REACHED BOTH WAYS AND WAS REPORTED AS TWO
     DEFECTS THAT LOOK NOTHING LIKE IT. An absolutely positioned pseudo extends its ancestor's
     SCROLL AREA to the right: `.map-popover .mp-body` began scrolling sideways by 4px, and the
     button's own `scrollWidth` grew by 10, which the rig reads as "text is cut off rather than
     wrapping". Left overflow is not scrollable in LTR and costs neither. What it overlaps is the
     "Owner" caption in front of the name, which opens the same card. */
  .mp-owner::after { content: ''; position: absolute; inset: -12px 0 -12px -22px; }
  /* The BOSS's name in the squad card's ID grid, and it is the same shape as `.mp-owner` one screen
     along: a person's name standing where a value goes, under a caption that is part of the same
     cell. A short name measures about 41px wide, and giving it a 44px box would turn a name into a
     button in a grid of plain readings. Left-only, for the scroll-area reason above; the 22px it
     reaches over is its own "Boss" caption, which nothing else is under. */
  .sq-member-link::after { content: ''; position: absolute; inset: -12px 0 -12px -22px; }
  #prem-portal-link::after { content: ''; position: absolute; inset: -14px -2px; }
  /* A native tick box takes neither padding nor a size that keeps its glyph: Chrome ignores padding
     on one outright (measured — 18×18 with 12px of it) and scales the drawn box to `width`/`height`,
     so a 44px checkbox is a 44px PICTURE of a checkbox. It does render a pseudo-element, which is
     what this leans on. The settings forms carry hundreds of these, so this is the shape that
     matters most and the one with the least room to change how it looks. */
  /* ⚠ AND ITS 14px OF REACH WAS AUDITED RATHER THAN ASSUMED, because a reach that covers the
     neighbour's own picture trades a miss for a WRONG ACTION and this is the screen where a tap
     writes a config. `check-panel-render`'s `hitlap` rule drives every settings form at 310px and
     measures it: 45 tick boxes on the in-game mods screen, 38 on the discord form, 93 on
     ServerSettings.ini, and NOT ONE of them reaches over another control's own box. A settings row
     is label / description / control with the field's own 0.8rem of padding under it, so the
     nearest thing below a tick box is about 30px away and the nearest thing beside it is its own
     caption.

     ⚠ ITS FIRST AUDIT SAID THE OPPOSITE AND WAS MEASURING A LAYOUT THAT NO LONGER EXISTED — see
     the note beside `checkVisibility` in build/lib/render-rig.js. Two "fixes" were written against
     those numbers, a margin and then extra padding, and both are gone: the defect was in the
     instrument. */
  .settings-field input[type="checkbox"] { position: relative; }
  .settings-field input[type="checkbox"]::after {
    content: ''; position: absolute; inset: -14px;
  }
}

/* ══ THE PLUGIN WORKSPACE FLOOR ══════════════════════════════════════════════════════════════════
   A plugin's own tab is a PAID surface drawn inside this document, and until this block existed it
   inherited the panel's phone backlog wholesale: measured across the 37 plugin views, 76 distinct
   control shapes came back under 40px — a 22px chevron, a 22px switch, token chips at 19 to 27px,
   and one text box 14 PIXELS WIDE that nobody could type into.

   Forty-four of those 76 were not the plugin's doing at all. They were controls this panel hands a
   plugin and then draws itself: the generic button (39px — one pixel short, twelve times over),
   `SSA.table`'s sortable header, the `.switch`, the `.cdd-btn` dropdown, the workspace's own tab
   strip, the footer above. So the floor belongs HERE, where one rule fixes every plugin at once and
   covers a plugin somebody writes next year, rather than in eight stylesheets that each have to
   remember.

   ⚠ IT IS A `min-height`, DELIBERATELY, AND NOT A BLANKET `height`. A dense form is the normal shape
   of a plugin's settings screen and a fixed height would wreck one; a floor only ever moves a
   control that was already too small. And it is scoped to `#pl-ws-content` plus the workspace chrome
   rather than written as `button { … }` — which mattered while the panel around it still carried its
   own backlog, and matters less now that THE PANEL'S OWN TOUCH FLOOR above covers the rest. The two
   are kept apart on purpose: this one is 44px for a PAID third-party surface and answers to
   `plugtap`, which no view can narrow, while the block above answers to `tap`. Neither inherits the
   other's exceptions.

   ⚠ WIDTH IS NOT FLOORED HERE AND THAT IS THE HONEST GAP. `min-width` on a flex item replaces the
   automatic content floor that is the only thing stopping a word being cut in half (the pager above
   pays for that lesson), so a blanket width floor would trade an unreachable control for an
   unreadable one. An icon-only button — a chevron, a ✕, a "?" — therefore states its own 44px in its
   own stylesheet, and `plugtap` in build/lib/render-rig.js is what stops the next one shipping
   without it: that rule is ON for every plugin view and has no way to be switched off. */
@media (max-width: 760px) {
  /* ⚠ `:where()` WEIGHS NOTHING, AND THAT IS THE ESCAPE HATCH RATHER THAN A WEAKNESS. Written at its
     own specificity this rule is (1,1,2) and a plugin could not answer it at all except with
     `!important` — which matters, because some controls are RIGHT to stay small on screen and the
     honest fix for those is transparent slop on a pseudo-element, not a bigger box. At zero weight
     the floor still reaches every control that does not state a `min-height` of its own (a plugin's
     `height: 22px` does not answer it — `min-height` clamps `height`, whatever the order), and a
     plugin that means it says so in one declaration. `plugtap` then judges the result either way. */
  :where(#pl-ws-content) :where(
    button, select, textarea, [role="button"],
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="hidden"]),
    label:has(input:not([type="hidden"])), label:has(select)),
  :where(.pl-ws-tab, .pl-ws-back) {
    min-height: 44px;
  }
  /* Text inside a control that has just grown has to be told where to sit, or it stays at the top of
     a 44px box and the control reads as misaligned rather than as roomy. `:where()` weighs nothing,
     so a plugin that lays its own button out as a grid or a column keeps its rule.

     ⚠ A TABLE CELL IS EXCLUDED, AND IT WAS NOT AT FIRST — WHICH BROKE EVERY PLUGIN TABLE ON A PHONE.
     `SSA.table` builds its sortable headers as `<th role="button" tabindex="0">`, quite correctly:
     they are pressed, and that is what tells a screen reader so. But `display` on a table cell is
     not decoration — it takes the cell OUT of the table's layout, so four `<th>`s became four
     inline-flex boxes that wrapped into three stacked lines above a body row that had not wrapped at
     all. The header said `#` / `TYPE LOCATION` / `PLACER` over a row reading `4411
     ImprovisedMine`, and nothing could report it: the cells still fit, nothing was clipped, and the
     table was inside its own scroller so the page never went sideways. Found by looking at the
     rendered page. The height floor above is safe on a cell; a `display` is not. */
  :where(#pl-ws-content button, #pl-ws-content [role="button"]:not(th, td), .pl-ws-tab, .pl-ws-back) {
    display: inline-flex; align-items: center; justify-content: center;
  }
  /* A BARE SWITCH IS THE ONE CONTROL WHOSE WIDTH IS SAFE TO FLOOR. A switch with a caption is
     already wider than 44px and the floor does nothing to it; one without is a 2.35rem track and
     nothing else, so there is no word to cut. */
  #pl-ws-content .switch { min-width: 44px; }
  /* A TABLE CELL IGNORES `min-height`. `SSA.table`'s sortable headers are `th`s, and the CSS box
     model gives a table cell's `height` the meaning of a minimum — so this is the floor, spelled the
     way the one element that cannot take `min-height` understands it. `min-width` is safe HERE and
     nowhere else in this block: a table cell is not a flex item, so a width floor cannot let its
     caption be cut in half — it widens the column instead, which is what a one-character header
     like `#` (43px, one pixel short) needs. */
  #pl-ws-content .data-table th.th-sort { height: 44px; min-width: 44px; }
  /* ⚠ A `<summary>` MUST NOT BE GIVEN A `display`, AND THE FIRST VERSION OF THIS RULE GAVE IT ONE.
     The disclosure triangle is generated by `display: list-item` and nothing brings it back — so
     `display: flex` (which is how every other control here is centred) silently deletes the arrow
     from every `<details>` a plugin ships. Restoring it with a `::before` looked like the answer and
     is worse: `commands-kits` turns its own marker OFF on purpose and replaces it with a bulb icon,
     so the panel drew a second, stray arrow beside it — a rule that fires on correct markup.

     `align-content` centres block content without touching layout mode, so the arrow, the plugin's
     own `list-style: none`, and anything it put in the summary all survive untouched. Two plugins
     ship a bare `<summary>` today at 20px and 33px; this is what the next one gets for free. */
  #pl-ws-content summary { min-height: 44px; align-content: center; }
}

/* ── the two tables where the content column lost to the metadata ───────────────────────
   Chat and the log viewer inherit `min-width: 640px`, and spent ~65% of it on a timestamp and
   a category before the message — the one thing anyone opens those tabs to read — even
   started. It is inside a scroll container, so nothing overflowed the page; you simply had to
   swipe sideways on every single line.

   ⚠ THAT FIX WAS WRITTEN AND HAS NEVER ONCE RUN. It selected `#chat-table` and `#gamelog-table`,
   and NOTHING IN THIS PRODUCT HAS EITHER ID: both tables are built in `app.js` as
   `<table class="data-table roster-table chat-table …">` with no id at all, and the containers
   around them are `#chat-history` and `#gamelog-list`. So the rules matched nothing, the media
   query was inert, and on a 320px phone both screens rendered a full-width timestamp, a `CATEG`
   header cut mid-word, category chips clipped to two letters — and the log line and the chat
   message, the entire content of both tabs, off-screen to the right. Rows stood at three times
   their height because the invisible text was wrapping inside a column nobody could see, so even
   the striping stopped lining up with the rows.
   Every rule was correct CSS and no rule was reached. `check-mobile` reads declarations, and these
   declarations are fine; `check-panel-render` is the only thing that can see a selector that
   matches nothing, and it does now — `chat · phone` and `log viewer · phone` measure the message
   column itself.

   The layout is a STACK rather than a narrower table. There is no width at which four columns of
   metadata and a sentence share 310px, and the columns are not equals: time and channel are how
   you place a line, the line is why you opened the tab. So on a phone each row becomes a small
   card — the metadata on one quiet line, the content underneath at the full width of the screen. */
/* ⚠ **860 RATHER THAN 640, AND THE NUMBER IS MEASURED.** Four columns with a whole log line
   among them stop being a table long before a phone: at 640 the timestamp column was being
   squeezed to 174px and cut to 89% of itself, so a log about WHEN things happened lost the
   time off its dates on every tablet. The stacked form below has no columns to squeeze. */
@media (max-width: 860px) {
  .chat-table.roster-table { min-width: 0; }
  /* `table-layout: fixed` and the desktop column widths are both about columns, and there are no
     columns here any more. */
  .chat-table.roster-table, .chat-table.roster-table thead, .chat-table.roster-table tbody,
  .chat-table.roster-table tr, .chat-table.roster-table th, .chat-table.roster-table td {
    display: block; width: auto; table-layout: auto;
  }
  /* The header row goes: it is four captions for a shape that no longer has four columns, and
     `CATEG` cut mid-word was the most visible half of this defect. Sorting is not lost — the Time
     and Category dropdowns above the table are the controls on a phone. */
  .chat-table.roster-table thead { display: none; }
  .chat-table.roster-table tbody tr {
    padding: 0.5rem 0.1rem; border-bottom: 1px solid var(--line-2);
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.5rem;
  }
  .chat-table.roster-table tbody td { border-bottom: 0; padding: 0; }
  /* Time, channel and player sit together on the first line and take only what they need; the
     message and the log line each get a line of their own, all of it. */
  .chat-table.roster-table tbody td.chat-ts,
  .chat-table.roster-table tbody td.chat-ch-cell,
  .chat-table.roster-table tbody td.rr-name { flex: 0 0 auto; }
  .chat-table.roster-table tbody td.chat-msg,
  .chat-table.roster-table tbody td.gamelog-line {
    flex: 1 0 100%; min-width: 0; margin-top: 0.15rem;
    white-space: normal; overflow-wrap: anywhere;
  }
  /* The category chip was clipped to `Lo`, `Kil`, `Bu`, `Ra` by a 4.4rem column that no longer
     exists; it is content-sized now and the cell it lives in must stop hiding it. */
  .gamelog-table td.chat-ch-cell { overflow: visible; }
  .gl-cat-t { overflow: visible; text-overflow: clip; }
}

/* The SDK's dialog footer never wrapped, so three long action labels ran off a 320px box. */
.ssa-dlg-ft { flex-wrap: wrap; }

/* `openModal({ wide: true })` set this class and no stylesheet ever defined it, so every caller
   asking for a wide dialog silently got a normal one. Capped at the viewport so it still fits a
   phone — wide is a desktop affordance, not a licence to overflow. */
.ssa-dlg.wide { max-width: min(1040px, calc(100vw - 40px)); }

/* A segmented pill keeps its shape and scrolls inside itself rather than wrapping into a
   second row of half-pills. The scrollbar is hidden because the control is 30px tall. */
.pl-viewtabs::-webkit-scrollbar, .pl-filter::-webkit-scrollbar { display: none; }
.pl-viewtabs > *, .pl-filter > * { flex: 0 0 auto; }

/* ══ Forty-one in-game mods, three hundred settings ═══════════════════════════════════════
   The bridge shipped three mods once; nine more arrived in a single round. A flat wall of
   cards with a couple of hundred switches in it is not something a person finds anything in,
   so the view narrows four ways — free text over every name, group, label AND hint; a topic;
   "what is on"; "what has a catch" — and every card starts folded to a header row.

   The bar is a plain wrapping flex row: the chips reuse `.pl-filter`/`.pl-fbtn` from the
   plugin type filter directly, so they inherit a hover pair that check-contrast has already
   folded, and the two switches reuse `.pl-btn` with `.primary` as their lit state. Nothing
   here declares a new hover half. */
.mod-bar { display: flex; flex-direction: column; gap: 0.55rem; margin: 0 0 0.9rem; }
.mod-bar-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* Basis, not width: it grows into a desktop row and folds to full width on a phone without
   ever declaring a number wider than the screen. */
.mod-search { flex: 1 1 15rem; min-width: 0; }
.mod-bar-btns { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.mod-cats { flex: 0 1 auto; }
.mod-cat-n { margin-left: 0.3rem; color: var(--faint); font-variant-numeric: tabular-nums; }
.pl-fbtn.active .mod-cat-n { color: var(--on-amber); opacity: 0.75; }
.mod-bar-sum { margin: 0; color: var(--muted); font-size: 0.78rem; line-height: 1.5; }

/* A heading per topic in the unfiltered view, so forty-one cards read as six short lists. */
.mod-sec {
  display: flex; align-items: center; gap: 0.45rem; margin: 0.6rem 0 -0.35rem;
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted);
}
.mod-sec-n { color: var(--faint); font-weight: 600; letter-spacing: 0; }

/* The fold. Ghost, because `button:hover` would otherwise paint the entire title bar amber and
   put dark text on it — the pair travels on the element, never split across a :hover rule. */
.mod-open {
  border: 0; background: transparent; padding: 0.1rem 0; margin: 0;
  font: inherit; letter-spacing: 0; text-align: left; color: var(--text); cursor: pointer;
  --hov-bg: transparent; --hov-fg: var(--amber-hi); --hov-line: transparent;
}
.mod-chev { flex: none; color: var(--muted); font-size: 0.7rem; transition: transform 0.15s; }
.mod-open[aria-expanded="true"] .mod-chev { transform: rotate(90deg); color: var(--amber-hi); }
/* What has to survive the fold: how much is inside, and how much of it has a catch. */
.mod-count, .mod-lim-n, .mod-why, .mod-dirty, .mod-cost-n {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: 0.72rem; font-variant-numeric: tabular-nums; color: var(--muted);
}
/* `flex: none` because these chips now carry WORDS beside the number — "1 kann das Spiel
   verändern" is 26 characters in German — and a flex item's default is to shrink. Without it the
   glyph is the thing that gives way on a 320px phone, and an icon squeezed to four pixels is an
   icon nobody can see, which is the one defect a rendered check can only find by measuring. */
.mod-count .ico, .mod-lim-n .ico, .mod-why .ico, .mod-cost-n .ico { width: 12px; height: 12px; flex: none; }
.mod-lim-n { color: var(--standby); }
/* What it costs to leave ON — drawn only on the nineteen cards that cost something, so the absence
   of one is itself the answer for the other twenty-four. Muted rather than amber: this is a fact to
   weigh, not a warning, and painting it like `.mod-lim-n` would make "runs on a timer" shout as
   loudly as "cannot be undone". `white-space: nowrap` because the labels are phrases, not counts. */
.mod-cost-n { white-space: nowrap; }
/* Where the search landed, on the folded header. The rank is only usable if the reason for it is
   on the card: "in its name" is why this one is first, "3 explanations" is why that one is last. */
.mod-why { color: var(--amber-hi); }
.mod-dirty {
  padding: 0.05rem 0.4rem; border-radius: 999px;
  border: 1px solid var(--line-amber); color: var(--amber-hi);
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.05em;
}

/* ── the honest limits ─────────────────────────────────────────────────────────────────
   Most hints describe a setting. A minority describe something an owner MUST know before
   switching it on: a weather value the game re-rolls within a second, a spawned creature
   that does not survive two minutes and cannot be made to, an admin command whose success
   is unknowable because the game returns void, garden writes that have never once run
   against a live server, a killbox `activated` flag that starts no killbox. Those were the
   same 0.78rem of grey as "off by default", so they read as filler and got skipped.

   They are now a bordered callout with the SHAPE of the limit named on it — never a
   tooltip, never truncated, never behind a click. `modLimitTags()` decides which hints get
   one, and it matches the English the schema ships rather than the translation: the schema
   is a C++ literal that only carries English, so classifying the translated text would mute
   the warning in exactly the languages whose owners cannot go and read the source. */
.mf-limit {
  display: flex; gap: 0.5rem; align-items: flex-start;
  margin: 0.2rem 0 0.5rem; padding: 0.45rem 0.55rem;
  border: 1px solid rgba(230, 178, 58, 0.28); border-left: 3px solid var(--standby);
  border-radius: var(--radius); background: rgba(230, 178, 58, 0.07);
}
.mf-limit-ico { width: 14px; height: 14px; flex: none; margin-top: 0.12rem; color: var(--standby); }
.mf-limit-b { min-width: 0; }
.mf-limit-tags { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.25rem; }
.mf-limit-tag {
  padding: 0.02rem 0.4rem; border-radius: 999px;
  border: 1px solid rgba(230, 178, 58, 0.45); color: var(--standby);
  font-size: 0.63rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  white-space: nowrap;
}
/* Full brightness, unlike a normal hint: this is the sentence the page exists to make someone
   read, and muting it is how it was missed in the first place. */
.mf-limit-t { margin: 0; font-size: 0.78rem; line-height: 1.5; color: var(--text); }
/* An option's own hint gets the same badges inline — the warning is on the choice as often as
   it is on the question ("opens a TCP port to the network" is an option, not a field). */
.mf-opt-lim { border-left: 3px solid var(--standby); }
.mf-opt-h .mf-limit-tags { display: flex; margin: 0.1rem 0 0.2rem; }

/* Where a search landed. Without it a hit on one hint inside a mod of twenty settings only
   says which card to go and read. */
.mf-hit { border-left: 2px solid var(--amber); padding-left: 0.5rem; }

/* The span a number is allowed to take, stated rather than only enforced — a box that
   silently refuses 30 because the floor is 50 is the same box as one that is broken. */
.mf-span { margin-left: 0.4rem; color: var(--faint); font-size: 0.72rem; font-weight: 400; font-variant-numeric: tabular-nums; }

/* Switch, fold and actions share one row; on a narrow screen the actions drop to their own line
   rather than crushing the name into two characters. The fold keeps `flex: 1` from `.mod-title`. */
.mod-head { flex-wrap: wrap; }

/* ══ In-game mods: the way in ══════════════════════════════════════════════════════════
   A first screen that is not a list of switches. Everything below is laid out with the
   panel's own tokens and its existing button shapes, so nothing here declares a hover
   half on its own — the two rules that bite (a bare length in `minmax`, and half a hover
   pair) are avoided by construction rather than by remembering.

   Every grid minimum is `min(100%, N)`. The root font is 106% below 640px, so a bare
   `20rem` is 339px, and inside a card that is `overflow: hidden` the right edge of every
   control in it is clipped off rather than scrolled to. `min()` collapses it to one
   column instead. */

.mod-setup { display: flex; flex-direction: column; gap: 1.1rem; }

.mp-hero { padding: 0.2rem 0 0; }
.mp-hero-t { margin: 0 0 0.3rem; font-size: 1.15rem; font-weight: 700; color: var(--text); }
.mp-hero-p { margin: 0; color: var(--muted); font-size: 0.86rem; line-height: 1.55; max-width: 62ch; }

/* Twenty cards in one grid is the wall this screen exists to replace. The shipped table already
   separates "what kind of server do you run" (asked once) from "I want to do this one thing"
   (asked whenever), so each gets its own heading and its own grid. A table with only one kind in
   it renders as a single unheaded grid, exactly as before. */
.mp-sec { display: flex; flex-direction: column; gap: 0.55rem; }
.mp-sec + .mp-sec { margin-top: 0.4rem; }
.mp-sec-t { margin: 0; font-size: 0.95rem; font-weight: 700; color: var(--text); }
.mp-sec-p { margin: -0.35rem 0 0.1rem; color: var(--muted); font-size: 0.8rem; line-height: 1.5; max-width: 68ch; }

/* 28rem, and the number was MEASURED rather than chosen. The root font is 124%, so the old 20rem
   minimum was 397px and auto-fit put four 451px columns on a 1920 screen and three 446px ones on a
   1440: a card 446 wide and 381 TALL, whose header wrapped onto three lines (77px average, 101 for
   the worst) and whose footer wrapped onto two (58px). Portrait cards full of two-word lines, which
   is exactly what "narrow and stretched downward" describes.
   28rem is 556px, which is where the content stops wrapping: 1920 → 3 × 606px cards, 259px tall,
   header and footer one line each; 1440 → 2 × 677px, 226px; 1366 → 2 × 640px, 238px. The whole goal
   grid also got SHORTER at every width despite having fewer columns (1997→1639px at 1920,
   2371→2161px at 1440), because a card that does not wrap is worth more than a column.
   Still `min(100%, …)`: a bare length in minmax() widens the page instead of collapsing to one
   column, which is the rule check-mobile enforces. */
.mp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr)); gap: 0.8rem; }
.mp-card {
  display: flex; flex-direction: column; gap: 0.55rem; min-width: 0;
  padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--panel-2);
}
/* Already true of this server. Said in green rather than by removing the card: a goal that is
   already applied is still the quickest way to read what it stands for. */
.mp-card.done { border-color: rgba(84, 201, 138, 0.35); }
.mp-top { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; min-width: 0; }
/* A 30px `.mp-ico` used to sit HERE, and it is the third time this one card has been wrecked from
   the far end of this file. It was written for the preset card's own icon; when that icon was moved
   to `.mps-ico` (because its glyph was invisible — see the note at `.mps-ico`) the markup was
   repointed and the rule was left behind. Both `.mp-ico` rules are bare and specificity 0-1-0, so
   the LATER one won: the map's marker plate has been rendering at 30px instead of the 58px declared
   1,200 lines further up, with 20px of a 512px creature portrait inside it.
   Nothing emits `.mp-ico` in the panel except the marker card, so the fix was to delete the rule.
   `check-panel-render` now MEASURES that plate, because neither of the two rules is wrong on its
   own and no check that reads declarations can see which one the browser picked. */
/* `flex: 1 1 8rem` and not `auto`. A wrapping flex container breaks its lines using each item's
   HYPOTHETICAL size — flex-basis before any shrinking — so `min-width: 0` does not save a long
   title: it was landing on its own line UNDER the icon, 101px of header for one heading. A basis
   that fits beside the icon keeps it on the line and lets it wrap inside its own box instead. */
.mp-name { margin: 0; flex: 1 1 8rem; min-width: 0; font-size: 0.95rem; font-weight: 700; color: var(--text); }
.mp-done {
  display: inline-flex; align-items: center; gap: 0.2rem;
  padding: 0.02rem 0.4rem; border-radius: var(--pill);
  border: 1px solid rgba(84, 201, 138, 0.45); color: var(--online);
  font-size: 0.63rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
}
.mp-done .ico { width: 11px; height: 11px; }
.mp-goal { margin: 0; color: var(--muted); font-size: 0.82rem; line-height: 1.55; }
/* What it costs to run, on the card as well as in the preview. "Why is my server lagging" is the
   predictable next question after a one-click setup and the answer was written down all along. */
.mp-cost { display: flex; align-items: flex-start; gap: 0.35rem; margin: 0; color: var(--faint); font-size: 0.76rem; line-height: 1.5; }
.mp-cost .ico { width: 13px; height: 13px; flex: none; margin-top: 0.15rem; }
.mp-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem; margin-top: auto; }
.mp-size { color: var(--faint); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
.mp-lim { display: inline-flex; align-items: center; gap: 0.22rem; color: var(--standby); font-size: 0.72rem; }
.mp-lim .ico { width: 12px; height: 12px; }
.mp-go { margin-left: auto; }

/* The three standing questions under the goals: what must I read, what is on, and where is
   everything. Each row ANSWERS with a number and hands over a button that arrives with the
   matching filter already set, so the answer is not a second question.

   ── Every selector below is scoped to `.mod-setup`, and that is not tidiness ──────────────
   `.mp-row` and `.mp-rows` are ALSO the map's marker-card classes, twelve hundred lines above.
   Written bare these rules reached straight into that card: `.map-popover .mp-row` beats them on
   specificity for the four properties it happens to declare, and everything it does NOT declare
   simply applied. The marker card therefore had, invisibly and from a screen it shares nothing
   with:

     · `flex-wrap: wrap`   — a value that could not fit dropped onto its own line and went
                             LEFT-aligned under its own label. "Last accessed" and "Locked with"
                             read as two broken rows on every card that had them.
     · `border: 1px solid var(--line)` — only the TOP edge was overridden, so every detail row
                             carried a left, right and bottom hairline as well: a stack of boxes
                             where the design is one hairline between neighbours.
     · `background: var(--panel)`, `border-radius`, `align-items: flex-start`
     · `.mp-rows` becoming `display: flex` with a 0.6rem gap, so the hairlines floated in the
                             middle of a gutter instead of separating two rows.

   This is the second time on this one card: `.mp-ico` went the other way, and the note at
   `.mps-ico` records it. The lesson both times is the same — a class name with a component
   prefix belongs to that component, and a second screen that wants the same name has to scope
   itself or rename. `check-css-reach` now fails on the shape rather than trusting the comment. */
.mod-setup .mp-rows { display: flex; flex-direction: column; gap: 0.6rem; }
.mod-setup .mp-row {
  display: flex; align-items: flex-start; gap: 0.7rem; flex-wrap: wrap;
  padding: 0.75rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--panel);
}
.mod-setup .mp-row > .ico { width: 18px; height: 18px; flex: none; margin-top: 0.1rem; color: var(--muted); }
.mod-setup .mp-row-b { flex: 1 1 16rem; min-width: 0; }
.mod-setup .mp-row-b b { display: block; margin-bottom: 0.2rem; font-size: 0.88rem; }
.mod-setup .mp-row-b p { margin: 0; color: var(--muted); font-size: 0.79rem; line-height: 1.5; }
/* The limits row keeps the warning colour it has everywhere else on this page. */
.mod-setup .mp-row-warn { border-color: rgba(230, 178, 58, 0.28); border-left: 3px solid var(--standby); background: rgba(230, 178, 58, 0.05); }
.mod-setup .mp-row-warn > .ico { color: var(--standby); }
.mp-shapes { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.4rem; }
.mp-jump { flex: none; }

/* ── the manager's own set ──────────────────────────────────────────────────────────────
   Above the goals rather than among them, because it is not one. A goal answers "what do you
   want your SERVER to do" and this answers "which of these does the product you are already
   paying for read". Same box as the rows below it, so it does not read as an advertisement;
   the amber glyph and the accent edge are the only thing marking it out.
   Scoped under `.mod-setup` for the reason the long note above records — `.mp-row` belongs to
   the map's marker card and a bare rule here reaches straight into it. */
.mod-setup .mgr-row { margin-bottom: 0.9rem; border-left: 3px solid var(--line-amber); }
.mod-setup .mgr-row > .ico { color: var(--amber-hi); }
.mgr-state { margin: 0.4rem 0 0; font-size: 0.76rem; color: var(--amber-hi); line-height: 1.5; }
.mgr-go, .mgr-back { flex: none; }
/* Continuous or on demand. It is the difference between a switch whose absence is silence and
   one whose absence is a sentence the panel already routes back to the switch, so it belongs
   beside the name rather than in the explanation. */
.mgr-mode { color: var(--faint); font-family: var(--mono); font-size: 0.68rem; font-weight: 400; }
.mgr-why { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.78rem; line-height: 1.5; }
.mgr-note { margin: 0.35rem 0 0; color: var(--faint); font-size: 0.74rem; line-height: 1.5; }
/* What the set deliberately does NOT switch on, listed rather than left out: absent and
   excluded-on-purpose look identical, and only one of them is something an owner can act on. */
.mgr-blocked li { color: var(--faint); }
/* ⚠ A SENTENCE IN A BADGE BUILT FOR ONE WORD. `.mf-limit-tag` is a pill — uppercase, letter-spaced,
   `white-space: nowrap` — and it is right for the tags it was written for ("PERMANENT",
   "AUTHORITY"). What this list puts in it is `MGR_BLOCK`, whose shortest entry is *"This bridge
   build does not have it"* and whose longest is fifteen words. At nowrap that measured **548px
   inside a 310px viewport**, which is what pushed the preview sideways on a phone — and the
   page-level width rule could not see it, because a modal's own `overflow-x: auto` is the first
   ancestor it stops at and reads as intentional.

   Two halves, and both are needed: it has to WRAP, and it has to stop pretending to be a badge —
   uppercase letter-spaced prose is a sentence nobody finishes reading. */
.mgr-blocked .mf-limit-tag {
  white-space: normal; text-transform: none; letter-spacing: 0;
  font-weight: 600; font-size: 0.72rem; line-height: 1.4;
  border-radius: var(--radius); padding: 0.1rem 0.45rem;
  min-width: 0; overflow-wrap: anywhere;
}

/* The preview. Nothing is written until the button at the bottom is pressed, so it has room
   for the whole list rather than a summary of it. */
.mp-modal { width: min(760px, 100%); }
.mp-modal-b { padding: 1rem 1.1rem; overflow: auto; display: flex; flex-direction: column; gap: 0.7rem; }
.mp-modal-goal { margin: 0; color: var(--muted); font-size: 0.85rem; line-height: 1.55; }
.mp-modal-lead { margin: 0; font-size: 0.82rem; color: var(--text); }
.mp-modal-note { margin: 0.2rem 0 0; color: var(--faint); font-size: 0.74rem; line-height: 1.5; }
.mp-modal-f { display: flex; justify-content: flex-end; gap: 0.6rem; padding: 0.8rem 1.1rem; border-top: 1px solid var(--line); }
.mp-modal-cost { display: flex; align-items: flex-start; gap: 0.4rem; margin: 0; color: var(--muted); font-size: 0.8rem; line-height: 1.55; }
.mp-modal-cost .ico { width: 15px; height: 15px; flex: none; margin-top: 0.15rem; color: var(--muted); }
.mp-modal-cost b { color: var(--text); }
.mp-limits { margin: 0; }
.mp-limits .mf-limit-t + .mf-limit-t { margin-top: 0.55rem; }
/* Each limit carries its OWN badges now. The block used to be one heading over a run of
   sentences, so "does not last" and "cannot be undone" read the same — which is the difference
   between a value that resets on restart and somebody's base gone for good. */
.mp-limits .mf-limit-t .mf-limit-tags { display: inline-flex; flex-wrap: wrap; gap: 0.25rem; margin-right: 0.35rem; vertical-align: middle; }
/* The goal's own warning, above the per-setting ones: it covers what the whole area can do,
   including the switches the goal deliberately leaves off for the owner to decide about. */
.mp-danger { border-left-width: 5px; }
.mp-modal-closes { margin: 0; color: var(--faint); font-size: 0.76rem; line-height: 1.5; }
.mp-missing { margin: 0; }
.mp-mod { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.6rem 0.75rem; }
.mp-mod-h { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; font-weight: 700; font-size: 0.86rem; }
.mp-tog {
  padding: 0.02rem 0.4rem; border-radius: var(--pill);
  font-size: 0.63rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
}
.mp-tog.on { border: 1px solid rgba(84, 201, 138, 0.45); color: var(--online); }
.mp-tog.off { border: 1px solid var(--line); color: var(--muted); }
.mp-chg { list-style: none; margin: 0.45rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.mp-chg li { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.4rem; font-size: 0.8rem; }
.mp-chg-l { flex: 1 1 12rem; min-width: 0; color: var(--muted); }
.mp-chg-v { font-variant-numeric: tabular-nums; }
.mp-chg-v s { color: var(--faint); }
.mp-chg-v b { color: var(--amber-hi); }

/* ── the card's own identity ───────────────────────────────────────────────────────────
   `virtual` means nothing to an owner, and neither does "Virtualized items and world
   containers". The name leads, the purpose follows it as a full sentence, and the id
   survives as a small tag because that is still what a log line and a bug report call it. */
.mod-open { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem; }
/* ⚠ THE SWITCH HAS TO LINE UP WITH THE NAME IT SWITCHES, and it did not — it lined up with the
   whole BLOCK. `.mod-head` centres its children, and `.mod-open` beside the switch is a column of
   one, two or three lines (the title row, an "about" sentence, a row of limit tags). So the switch
   sat at the middle of however tall that column happened to be: level with the name on a card with
   no description, 14px below it on one with a sentence, 28px below it on one carrying a warning
   chip. Across the forty-five rows of the setup screen the switch column visibly wandered, and it
   most often came to rest beside the DESCRIPTION rather than the thing it turns on.

   Top-aligned, then nudged down by the difference between the switch's own height and the title
   line's — so it centres on the NAME and stops moving when the text below it changes. */
.mod-head { align-items: flex-start; }
.mod-head > .pl-switch, .mod-head > .mod-core-ico { flex: none; margin-top: 0.12rem; }
.mod-head > .mod-head-actions { margin-top: -0.1rem; }
.mod-titlerow { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; width: 100%; min-width: 0; }
.mod-about { display: block; color: var(--muted); font-size: 0.79rem; line-height: 1.5; font-weight: 400; }
.mod-id { color: var(--faint); font-family: var(--mono); font-size: 0.7rem; }
/* Which SHAPES of limit are inside, on the folded card. The count said "four things are in
   here"; these say which four, and that is what decides whether the card gets opened. */
.mod-shapes { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.mod-write-n {
  display: inline-flex; align-items: center; gap: 0.25rem;
  font-size: 0.72rem; font-variant-numeric: tabular-nums; color: var(--amber-hi);
}
.mod-write-n .ico { width: 12px; height: 12px; flex: none; }

/* ── the decision, beside the description ──────────────────────────────────────────────
   What the mod ships with, whether this server differs from it, and whether the setting can
   change the game at all. All three are read out of the schema, so a module the panel has
   never heard of gets them too. */
.mf-dec { display: flex; align-items: center; flex-wrap: wrap; gap: 0.35rem; margin: 0.1rem 0 0.45rem; font-size: 0.71rem; }
.mf-dec-def { color: var(--faint); }
.mf-dec-chg {
  padding: 0.02rem 0.35rem; border-radius: var(--pill);
  border: 1px solid var(--line-amber); color: var(--amber-hi);
  font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
}
/* Ghost: no fill, the colour change is the whole affordance. The pair travels on the element,
   so no :hover rule anywhere states half of it. */
.mf-dec-rst {
  border: 0; background: transparent; padding: 0; margin: 0;
  font: inherit; font-size: 0.71rem; letter-spacing: 0; text-decoration: underline;
  color: var(--muted); cursor: pointer;
  --hov-bg: transparent; --hov-fg: var(--amber-hi); --hov-line: transparent;
}
.mf-dec-pow {
  display: inline-flex; align-items: center; gap: 0.22rem;
  padding: 0.02rem 0.35rem; border-radius: var(--pill);
  border: 1px solid var(--line-amber); color: var(--amber-hi);
  font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
}
.mf-dec-pow .ico { width: 10px; height: 10px; }

/* The way back to the goals, at the head of the full list. */
.mod-back { flex: none; }
/* ── FEED 03: the world ────────────────────────────────────────────────────────────────
   One card out of the two that used to argue — the running game through the SSA Bridge on
   top, the last save underneath, and nothing on it twice. The overlap is decided in
   `wsRender()`, not here; this file is about the SHAPES.

   The first version of the live half laid seventeen readouts into one uniform label/value
   grid — 643px on a desktop and 1057px on a phone, the tallest card on the dashboard by a
   distance, with fog carrying the same visual weight as world uptime. Three of these blocks
   replace that grid with the value's own model: a strength is a bar, a temperature is a
   point on its range, a clock is a position in a day. The label/value grid stays for the
   things that really are counts — the match figures and the save's totals.

   Every class name in this block is new or already scoped to it. That is deliberate rather
   than tidy: a bare rule for a class that already has one is what wrecked the map's marker
   card THREE times, most recently `.mp-ico` at 58px losing to `.mp-ico` at 30px twelve
   hundred lines later, and `wiki/Web-Interface.md` records the rest. */
/* ── the five blocks, side by side ─────────────────────────────────────────────────────────────
   This was a single COLUMN of blocks, and it was the card the owner named: at 2560px it drew a
   2,433px body in which the widest thing was the 780px day band, ran 825px down, and left
   1,653px — sixty-eight per cent of its own width — carrying nothing at all.

   So the blocks are grid items now, and the width buys COLUMNS of them instead of a longer line
   under each heading. Sky, weather, temperature, the match and the save are five independent
   readings that happen to be about one world; nothing in the card reads left-to-right across a
   block boundary, which is exactly the condition for laying them out this way.

   `auto-fit` here, and `auto-fill` twelve lines down on `.lw-meters`, and the difference is not an
   inconsistency. How many blocks this card has is not a layout choice — it is what the bridge
   answered: five with every switch on, three with a group off, one on a stopped server. With
   `auto-fill` at 2560 the card had SIX 377px tracks and five blocks, and the sixth stayed empty:
   411px of exactly the nothing this section is about. `auto-fit` collapses the track nobody landed
   in, so five blocks share the row at 459px each and the card's own reading fills its own width.

   What `auto-fill` was protecting against is real and is handled by the cap instead. Collapse the
   empty tracks and a card with ONE block gives that block all 2,433px — a heading rule two metres
   long with three little readings under the left end of it, which is the same defect from the
   other side. `max-width: 40rem` is two tracks and a gap: it never binds while there are three or
   more blocks (the tracks are narrower than it), and it stops the card that has one from drawing a
   banner. `.lw-meters` gets no such cap because it is INSIDE a block that is already bounded, and
   its `.lw-m-head` really does pin a label to one edge and a figure to the other.

   `min(100%, 19rem)` and never a bare length. The root font is 124%, so 19rem is 377px on a
   desktop and the panel's phone root makes it 322px — over a 320px screen, which is precisely
   the collapse `min(100%, …)` exists for and `check-mobile` exists to demand.

   No `grid-column: span 2` anywhere, on anything. That is the defect recorded at `.lw-meter-wide`
   below: a span of two CREATES an implicit second column wherever the grid resolved to one, which
   is every phone, and the card scrolled sideways by 37px with a meter rendered 0px wide. The two
   full-row items here use `1 / -1`, which means "every column there is" and is one column on a
   phone.

   `align-items: start` because the blocks are honestly different heights — the weather block
   carries six bars and a caveat, the save carries three numbers — and stretching them to match
   would put back, one axis over, the empty space this whole block is about. */
.lw-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
  gap: 0.9rem 1.7rem;
  align-items: start;
}
.lw-block { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; max-width: 40rem; }
/* A SENTENCE, not a reading: the bridge is not answering, a switch is off, the controller has not
   resolved yet. Squeezed into a 417px track it wraps to five lines beside a block of numbers and
   reads as one more column of card furniture; across the row it reads as what it is, and the
   button that acts on it sits directly under it. */
.lw-block.lw-b-msg { grid-column: 1 / -1; }

/* The group heading — the same eyebrow language as `.status-item .label`, one step up, so
   the blocks of readouts do not read as one undifferentiated wall of numbers. */
.lw-head {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--faint);
  text-transform: uppercase;
  letter-spacing: 0.18em;
  padding-bottom: 0.1rem;
  border-bottom: 1px solid var(--line-2);
}
/* The four sibling margin rules that used to live here — `.lw-head + .lw-sky`, `.lw-grid +
   .lw-head` and their pairs — are gone rather than kept as dead weight. A heading and its readouts
   are now the two children of one `.lw-block` and are spaced by that block's `gap`; a heading and
   the PREVIOUS block's readouts are no longer siblings at all, so those selectors could not match
   whatever they said. `.lw-body`'s own row gap is what separates one block from the next. */

/* More readouts than the eight on the Vitals card, so the figure steps down a size: at the
   1.4rem Vitals uses, "range -10 to 35" beside "18.2 °C" makes a card taller than the two
   above it put together. Scoped through `.lw-grid`, never as a bare `.value`. */
.lw-grid .status-item .value { font-size: 1.08rem; }
.lw-grid .status-item { gap: 0.18rem; }

/* The unit, or the range, or what the three cloud figures are — the half of a reading that
   makes the number mean something. Never the number itself. */
.lw-sub {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--faint);
  letter-spacing: 0.02em;
  overflow-wrap: anywhere;
}

/* ── The sky, as a position in a day ──────────────────────────────────────────────────
   The clock is the one figure on this card people read at a glance, so it is the one thing
   drawn at a headline size — and the daylight band under it answers "is it dark out there"
   without anybody having to compare three numbers in their head. */
/* Capped, and left-aligned inside the cap. A 1297px card gave the day bar a 1297px track and put
   "clock speed" and "night darkness" against the far right edge with 500px of nothing between them
   and the clock they belong to — the same "value adrift from its label" the status chip suffered in
   the eyebrow, from the same cause: a `space-between` across a box far wider than its content.
   A `max-width` rather than a fixed width, so it collapses cleanly and `check-mobile` has nothing
   to object to. */
.lw-sky { display: flex; flex-direction: column; gap: 0.35rem; max-width: 780px; }
.lw-sky-top { display: flex; align-items: flex-end; gap: 0.6rem 2.2rem; flex-wrap: wrap; }
.lw-clock-wrap { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; flex-wrap: wrap; }
.lw-clock { font-family: var(--mono); font-size: 1.85rem; line-height: 1; color: var(--text); letter-spacing: 0.01em; }

/* Not a warning and not a status — a fact about the world — so both states are quiet, and
   they differ in HUE rather than in loudness. The pair is declared together in each rule,
   which is the house rule `check-contrast` enforces on hovers and is worth following here
   too: a background changed on its own is how text disappears. */
.lw-daynight {
  display: inline-flex; align-items: center;
  padding: 0.08rem 0.45rem; border-radius: var(--pill);
  font-family: var(--mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.12em;
  border: 1px solid transparent;
}
.lw-daynight.is-day { background: rgba(230, 178, 58, 0.14); border-color: rgba(230, 178, 58, 0.4); color: var(--amber-hi); }
.lw-daynight.is-night { background: rgba(120, 145, 190, 0.14); border-color: rgba(120, 145, 190, 0.38); color: #9fb2d6; }

.lw-minis { display: flex; gap: 0.35rem 1.1rem; flex-wrap: wrap; min-width: 0; }
.lw-mini { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.lw-mini-v { font-family: var(--mono); font-size: 0.95rem; color: var(--text); }

/* Midnight to midnight. The track is the night ground, the lit band is the daylight the game
   states, and the needle is now. Drawn only where sunrise and sunset bracket an ordinary day
   — `wsRender()` skips the whole thing otherwise, because a band that wraps midnight cannot
   be drawn on a straight bar without lying about one end of it. */
.lw-day-track {
  position: relative; display: block; height: 8px; border-radius: var(--pill);
  background: linear-gradient(180deg, rgba(24, 32, 52, 0.9), rgba(16, 22, 38, 0.9));
  border: 1px solid var(--line-2); overflow: hidden;
}
.lw-day-lit {
  position: absolute; top: 0; bottom: 0;
  background: linear-gradient(90deg, rgba(230, 178, 58, 0.32), rgba(240, 199, 92, 0.55), rgba(230, 178, 58, 0.32));
}
.lw-day-now {
  position: absolute; top: -2px; bottom: -2px; width: 2px; margin-left: -1px;
  background: var(--text); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
}

/* ── Meters: a strength, and a temperature on its range ───────────────────────────────
   `min(100%, …)` and never a bare length. A bare minmax minimum WIDENS THE PAGE instead of
   collapsing to one column, which is the one-line defect `check-mobile` exists to stop — and
   the panel's root font is 124%, so a value written in rem is a third larger than it reads. */
/* `auto-FILL`, not `auto-fit`, and it is the difference between a readout and a rumour.
   `auto-fit` COLLAPSES the tracks nothing landed in, so three meters on a 1297px card became three
   432px cells with the label at the far left and the figure at the far right — "RAIN" and "35%" a
   third of a metre apart on somebody's monitor, with a bar stretched between them. `auto-fill`
   keeps the empty tracks, so a meter is the same ~180px whether there are three of them or seven,
   and it still collapses to one column on a phone because `min(100%, …)` wins there.

   180px and not the 210px it was written at, because the meters no longer live on the card — they
   live in a ~19rem BLOCK of it (see `.lw-body`). Two 210px tracks plus their gap need 442px and a
   block is 377–460px, so every weather block on every width had fallen to a single column of bars,
   which is the tall shape the drawn meters were introduced to replace. Two 180px tracks need 382px
   and fit. The label is the constraint and it is measured: "LIGHTNING" at 0.68rem letter-spaced
   mono is ~100px, "100%" is ~48px, so 180px is the figure with room to spare rather than a guess. */
.lw-meters { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 170px), 1fr)); gap: 0.5rem 1.1rem; }
.lw-meter { display: flex; flex-direction: column; gap: 0.22rem; min-width: 0; }
/* `1 / -1` and never `span 2`. `span 2` was tried, to sit the cloud block beside the other three on
   a desktop, and it CREATES AN IMPLICIT SECOND COLUMN wherever the grid has resolved to one — which
   is any card narrower than two 210px tracks, phones included. The card then scrolled sideways by
   37px, one meter rendered 0px wide and the word "Fog" was clipped. `1 / -1` means "every column
   there is", which is the whole row on a desktop and the only column on a phone. The spread is
   handled below, where it belongs, by capping the decks rather than the cell. */
.lw-meter-wide { grid-column: 1 / -1; }
.lw-m-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; min-width: 0; }
.lw-m-label {
  font-family: var(--mono); font-size: 0.68rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.16em; min-width: 0; overflow-wrap: anywhere;
}
.lw-m-val { font-family: var(--mono); font-size: 0.95rem; color: var(--text); flex: none; }

.lw-m-track {
  position: relative; display: block; height: 6px; border-radius: var(--pill);
  background: var(--inset); border: 1px solid var(--line-2); overflow: hidden;
}
.lw-m-fill {
  display: block; height: 100%;
  background: linear-gradient(90deg, rgba(230, 178, 58, 0.55), var(--amber-hi));
}

/* A reading the game put outside 0–1 (or a temperature outside its own stated range). The
   figure keeps its real value — see `lwPct` — and this is what stops the clamped bar quietly
   agreeing with a number the game never reported. */
.lw-out { color: var(--alarm); }
.lw-m-fill.lw-out { background: linear-gradient(90deg, rgba(255, 92, 92, 0.55), var(--alarm)); }

/* The three cloud decks, side by side and never averaged: only the nimbostratus deck brings
   rain, so a mean would erase the one that matters. */
/* Capped for the same reason the sky block is: the cell spans the whole row, and three decks given
   1297px become three 430px bars with their labels adrift from their figures. 660px keeps each deck
   at about the width of an ordinary meter, which is what they are. */
.lw-decks { display: flex; gap: 0.7rem; min-width: 0; max-width: 660px; }
.lw-deck { display: flex; flex-direction: column; gap: 0.15rem; flex: 1 1 0; min-width: 0; }
.lw-d-k { font-family: var(--mono); font-size: 0.6rem; color: var(--faint); letter-spacing: 0.1em; text-transform: uppercase; }
.lw-d-v { font-family: var(--mono); font-size: 0.72rem; color: var(--muted); }

/* A temperature's range, with the reading on it. Taller than a strength bar and open at both
   ends, because it is a scale rather than a fill: what is being shown is WHERE in the range
   this sits, not how much of it is used up. */
.lw-t-track {
  position: relative; display: block; height: 6px; border-radius: var(--pill);
  background: linear-gradient(90deg, rgba(90, 150, 220, 0.35), rgba(120, 130, 140, 0.3), rgba(230, 178, 58, 0.45));
  border: 1px solid var(--line-2);
}
.lw-t-dot {
  position: absolute; top: 50%; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
  border-radius: 50%; background: var(--text); border: 1px solid var(--bg);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);
}
.lw-t-dot.lw-out { background: var(--alarm); }

/* ── What it says when it has nothing to say ──────────────────────────────────────────
   A switch nobody has turned on, the bridge not answering, or a controller that has not
   resolved yet. All three are ORDINARY states rather than errors, so none of them is painted
   as a warning — and none of them draws a grid of dashes, because a world with no weather and
   a feature nobody switched on must not look the same. */
/* `max-width` because a sentence reads at the width of a line of prose, not at the width of
   whatever grid track it landed in. Its BLOCK keeps its column — the heading has to stay with the
   group it names — and only the sentence inside it is capped. */
.lw-off {
  margin: 0;
  max-width: 62ch;
  font-size: 0.84rem;
  line-height: 1.5;
  color: var(--muted);
}

/* The scale caveat, INSIDE the weather block rather than at the foot of the card: the card
   now runs on past the temperatures and the match into the save, and a footnote three blocks
   below the percentages it qualifies explains nothing to anybody. */
.lw-note {
  margin: 0.4rem 0 0;
  padding-top: 0.4rem;
  border-top: 1px solid var(--line-2);
  font-size: 0.74rem;
  line-height: 1.5;
  color: var(--faint);
}

/* The route to the switch, under the sentence that named it and never off in a column of its own —
   `1 / -1` is "every column there is", which is the whole row on a monitor and the only column on
   a phone. `span 2` is what breaks that; see `.lw-meter-wide`. */
.lw-foot { grid-column: 1 / -1; margin-top: 0.35rem; }
/* Sized down from the page's default button, which is a Start/Stop control and would shout
   next to a paragraph. No :hover rule of its own — `button.secondary` already declares its
   background and colour as a pair, and overriding one half of that is the defect
   `check-contrast` exists to catch. */
.lw-go { padding: 0.42rem 0.95rem; font-size: 0.8rem; }

/* Below 480px the label/value cells go two across instead of one, which reads the way a
   weather app does; the meters and the sky go to a single column of their own accord, because
   their minimum is `min(100%, 170px)` and 100% wins. Scoped to `.lw-grid` and `.lw-meters`, so
   `.status-grid` everywhere else in the panel is untouched. */
@media (max-width: 480px) {
  .lw-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 104px), 1fr)); gap: 0.85rem 0.7rem; }
  .lw-clock { font-size: 1.6rem; }
  .lw-sky-top { align-items: flex-start; }
}

/* ══ The record card ═══════════════════════════════════════════════════════════════════════════
   The panel's route out of every surface that names a game thing — the spawn picker, a chest's
   contents, a vehicle's fitted parts, an economy override row, the log viewer's item popover.

   Its BODY is markup `itemRecord.js` builds, the same builder the field console's item page uses,
   so the class names below (`.ds`, `.rel-chip`, `.ing-row`, `.ith`…) are that renderer's. They are
   deliberately not shared as CSS: this panel's root font is 124% against the site's 100%, the two
   modals are different shells, and a visual difference between the two surfaces is a matter of
   taste where a factual one would be a bug. Every rule here is scoped under `.rec-card`, so none of
   those names enters the panel's document globally and none can collide with a screen that already
   owns one — the "declared twice, 2,900 lines apart" defect that shrank the map card's portrait to
   20px of artwork cannot happen to a name that is never declared bare.

   Every button below states its hover as a PAIR through --hov-bg / --hov-fg / --hov-line, because
   `button:hover` paints every button in the panel amber with dark text and overriding one half of
   that is the defect `check-contrast` exists to catch. */
.pl-modal.rec-card { width: min(860px, 100%); }
.rec-card .pl-modal-head { align-items: flex-start; }
.rec-head { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.rec-eyebrow { font-family: var(--mono); font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.18em; color: var(--faint); }
.rec-name { font-size: 1.2rem; font-weight: 700; line-height: 1.15; color: var(--text); overflow-wrap: anywhere; }
.rec-back {
  align-self: flex-start; margin-bottom: 0.3rem; padding: 0.12rem 0.5rem; cursor: pointer;
  background: var(--inset); border: 1px solid var(--line-amber); border-radius: var(--radius);
  color: var(--amber); font-family: var(--ui); font-size: 0.66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  --hov-bg: var(--panel-2); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}
.rec-scroll { padding: 0.9rem 1.1rem 1.1rem; overflow-y: auto; }

/* ── the strip an item database cannot fill in ────────────────────────────────────────────────
   The spawn code, how many exist in this save, and the owner's own trader price. Each is absent
   rather than zero when it could not be answered, so this row can be one chip or four. */
.rec-admin {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap;
  padding-bottom: 0.8rem; margin-bottom: 0.9rem; border-bottom: 1px dashed var(--line-amber);
}
.rec-facts { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; min-width: 0; }
.rec-acts { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin-left: auto; }
.rec-chip {
  display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; min-width: 0;
  padding: 0.25rem 0.55rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--inset); color: var(--muted); font-size: 0.74rem; font-family: var(--ui);
}
.rec-chip .ico { color: var(--amber); font-size: 1rem; flex: none; }
.rec-chip b { color: var(--text); font-family: var(--mono); }
.rec-chip .mono { font-family: var(--mono); color: var(--amber-hi); overflow-wrap: anywhere; }
.rec-copy {
  cursor: pointer;
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: var(--line-amber);
}
.rec-ov { flex-wrap: wrap; border-color: var(--line-amber); }
.rec-ov-row { display: inline-flex; gap: 0.3rem; font-family: var(--mono); font-size: 0.7rem; color: var(--text); }
.rec-ov-row b { color: var(--amber-hi); }
.rec-acts .rec-use, .rec-acts .rec-give { padding: 0.35rem 0.75rem; font-size: 0.78rem; }
.rec-locked { margin: 0 0 0.9rem; }

/* ── the shared record body ───────────────────────────────────────────────────────────────── */
.rec-card .item-detail { display: flex; gap: 1.1rem; flex-wrap: wrap; }
.rec-card .item-detail-side { flex: none; }
.rec-card .item-detail-img {
  width: 132px; height: 132px; object-fit: contain; display: flex; align-items: center; justify-content: center;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 8px;
}
.rec-card .item-img-x { color: var(--faint); }
.rec-card .item-img-x .ico { width: 2rem; height: 2rem; }
/* `min-width: 0` and not a length: this is the flex child that holds every wide thing on the card
   (a chip row, a grid of stats, a monospace code) and without it the modal is as wide as its
   longest name — the shape the field console measured at 51px past a 310px phone. */
.rec-card .item-detail-body { flex: 1 1 16rem; min-width: 0; }
/* `white-space: pre-line` is gone, here and on `.rec-note` below, and that is the fix rather than
   a tidy-up: it was the only thing standing in for the game's own paragraph breaks and it cannot
   tell a paragraph from a wrapped line. `gameText.js` emits real `<p>` and `<br>` now. */
.rec-card .item-desc { color: var(--text); margin: 0 0 1rem; line-height: 1.5; font-size: 0.86rem; }
/* The GAME's own voice inside the card — a medicine's effect, the repair cap, a codex paragraph.
   The field console has had this since the renderer was shared and the panel never did, so the same
   markup drew as an unstyled paragraph on one of the two surfaces. `pre-line`, because the codex
   ships hard line breaks inside one block. */
.rec-card .rec-note {
  margin: 0.3rem 0 0.8rem; padding: 0.4rem 0 0.4rem 0.7rem;
  border-left: 2px solid var(--line-amber); color: var(--muted);
  font-size: 0.8rem; line-height: 1.5;
}
/* ── THE GAME'S OWN STYLING, in this panel's tokens ────────────────────────────────────────────
   The field console's sheet carries the same block, for the same reason and with the same
   mapping; see the note there. The two sheets are deliberately separate (only the LOGIC is
   shared) and both are scoped so `check-contrast` and `check-mobile` keep reading them. */
.rec-card .gt-p { margin: 0 0 0.7rem; }
.rec-card .gt-p:last-child { margin-bottom: 0; }
.rec-card .gt-blue { color: var(--amber); }
.rec-card .gt-gold, .rec-card .gt-important, .rec-card .gt-default { color: var(--standby); font-weight: 600; }
.rec-card .gt-gray { color: var(--muted); }
.rec-card .gt-key { color: var(--amber); font-weight: 600; letter-spacing: 0.02em; }
.rec-card .gt-sub { font-size: 0.85em; vertical-align: sub; }
/* A codex section title, and its plate. See the field console's copy for why a title is an `h4`
   rather than a second `h3`. */
.rec-card .rec-sub { margin: 0.9rem 0 0.15rem; font-size: 0.86rem; font-weight: 700; line-height: 1.3; color: var(--text); }
.rec-card .rec-sub:first-child { margin-top: 0.25rem; }
/* `width: auto` rather than `100%` — see the field console's copy: every codex plate in the game is
   16:9, so a full-width element under a height cap paints a bordered box round 450px of nothing. */
.rec-card .rec-fig {
  display: block; width: auto; max-width: 100%; max-height: 18rem;
  margin: 0.25rem 0 0.8rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--inset);
}
.rec-card h3 {
  margin: 1.1rem 0 0.4rem; font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--amber);
}
.rec-card .item-detail-body > h3:first-child { margin-top: 0; }
.rec-card .small { font-size: 0.7rem; }
/* `min(100%, …)` on the minimum, never a bare length: at 124% root font a bare 12rem is 238px and
   this grid sits inside a card that is `overflow: hidden`, so the right-hand column would be
   clipped off rather than scrolled to. With the guard it collapses to one column. */
.rec-card .dstats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); gap: 0.4rem; }
.rec-card .ds {
  display: flex; align-items: center; gap: 0.6rem; min-width: 0;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.45rem 0.6rem;
}
.rec-card .ds-txt { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.rec-card .ds-v { font-family: var(--mono); font-size: 0.95rem; color: var(--text); overflow-wrap: anywhere; }
.rec-card .ds-k { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.rec-card .price-buy .ds-v { color: var(--alarm); }
.rec-card .price-sell .ds-v { color: var(--online); }

.rec-card .rel-list { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; }
/* The chip's name CLIPS. A flex item's automatic floor is its content, and a real published name
   ("5.56x45mm Armor Piercing Ammunition Box") is 18px past a 310px phone on its own — measured, by
   deleting this and rendering it. `overflow: hidden` is the half that carries the load: it resolves
   `min-width: auto` to zero all by itself, which is why the `min-width: 0` beside it is belt and
   braces rather than the fix. Removing THAT alone changes nothing, and a break-proof aimed at it
   passed while the page was fine — see `check-panel-render`'s fourteenth defect. */
.rec-card .rel-chip {
  display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; cursor: pointer;
  font-family: inherit; font-size: 0.7rem; color: var(--text);
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 6px; padding: 0.2rem 0.45rem 0.2rem 0.25rem;
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); --hov-line: var(--line-amber);
}
.rec-card .rel-chip img, .rec-card .rel-chip .rel-chip-x { width: 24px; height: 24px; object-fit: contain; flex: none; }
.rec-card .rel-chip .rel-chip-x { display: inline-flex; align-items: center; justify-content: center; color: var(--muted); opacity: 0.6; }
.rec-card .rel-chip-n { white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.rec-card .att-sub { margin: 0.3rem 0 0.45rem; }
.rec-card .att-sub-h { display: block; font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 0.2rem; }

.rec-card .craft-list { margin-top: 0.35rem; }
.rec-card .ing-row { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; margin-bottom: 0.3rem; }
.rec-card .ing-amt { font-family: var(--mono); color: var(--amber); font-size: 0.76rem; flex: none; min-width: 2rem; }
.rec-card .ing-opts { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; min-width: 0; }
.rec-card .ing-or { color: var(--muted); font-size: 0.66rem; }
/*
 * A quest OBJECTIVE, which is a sentence rather than a chip.
 *
 * `.ing-opts` holds item chips everywhere else on this card, and a bare `<span>` dropped into a
 * flex row inherits whatever the row happens to give it. A quest's objective is the game's own
 * translated sentence ("Find a Deer Head at 80%+ durability"), so it is given the card's ordinary
 * text colour and allowed to WRAP — `min-width: 0` on the flex child, because a long caption in a
 * nowrap row is what pushes a card sideways on a phone.
 */
.rec-card .q-obj { color: var(--text); font-size: 0.78rem; min-width: 0; overflow-wrap: anywhere; }
.rec-card .craft-meta {
  margin: 0.15rem 0 0.6rem; display: flex; flex-wrap: wrap; gap: 0.1rem 0.5rem;
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
}
.rec-card .loot-list { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; }
.rec-card .loot-chip {
  display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.7rem; color: var(--text);
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 5px; padding: 0.15rem 0.45rem;
}
.rec-card .loot-w { font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }

.rec-card .item-history { list-style: none; margin: 0.3rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; max-height: 260px; overflow-y: auto; }
.rec-card .ith {
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; padding: 0.3rem 0.5rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); font-size: 0.8rem;
}
.rec-card .ith-act { font-family: var(--mono); font-size: 0.68rem; min-width: 4.5rem; }
.rec-card .ith-act em { color: var(--amber); font-style: normal; }
.rec-card .ith-sell .ith-act { color: var(--online); }
.rec-card .ith-buy .ith-act { color: var(--alarm); }
.rec-card .ith-price { font-family: var(--mono); color: var(--amber-hi); }
.rec-card .ith-sub { color: var(--muted); font-size: 0.72rem; margin-left: auto; }

/* ══ The way IN, from every surface that names a game thing ════════════════════════════════════
   One glyph, one shape, five screens. It is a quiet button everywhere: the row it sits on already
   has a job — pick this item, open this box, edit this price — and the record is the second
   question, so it must be reachable without ever being in the way of the first. */
.cmd-ip-i, .gl-preview-more, .mp-c-info, .eco-item-info, .eco-arow-info {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; padding: 0; cursor: pointer;
  background: transparent; border: 1px solid transparent; border-radius: var(--radius);
  color: var(--muted);
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); --hov-line: var(--line-amber);
}
.cmd-ip-i .ico, .gl-preview-more .ico, .mp-c-info .ico, .eco-item-info .ico, .eco-arow-info .ico { width: 0.95rem; height: 0.95rem; }
/* On the picker card the glyph sits in the corner, because the card's whole face is the pick
   target and a button in the flow would take a row of its own on a 140px tile. The `position:
   relative` it hangs off is declared with the card itself, at the card's own rule — not repeated
   here, because a class declared twice far apart in this file is the defect `wiki/Web-Interface.md`
   keeps a list of names for. */
.cmd-ip-i { position: absolute; top: 0.15rem; right: 0.15rem; background: var(--inset); }
.gl-preview-more { align-self: center; }
/* `.mp-c-slot` used to sit here — the part's own socket, drawn as a second line under its name.
   It is gone from `app.js` and the rule goes with it rather than being left for the next reader to
   wonder about: a class nothing emits is indistinguishable, from this file, from one whose call
   site was deleted by accident. The reasoning is beside the row builder in `mapLoadParts`. */
/* In the PARTS list the name is the content, so it wraps rather than eliding.
   `.mp-c-name` elides everywhere else and that is right there: a contents row ends in `×3` and a
   record button, and the count is what the eye is running down. A parts row ends in nothing — the
   name IS the row — and eliding it turned four wheels into four rows reading "Laika Front…".
   Measured on a 310px phone with the `max` chip gone, the column still only reached 79px and ten of
   eleven names were cut; wrapping is what actually answers "which wheel is that".

   `overflow-wrap: anywhere` is the other half and is not decoration: wrapping only helps down to
   the longest WORD, and on a 310px phone the column is 79px while "Compartment" alone is 87px — so
   without it exactly one row in eleven was still cut, by eight pixels, which is precisely the sort
   of residue that gets called "close enough" and then reported by an owner. */
.mp-parts .mp-c-name {
  white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere;
}

/* ── the four EVENT layers ────────────────────────────────────────────────────────────────────
   Everything else on this map is a thing that is standing somewhere. These four are things that
   are HAPPENING, and the colour has to carry that: a crate and a raid are the reason somebody
   opened the map, so they read hot and sit on top of the draw order; a bunker and an arena are
   places with a state, so they read cool and sit with the points of interest.

   Declared as tokens and read back through --badge-bg, exactly as every other kind is, so the
   layer picker, the marker and the card cannot drift apart — the failure this block's neighbour
   above records having had three copies of. */
:root {
  --c-cargo:  #ffd23f;   /* a flare in the sky — the brightest thing on the map, and rare */
  --c-bunker: #b9c2cf;   /* concrete: a place, and one that is there whether or not anything is */
  --c-gevent: #d34fd9;   /* a scheduled competition, which nothing else on the map is */
  --c-raid:   #ff3b30;   /* somebody's base is being taken apart right now */
}
.mk-cargo  { --badge-bg: var(--c-cargo); }
.mk-bunker { --badge-bg: var(--c-bunker); }
.mk-gevent { --badge-bg: var(--c-gevent); }
.mk-raid   { --badge-bg: var(--c-raid); }
.mk-cargo  .mk-dot, .ml-dot.mk-cargo  { background: var(--c-cargo); }
.mk-bunker .mk-dot, .ml-dot.mk-bunker { background: var(--c-bunker); }
.mk-gevent .mk-dot, .ml-dot.mk-gevent { background: var(--c-gevent); }
.mk-raid   .mk-dot, .ml-dot.mk-raid   { background: var(--c-raid); }

/* ── the three layers the SAVE alone answers ──────────────────────────────────────────────────
   A garden bed, a fuel or water station and a raid-protection flag. They are not events and they
   are not the running world: every one of them is standing there whether the server is up or not,
   which is the whole reason they exist as layers, so none of them reads "hot".

   The hues are picked to be separable from what is already on this map rather than to be pretty.
   Leaf green is the one colour a garden can be and it is kept clearly lighter and more yellow than
   the player green, which is the only thing near it. Indigo is a free corner of the wheel and a
   station is infrastructure. Pink is the deliberate mirror of the raid red: the same subject — a
   base under a raiding rule — in the opposite state, and no other layer is anywhere near it.

   Declared as tokens and read back through --badge-bg exactly as every other kind is, so the layer
   picker, the marker, the list dot and the card cannot drift apart. */
:root {
  --c-garden:   #8fd14f;   /* something growing, and the only green that is not a player */
  --c-resource: #6f5bd9;   /* the island's own plumbing — a pump, a well, a dispenser */
  --c-protect:  #ff7de0;   /* the mirror of --c-raid: protected rather than being taken apart */
}
.mk-garden   { --badge-bg: var(--c-garden); }
.mk-resource { --badge-bg: var(--c-resource); }
.mk-garden   .mk-dot, .ml-dot.mk-garden   { background: var(--c-garden); }
.mk-resource .mk-dot, .ml-dot.mk-resource { background: var(--c-resource); }

/* The slot list on a garden card. It borrows `.mp-c-row` wholesale — a crop, an item in a box and
   a vehicle part are one shape of row — so all this adds is the two things that are only true here:
   a dead plant reads as spent rather than as a normal row, and the growth/water/weeds line is
   secondary to the crop's name and must give the width up first. */
.mp-slots { max-height: 15rem; overflow-y: auto; }
/* `min-width: 0` is what lets the name ellipsis work at all inside a flex row, and it is the line
   that is always forgotten. Without it the column takes its content's width and pushes the card
   sideways instead of truncating. */
.mp-slot-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.05rem; }
.mp-slot .mp-c-name { flex: none; }
.mp-slot-meta {
  color: var(--standby); font-size: 0.72rem; line-height: 1.35;
  /* WRAPS. A phone card is 320px wide and this line is four facts long; `nowrap` here would push
     the whole popover past the viewport, which is what `check-panel-render` measures at that width. */
  white-space: normal; overflow-wrap: anywhere;
}
.mp-slot-dead { opacity: 0.55; }
.mp-slot-dead .mp-c-name { text-decoration: line-through; }

/* A cargo drop at its three moments. Same reasoning as the live kinds: one basket for "will be
   dropped", "is in the air" and "is on the ground counting down" answers none of the three
   questions somebody turned the layer on to ask. */
.ck-scheduled { --badge-bg: var(--c-cargo-sched); }
.ck-falling   { --badge-bg: var(--c-cargo); }
.ck-landed    { --badge-bg: var(--c-cargo-down); }
:root { --c-cargo-sched: #8a7c3e; --c-cargo-down: #ff8c1a; }
.ck-scheduled .mk-dot, .ml-dot.ck-scheduled { background: var(--c-cargo-sched); }
.ck-falling   .mk-dot, .ml-dot.ck-falling   { background: var(--c-cargo); }
.ck-landed    .mk-dot, .ml-dot.ck-landed    { background: var(--c-cargo-down); }
/* Not there YET, drawn the way the map already draws a thing that is not there yet. */
.ck-scheduled .mk-dot { border-style: dashed; }

/* A raid is the one marker on this map that wants to be noticed rather than found. The ring is on
   a pseudo-element so it costs the marker nothing to lay out, and it is dropped entirely under
   `prefers-reduced-motion` — a pulsing dot is exactly what that setting is for. */
.mk-raid .mk-dot::after {
  content: ''; position: absolute; inset: -3px; border-radius: 50%;
  border: 2px solid var(--c-raid); animation: raidPulse 1.8s ease-out infinite;
}
@keyframes raidPulse {
  0%   { transform: scale(0.9); opacity: 0.9; }
  100% { transform: scale(2.1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .mk-raid .mk-dot::after { animation: none; opacity: 0.5; }
}

/* ── a layer that is empty because a switch is off ────────────────────────────────────────────
   The same shape, and the same restraint, as the live-world card's `.lw-off`: a sentence in the
   muted colour, never a warning and never a grid of dashes, because "you have not turned this on"
   is an ORDINARY state on a working server — every bridge switch ships off — and painting it as a
   fault teaches people that the map is broken.

   No :hover rule on the button. `button.secondary` already states its background and its colour as
   a pair and overriding one half of that is the defect `check-contrast` exists to catch. */
.ml-off {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem;
  padding: 0.6rem;
  font-size: 0.8rem; line-height: 1.5; color: var(--muted);
}
.ml-off-go {
  padding: 0.3rem 0.7rem; font-size: 0.74rem;
  background: var(--inset); border: 1px solid var(--line-amber); border-radius: var(--radius);
  color: var(--amber); font-family: var(--ui); font-weight: 700; cursor: pointer;
  --hov-bg: var(--panel-2); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}

/* ── the item picker's footer: a session, rather than a single shot ───────────────────────────
   Only drawn when the picker was opened with `multi`, which is the spawn menus and nothing else —
   every other caller gets the modal exactly as it was.

   The quantity lives HERE, in the picker, rather than in the second modal every spawn caller used
   to open after a pick. That second modal is another `.pl-modal-ov` at the same z-index: with the
   picker closing first it landed on a clear screen, and with the picker staying up it would have
   painted on top of it, two backdrops and two blurs deep, once per item.

   `flex-wrap` is the whole mobile story. At 320px the three chips, the number box, the tally and
   Done cannot share a line, and the alternative to wrapping is a footer that pushes the modal
   wider than the phone. Nothing here declares a width. */
/* `:not([hidden])` on both, and this file has already paid for that lesson once — see the note
   above `.vt-track`. The UA stylesheet's `[hidden] { display: none }` is specificity (0,1,0) from
   the user-agent origin, so ANY author rule saying `display: flex` beats it. `hidden` on the footer
   is what a single-shot picker uses to not have one, and on the quantity group what a `multi`
   picker without `count` uses — both would have been on screen regardless. */
.cmd-ip-foot:not([hidden]) {
  flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  padding: 0.6rem 1.1rem 0.9rem; border-top: 1px solid var(--line);
}
.cmd-ip-qty:not([hidden]) { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
.cmd-ip-qlbl { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
/* Ghost shape: no fill at rest, and the hover PAIR retuned on the element so the two halves can
   never be changed one at a time — see the note at the top of this file. */
.cmd-ip-chip {
  min-width: 2rem; padding: 0.25rem 0.5rem; font-size: 0.76rem; font-weight: 700;
  background: transparent; border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--muted); font-family: var(--ui); cursor: pointer;
  --hov-bg: var(--hover-bg); --hov-fg: var(--amber-hi); --hov-line: var(--line-amber);
}
/* The chosen quantity, stated as a fill rather than only as a colour: at 0.76rem a colour change
   alone is not a strong enough signal for the one control that decides how much gets spawned. */
.cmd-ip-chip.on {
  background: var(--inset); border-color: var(--line-amber); color: var(--amber-hi);
  --hov-bg: var(--inset); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}
.cmd-ip-qn {
  width: 4.5rem; max-width: 100%; padding: 0.25rem 0.4rem; font-size: 0.78rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--mono);
}
.cmd-ip-qn:focus { border-color: var(--amber); outline: none; }
/* Takes the slack so Done stays on the right, and stops being a width of its own at 320px —
   `min-width: 0` is what lets a long item name shrink instead of widening the footer. */
.cmd-ip-tally { flex: 1 1 8rem; min-width: 0; font-size: 0.74rem; color: var(--muted); overflow-wrap: anywhere; }
.cmd-ip-done { padding: 0.35rem 0.9rem; font-size: 0.78rem; }
/* A pick is a thing that HAPPENED, and with the picker staying open nothing else says so — the
   modal does not close, the grid does not move. Dropped under reduced motion, where the tally
   alone carries it. */
.cmd-ip-card.took { animation: ipTook 0.45s ease-out; }
@keyframes ipTook {
  0%   { border-color: var(--amber); background: var(--inset); }
  100% { border-color: var(--line); background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .cmd-ip-card.took { animation: none; }
}

/* ── move mode: drag a marker, drop it, the game teleports the thing ──────────────────────────
   The control lives on the MAP'S OWN TOOLBAR, beside Admin actions, and it used to be a fourth
   checkbox in `.map-opts-top` between Names, Sectors and In view. That is worth writing down
   because it is the likeliest single reason the whole feature was reported as not working: those
   three are preferences about what is DRAWN, this one changes somebody's server, and the drawer
   they sit in is `#map-side`, which is collapsed by default below 760px and remembered per browser
   — so on a phone, and on any desktop where the filters had ever been slid away, the only way to
   arm the map's headline gesture was not on screen at all.

   It is an ordinary `.secondary` button, so it inherits the row's sizing and wrapping and adds no
   width of its own. Only the ARMED state is new, and it states its background and its text in one
   go rather than tinting one of them — the rule this stylesheet ends with, and the reason
   `check-contrast` exists. `--alarm` is the panel's own word for "this changes the world". */
#map-drag[aria-pressed="true"] {
  border-color: var(--alarm); background: rgba(255, 92, 92, 0.16); color: #f4bdbb;
  --hov-bg: rgba(255, 92, 92, 0.26); --hov-fg: #ffd9d7; --hov-line: var(--alarm);
}

/* Armed, said on the MAP as well as on the toggle. The toggle is in a side panel that is collapsed
   on a phone and off to the left on a desktop, and the thing that is now dangerous is the map. */
/* TOP-LEFT is the only free corner of this viewport, and that is why: top-centre is `.map-empty`,
   bottom-centre is `.map-trunc`, top-right is `.map-updated-badge` and bottom-right is `.map-zoom`.
   The right margin clears the badge rather than being a round number. */
.map-armed-note {
  position: absolute; z-index: 16; top: 0.5rem; left: 0.5rem;
  max-width: calc(100% - 9rem); padding: 0.22rem 0.6rem;
  background: rgba(255, 92, 92, 0.16); border: 1px solid var(--alarm); border-radius: var(--pill);
  color: #f4bdbb; font-family: var(--ui); font-size: 0.72rem; line-height: 1.4;
  text-align: center; overflow-wrap: anywhere; pointer-events: none;
}
/* Only what can actually be dragged offers to be. A marker whose kind cannot move keeps the
   pointer it has, so the cursor is a truthful answer to "will this one work" before the click. */
.map-armed .map-mk[data-kind="players"], .map-armed .map-mk[data-kind="vehicles"] { cursor: grab; }
.map-armed.map-dragmk, .map-armed.map-dragmk .map-mk { cursor: grabbing; }
/* The ghost, not the marker. The real marker is never moved — a live refresh lands every five
   seconds and would snap an inline-positioned one back mid-gesture.

   IT IS A DOT, and that is the whole shape of it: the element itself has no size, the crosshair
   sits exactly on the pointer, and the words hang off to the lower right. It used to be a labelled
   box centred on the pointer, which put the description on top of the thing being aimed at — at
   fit-to-screen zoom that box covers about a kilometre of island. */
.map-drag-ghost {
  position: absolute; z-index: 29; left: -9999px; top: -9999px;
  width: 0; height: 0; opacity: 0; pointer-events: none;
  font-family: var(--ui);
}
.map-drag-ghost.on { opacity: 1; }
/* The drop point. A filled dot with a ring of the panel colour around it, so it reads on the
   dark forest and on the pale sand without changing colour. */
.map-drag-ghost .mdg-dot {
  position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--alarm); box-shadow: 0 0 0 2px var(--panel), 0 0 0 3px var(--alarm);
}
/* …and the hairlines through it, which are what make the exact pixel readable rather than the
   general area. They stop short of the dot so the centre stays the brightest thing on screen. */
.map-drag-ghost .mdg-dot::before,
.map-drag-ghost .mdg-dot::after {
  content: ''; position: absolute; background: var(--alarm); opacity: 0.75;
}
.map-drag-ghost .mdg-dot::before { left: 50%; top: -14px; width: 1px; height: 34px; margin-left: -0.5px; }
.map-drag-ghost .mdg-dot::after { top: 50%; left: -14px; height: 1px; width: 34px; margin-top: -0.5px; }
/* WHERE IT WILL LAND, read while the hand still holds it. The sector and the coordinates are the
   two things the confirmation is about to ask about, and until this was here the drop itself was
   the first time anybody found out what they had pointed at — which on a fit-to-screen island is a
   kilometre either way. Offset from the dot rather than centred on it: it covers map the pointer
   is not aiming at, which is the whole point of moving it. */
.map-drag-ghost .mdg-tag {
  position: absolute; left: 0.8rem; top: 0.45rem;
  display: flex; flex-direction: column; gap: 0.05rem;
  max-width: min(12rem, 40vw); padding: 0.15rem 0.45rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-size: 0.7rem; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.map-drag-ghost .mdg-where { color: var(--muted); font-size: 0.66rem; letter-spacing: 0.02em; }

/* ── the catalogue's argument form ────────────────────────────────────────────────────────────
   One modal built from an action's own `params`, so there is no per-action form anywhere and a
   sixth field type added to the table lands here as a text box rather than as a missing control.

   Nothing declares a width but the modal itself, and that is `min(440px, 100%)` — the rows are a
   two-column grid that collapses to one below 420px, so the longest label a locale can produce
   wraps instead of widening the dialog past the phone it is on. */
.bpf-modal { width: min(440px, 100%); display: flex; flex-direction: column; max-height: 85vh; }
.bpf-body { padding: 0.9rem 1.1rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.7rem; }
.bpf-row { display: grid; grid-template-columns: minmax(min(100%, 9rem), 1fr) minmax(min(100%, 9rem), 1fr);
  gap: 0.5rem; align-items: center; font-size: 0.82rem; color: var(--text); }
@media (max-width: 420px) { .bpf-row { grid-template-columns: 1fr; } }
.bpf-row > span { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; overflow-wrap: anywhere; }
/* A CHOICE ROW IS ONE COLUMN AT EVERY WIDTH: the label above, the control under it across the whole
   dialog. Its options are the longest strings any of these forms carries — `Sharks and other large
   aquatic animals` is 38 characters, and German is longer — and half of a 440px dialog cannot hold
   them at any font this panel uses. In the two-column layout the demolish form read `Wooden
   palisa…`, on the one dialog where the truncated words are what is about to be destroyed. The open
   list shows the whole option whatever happens, so the closed state is the only thing to fix, and
   it is a width. `enhanceSelect()` is what a reader actually sees here — `.cdd-btn` with a
   `.cdd-lbl` inside — and that label is what `check-panel-render` measures. */
.bpf-row.bpf-rowwide { grid-template-columns: 1fr; gap: 0.25rem; align-items: stretch; }
.bpf-hint { font-size: 0.7rem; color: var(--muted); font-family: var(--mono); }
/* A parameter's own sentence, under the control it is about.
   Full width rather than in the value column: it is prose, and in a 9rem column it wrapped into a
   ragged block that read as a broken row. `margin-top: -0.2rem` closes the row gap so the note
   belongs to the field above it instead of floating between two of them. */
.bpf-note { margin: -0.2rem 0 0.25rem; font-size: 0.74rem; line-height: 1.45; color: var(--muted);
  overflow-wrap: anywhere; }
.bpf-row input[type="text"], .bpf-row input[type="number"], .bpf-row select {
  min-width: 0; width: 100%; padding: 0.32rem 0.5rem; font-size: 0.82rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui);
}
.bpf-row input:focus, .bpf-row select:focus { border-color: var(--amber); outline: none; }
/* ── the field that opens a picker ────────────────────────────────────────────────────────────
   The box and its Choose button share the value column, so the label column keeps the same width
   as every other row and the modal does not grow a third column for two of its fields. `1fr auto`
   with `min-width: 0` on the input is what lets the box shrink instead of pushing the button off
   the edge — a flex child without it is sized by its content and 320px is where that shows.
   Under the 420px breakpoint above the whole row is already one column, so the pair simply
   becomes the second line. */
.bpf-pickwrap { display: flex; gap: 0.4rem; align-items: center; min-width: 0; }
.bpf-pickwrap input { min-width: 0; flex: 1 1 auto; }
.bpf-pick { flex: 0 0 auto; padding: 0.3rem 0.55rem; font-size: 0.76rem; display: inline-flex;
  align-items: center; gap: 0.3rem; white-space: nowrap;
  /* Ghost: no fill of its own, so the hover pair travels together rather than being retuned in a
     `:hover` rule that changes one half. See the two shapes at the foot of this file. */
  --hov-bg: transparent; --hov-fg: var(--amber); --hov-line: var(--amber); }
.bpf-pick .ico { width: 13px; height: 13px; flex: none; }
.bpf-pick[disabled] { opacity: 0.55; cursor: progress; }
/* At a phone width the label already sits on its own line, so the button keeps its words rather
   than becoming an unlabelled glyph — this is the one control in the modal nobody would guess. */
@media (max-width: 360px) { .bpf-pick { padding: 0.3rem 0.4rem; } }
/* A checkbox row is a label with the tick beside the words, not a value in the right-hand column —
   a lone tick under a heading reads as an unlabelled control. */
.bpf-bool { grid-template-columns: auto 1fr; cursor: pointer; }
.bpf-bool input { accent-color: var(--amber); width: 15px; height: 15px; margin: 0; flex: none; }
.bpf-foot {
  flex: none; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem;
  padding: 0.6rem 1.1rem 0.9rem; border-top: 1px solid var(--line);
}

/* ── zones: the areas, drawn the way the game's own map draws them ────────────────────────────
   `pointer-events: none` on the SHAPE is load-bearing, not a tidy default. A trader circle is
   500 m across — at map scale that is a large part of a sector — and a clickable one would swallow
   every click on every marker standing inside it: the players, the vehicles, the base flags, the
   right-click that opens the spot menu. The NAME LABEL takes the pointer instead, so a zone is
   picked up by its name, which is also what makes dragging one precise rather than a guess about
   which of two overlapping circles was meant. */
.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 — measured: 118% of the map
   in each direction — 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; }
/* ONE value per layer, feeding THREE things: the shape's ring and fill through `color`, the marker
   dot, and — the one that was missed — the `--badge-bg` behind the icon in the panel's own layer
   list. `.ml-badge .ico` is painted `var(--bg)` on purpose, because it sits on a square filled with
   the layer's colour; a class that sets `color` and forgets `--badge-bg` leaves that square
   TRANSPARENT and the glyph is near-black on the panel, at 1.14:1. Nothing about that is visible in
   review — the markup is right, the sprite ids all resolve, and no rule is wrong on its own. It is
   the same defect `check-panel-render` was written for, found by it again, on the day it was added. */
.zn-custom  { --zn-c: #6ee7c8; }
.zn-outpost { --zn-c: #e6b96a; }
.zn-hazard  { --zn-c: #b6e34a; }
.zn-border  { --zn-c: #8ea2c0; }
/* The two AUTHORED sets. Both sit under everything a player can change, and both are picked to
   stay apart from the four above AND from each other where they overlap — 474 biome circles lie on
   top of one another by design, so a hue near the quest one would make the two layers one wash. */
/* ── how much a SHAPE can be trusted ──────────────────────────────────────────────
   A rotated radiation zone is drawn twice, because the game does not report the angle: the circle
   that fits inside it is right everywhere and too small, and the ellipse reaches exactly as far as
   the zone does with its corners pointing the wrong way. The certain one is solid and this is the
   other, dashed — the reader can see there are two claims and the card says which is which.
   Fainter as well as dashed, because it is the outer of the two and a hard edge would read as the
   boundary rather than as its limit. */
.map-zone.zn-uncertain { border-style: dashed; opacity: 0.55; }
/* …and the hottest part, drawn INSIDE its own zone rather than as the zone. Solid and brighter:
   this is the half of a radiation reading that is not a guess at all. */
/* THE ONE ZONE SHAPE THAT TAKES A POINTER, and it is small on purpose.

   `.map-zone` is `pointer-events: none` so a 500 m circle does not swallow the markers under it,
   and radiation's areas are far bigger than that. Its name pill used to be the way into its card;
   the pills are gone (28 of them said one word), so the CORE is the handle: already drawn, at the
   zone's centre, 9-12 m across. Small enough to swallow nothing, and the right shape to mean "this
   zone" — it is the peak the card's two rows are about. */
.map-zone.zn-core { opacity: 0.95; border-width: 2px; pointer-events: auto; cursor: pointer; }
.zn-custom, .zn-outpost, .zn-hazard, .zn-border { --badge-bg: var(--zn-c); }
.map-zone.zn-custom, .map-zone.zn-outpost,
.map-zone.zn-hazard, .map-zone.zn-border { color: var(--zn-c); }
/* ⚠ THE NAME AND THE NORTH RESIZE HANDLE WERE AT THE SAME POINT.
   `.zn-name` was `left: 50%; top: 0` with `translate(-50%, -50%)`, so its centre sat exactly on the
   middle of the shape's top edge — and `.zn-h-n` is `left: 50%; top: 0` with a 12px box centred on
   the same pixel. The handle is `z-index: 3` and the name is not, so the handle painted straight
   through the word: "Outpost A0" rendered as ◻tp◻st ◻◻, and on a zone small enough for all eight
   handles to pile into one blob that blob sat in the middle of the name. It is a screenshot
   finding — every declaration here is valid, the text is present and correct in the DOM, and no
   rule that reads CSS or reads the page's text could see it.

   So the name goes fully ABOVE the edge instead of on it. The offset is in PERCENT OF THE NAME'S
   OWN HEIGHT rather than pixels, which is what keeps it right at every zoom: the pill and the
   handles are both counter-scaled by `--mz`, so a fixed pixel gap would close as the map zoomed
   out while a proportional one cannot. −160% clears the handle's 6px reach with the pill's own
   height to spare, and the label is still visually attached to the top of its shape. */
.zn-name {
  position: absolute; left: 50%; top: 0; transform: translate(-50%, -160%);
  max-width: 12rem; padding: 0.1rem 0.45rem;
  background: rgba(11, 12, 14, 0.82); border: 1px solid currentColor; border-radius: var(--pill);
  color: var(--text); font-family: var(--ui); 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 `mapApplyTransform`. */
  scale: var(--mz, 1);
}
.zn-name:hover { border-color: var(--amber-hi); color: var(--amber-hi); }
/* The marker badges for the two POINT layers, so an outpost and a guarded zone are coloured like
   their own areas rather than borrowing another layer's. */
.zn-outpost .mk-dot, .ml-dot.zn-outpost { background: var(--zn-c); }
/* …and the two AUTHORED layers, which draw an area AND a point: the circle says how big it is and
   the marker is what a reader clicks, because a zone's body takes no pointer events.

   ⚠ These two lines were missing for exactly one render, and the comment above this block is the
   reason `check-panel-render` found it in seconds: `.mk-dot`'s glyph is painted `var(--bg)` on
   purpose, so a marker class that sets `--zn-c` and forgets the FILL leaves a near-black glyph on a
   near-black map — measured at 1.02:1, on all six markers, with every sprite id resolving and
   nothing in the markup wrong. A layer that draws a point needs its dot painted; setting the token
   is not the same as using it. */

/* The zones section of the side panel. Its own block rather than five rows in the layer list: a
   zone has no count, no sub-filter and no type filter, and putting it there would give it three
   controls that could never do anything. */
.map-zones-panel { border-top: 1px solid var(--line); }
/* ONE left edge down the whole column, and that is the whole of the "the switches don't sit right"
   report. `.map-layers` — the box every one of these lives in — has `padding: 0.6rem`, so the five
   layer cards start 0.6rem in. Everything added beside them carried `margin: … 0.75rem` instead,
   which is 0.6 + 0.75 = 1.35rem: fifteen pixels further in than the rows directly above them, on a
   column two hundred and eighty pixels wide. Five controls, four of them at one indent and the
   cards at another, reads as a panel that was never laid out — and no single rule is wrong, which
   is why it survived. The horizontal margin is now zero on all of them and the container's own
   padding is the only thing that sets the edge. */
.map-zone-edit { margin: 0.5rem 0 0; display: flex; }
/* Armed, in the panel's own danger colour — the same treatment move mode gets, because it is the
   same kind of switch: with it on, one click offers to delete something the game then SAVES. */
.map-zones-panel.zones-armed .map-zone-edit {
  padding: 0.34rem 0.5rem; border: 1px solid var(--alarm); border-radius: var(--radius);
  background: rgba(255, 92, 92, 0.16); color: #f4bdbb;
}
.map-zone-note {
  margin: 0.5rem 0 0; font-size: 0.72rem; line-height: 1.5; color: var(--muted);
}
.map-zone-note:empty { display: none; }

/* ── the layer panel, given a shape ───────────────────────────────────────────────────────────
   Nineteen layers, each with up to two filter dropdowns under it, is two and a half screens of
   scrolling in the same column as the search and the player filter — and every one of them looks
   like the same kind of thing as every other, so finding "Cargo drops" means reading past Turrets.
   Nothing is merged and nothing is renamed (every key is one an owner may already have set, and
   `EXPORT_LAYERS` names them too); what changes is that they now arrive in three headed groups with
   the empty ones folded away together. */
.ml-grp {
  padding: 0.55rem 0.15rem 0.2rem; font-family: var(--ui); font-size: 0.62rem;
  font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
  pointer-events: none; user-select: none;
}
.ml-grp:not(:first-child) { border-top: 1px solid var(--line); margin-top: 0.3rem; }
/* A layer with nothing in it still earns a switch — "Sentries exist and are empty" is a different
   fact from "sentries are not a thing here" — but not a place among the ones that do. */
.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.15rem;
  /* The pair, retuned on the element — never one half in a :hover rule. This one is a ghost: no
     fill, the colour change is the whole affordance. */
  --hov-bg: transparent; --hov-fg: var(--text); --hov-line: currentColor;
}
.ml-empties > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }
/* The sentence under the "nothing can answer these" disclosure. It is TEXT rather than a title,
   because a phone has no hover and this is the one fact that separates "your island has no puppets"
   from "nobody can be asked about puppets right now" — which is the whole reason the two groups are
   separate. Word-wrapping, not truncated: a sentence cut off mid-clause says neither. */
.ml-unavail-why {
  font-family: var(--ui); font-size: 0.66rem; line-height: 1.45; color: var(--muted);
  padding: 0.15rem 0.15rem 0.4rem;
}

/* ══ THE MAP YOU CAN PULL ON ═══════════════════════════════════════════════════════════════════
   Everything below belongs to one change: a live map that is manipulated directly — a zone dragged
   by its corner, a region drawn with a finger, a marker's own actions on the card rather than two
   clicks inside a menu. Nothing here is a new colour or a new shape; it is the panel's own tokens
   applied to controls that did not exist.

   The two rules this stylesheet is judged against are honoured the same way everywhere in it:
   a hover NEVER overrides one of `background` / `color` on its own — the pair is retuned on the
   element through `--hov-bg` / `--hov-fg` / `--hov-line` — and no grid minimum is a bare length. */

/* ── a zone's grab handles ─────────────────────────────────────────────────────────────────────
   Always in the markup (see `zoneShapeHtml`, which says why at length), and inert until the host
   carries `zones-edit`. Inert here means NO POINTER and no ink: a handle that could be clicked
   while the editor is off would put eight 14px dead zones on top of every zone on the island, and
   `.map-zone` is `pointer-events: none` precisely so a 500 m circle does not swallow the markers
   standing inside it. This must not undo that.

   Not `display: none`, and not behind a media query: the handles are positioned relative to a shape
   whose size changes on every zoom, and a hidden element has no layout to keep. Opacity costs
   nothing and keeps the geometry live, so arming the editor cannot make a zone jump. */
.zn-h {
  position: absolute; z-index: 3; width: 12px; height: 12px; margin: -6px 0 0 -6px;
  border: 1px solid currentColor; border-radius: 3px;
  background: var(--panel);
  opacity: 0; pointer-events: none;
  /* Counter-scaled with the markers and the zone names, so a handle is the same size to the hand at
     every zoom instead of growing into a tile across the island. `--mz` is set on the canvas by
     `mapApplyTransform`. */
  scale: var(--mz, 1);
}
.zn-h-nw { left: 0;    top: 0; }
.zn-h-n  { left: 50%;  top: 0; }
.zn-h-ne { left: 100%; top: 0; }
.zn-h-e  { left: 100%; top: 50%; }
.zn-h-se { left: 100%; top: 100%; }
.zn-h-s  { left: 50%;  top: 100%; }
.zn-h-sw { left: 0;    top: 100%; }
.zn-h-w  { left: 0;    top: 50%; }
/* The move grip is the centre, and it is round so it cannot be mistaken for a resize handle at a
   glance. It is the second way to pick a zone up — the name label is the first — and it exists
   because a zone whose name has been panned off screen still has a middle. */
.zn-h-move {
  left: 50%; top: 50%; width: 15px; height: 15px; margin: -7px 0 0 -7px; border-radius: 50%;
}
.map-markers.zones-edit .zn-h { opacity: 0.9; pointer-events: auto; }
.map-markers.zones-edit .zn-h:hover { background: var(--amber-hi); border-color: var(--amber-hi); }
.map-markers.zones-edit .zn-h-nw, .map-markers.zones-edit .zn-h-se { cursor: nwse-resize; }
.map-markers.zones-edit .zn-h-ne, .map-markers.zones-edit .zn-h-sw { cursor: nesw-resize; }
.map-markers.zones-edit .zn-h-n,  .map-markers.zones-edit .zn-h-s  { cursor: ns-resize; }
.map-markers.zones-edit .zn-h-e,  .map-markers.zones-edit .zn-h-w  { cursor: ew-resize; }
.map-markers.zones-edit .zn-h-move { cursor: grab; }
/* A circle has one number, so its four corner handles resize it exactly as its edge handles do —
   the cursor says so rather than promising a corner that can be pulled on its own axis. */
.map-markers.zones-edit .map-zone.zn-round .zn-h { cursor: nwse-resize; }
.map-markers.zones-edit .map-zone.zn-round .zn-h-move { cursor: grab; }
.map-viewport.map-zonedrag, .map-viewport.map-zonedrag .zn-h { cursor: grabbing; }
/* While a zone is being pulled it is the only thing on the map that matters. The ring thickens
   rather than the fill brightening, because the fill is what the reader is judging against the
   ground underneath it. */
.map-viewport.map-zonedrag .map-zone.zn-custom { border-width: 2px; }

/* ── the numbers, while the hand is still on it ────────────────────────────────────────────────
   Top-centre of the viewport, which is `.map-empty`'s corner — and `.map-empty` cannot be on screen
   at the same time as a zone somebody is dragging, because dragging one means there is a zone. */
.map-geom-hud {
  position: absolute; z-index: 30; top: 0.5rem; left: 50%; transform: translateX(-50%);
  max-width: min(22rem, calc(100% - 1rem));
  display: flex; flex-direction: column; align-items: center; gap: 0.1rem;
  padding: 0.3rem 0.7rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.74rem; line-height: 1.45;
  text-align: center; overflow-wrap: anywhere; pointer-events: none;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
}
.map-geom-hud .mgh-n { color: var(--amber-hi); font-size: 0.72rem; }
.map-geom-hud .mgh-s { font-family: var(--mono); font-size: 0.86rem; color: var(--text); }
.map-geom-hud .mgh-p { font-family: var(--mono); font-size: 0.7rem; color: var(--muted); }
.map-geom-hud .mgh-h { font-size: 0.66rem; color: var(--faint); }

/* ── the game's chat, in the corner of the map ─────────────────────────────────────────────────
   Inside the viewport, so it costs the map no width, and over the sea rather than the island at the
   default corner. Bounded by the viewport when dragged; `min()` on the width so a phone gets the
   whole column rather than a panel with its right-hand side off the screen. */
.map-chat {
  position: absolute; left: 12px; bottom: 12px; z-index: 28;
  width: min(23rem, calc(100% - 24px)); max-height: min(60%, 26rem);
  display: flex; flex-direction: column;
  background: rgba(14, 15, 12, 0.94); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 14px 40px rgba(0,0,0,0.5); overflow: hidden;
}
.map-chat-h {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.4rem 0.45rem 0.4rem 0.6rem; border-bottom: 1px solid var(--line);
  font-family: var(--ui); font-size: 0.78rem; font-weight: 600; color: var(--text);
  cursor: grab; user-select: none;
}
.map-chat-h.mc-dragging { cursor: grabbing; }
/* The controls inside the panel are controls, not map. The viewport's gestures already stand aside
   for `.map-chat` — this is the other half: nothing in here inherits the header's grab cursor or
   its text-selection block, so the log can be selected and copied like any other text. */
.map-chat-log, .map-chat-send { user-select: text; cursor: auto; }
.map-chat-h .mc-title { flex: 1 1 auto; min-width: 0; }
.map-chat-h .mc-state { font-weight: 400; font-size: 0.7rem; color: var(--standby); }
.map-chat-log {
  flex: 1 1 auto; min-height: 4rem; overflow-y: auto; overscroll-behavior: contain;
  padding: 0.35rem 0.55rem; display: flex; flex-direction: column; gap: 0.18rem;
}
.map-chat-log .mc-empty { color: var(--muted); font-size: 0.76rem; margin: 0.4rem 0; }
/* One line per message, wrapping under itself: a name and a sentence, with the time and the channel
   as the two things you scan down rather than read. */
.mc-line {
  display: grid; grid-template-columns: auto auto 1fr; gap: 0.35rem;
  font-size: 0.78rem; line-height: 1.35; align-items: baseline;
}
.mc-line .mc-t { font-family: var(--mono); font-size: 0.68rem; color: var(--faint); }
.mc-line .mc-ch { font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.mc-line .mc-ch-admin { color: var(--amber-hi); }
.mc-line .mc-ch-local { color: var(--ok, #8fe0a8); }
.mc-line .mc-who { grid-column: 3; font-weight: 600; color: var(--amber-hi); }
.mc-line .mc-msg { grid-column: 3; color: var(--text); overflow-wrap: anywhere; }
/* What arrived since the last draw. A quiet mark rather than a colour change: the point is to find
   the new lines in a list you were already reading, not to make them shout. */
.mc-line.mc-new .mc-who::before { content: '•'; color: var(--standby); margin-right: 0.25rem; }
.map-chat-send {
  display: flex; gap: 0.3rem; padding: 0.4rem 0.45rem; border-top: 1px solid var(--line);
}
.map-chat-send select {
  flex: 0 0 auto; max-width: 7rem; font-size: 0.72rem; padding: 0.25rem 0.35rem;
}
.map-chat-send input {
  flex: 1 1 auto; min-width: 0; font-size: 0.78rem; padding: 0.25rem 0.45rem;
}
.map-chat-send button {
  flex: 0 0 auto; padding: 0.25rem 0.55rem;
}

/* ── the two ends of a zone draft, on the map itself ───────────────────────────────────────────
   Nothing a gesture on this map does reaches the game until Send is pressed, so both ends of that
   have to be here: an owner who nudges four zones must not have to open a panel to finish. Full
   width of the column and wrapping, because the count sits inside the label. */
.map-zone-send {
  display: flex; flex-direction: column; gap: 0.35rem;
  margin: 0.5rem 0 0; width: 100%;
}
.map-zone-send button {
  display: flex; align-items: center; gap: 0.4rem; width: 100%;
  padding: 0.34rem 0.55rem; font-size: 0.72rem; line-height: 1.4;
  text-align: left; white-space: normal; overflow-wrap: anywhere;
}
.map-zone-send .ico { flex: none; }

/* ── the region selector ───────────────────────────────────────────────────────────────────────
   Drawn INSIDE the canvas so it stays over the same ground while the map is panned and zoomed, and
   `pointer-events: none` for the same reason the zones are: a selection is a thing you look at, and
   one that could be clicked would take every marker inside it out of reach. */
.map-sel {
  position: absolute; z-index: 1; pointer-events: none;
  border: 1px dashed var(--amber-hi);
  background: rgba(255, 106, 26, 0.10);
}
.map-sel.sel-round { border-radius: 50%; }
.map-viewport.map-selarmed { cursor: crosshair; }
/* A drawn region can be picked up and carried. The cursor is the whole affordance — a dashed shape
   says nothing about being grabbable — so the pair is the ordinary one every map in the world uses:
   an open hand over it, a closed hand while it is in the air. `map-selgrab` is set only while the
   pointer is really inside the shape, so the rest of the map keeps whatever cursor it had. */
.map-viewport.map-selgrab { cursor: grab; }
.map-viewport.map-selmoving,
.map-viewport.map-selmoving .map-mk { cursor: grabbing; }
/* …and while it is in the air the shape itself is solid rather than dashed, which is the second
   half of the same signal: this one is being held. */
.map-viewport.map-selmoving .map-sel { border-style: solid; }
.map-viewport.map-selarmed .map-mk { cursor: crosshair; }

/* The armed banner. Same corner and same shape as move mode's, in the panel's amber rather than its
   alarm colour — this mode changes nothing, and dressing it in the colour that means "this writes
   to the running world" would spend the one signal that has to keep meaning that. */
.map-sel-note {
  position: absolute; z-index: 16; top: 0.5rem; left: 0.5rem;
  max-width: calc(100% - 9rem);
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;
  padding: 0.22rem 0.5rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--pill);
  color: var(--text); font-family: var(--ui); font-size: 0.72rem; line-height: 1.4;
  overflow-wrap: anywhere;
}
.map-sel-note .msn-t { color: var(--muted); }
/* The shape switch, inside the banner rather than in the toolbar: it is only answerable while the
   mode is armed, and a control that is meaningless nine tenths of the time does not earn a
   permanent seat. Ghost when off, filled when on — the pair declared on the element either way. */
.msn-shape {
  padding: 0.1rem 0.5rem; border: 1px solid var(--line); border-radius: var(--pill);
  background: transparent; color: var(--muted);
  font-family: var(--ui); font-size: 0.68rem; line-height: 1.5;
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: var(--line-amber);
}
.msn-shape.on {
  background: var(--amber); border-color: var(--amber); color: var(--on-amber);
  --hov-bg: var(--amber-hi); --hov-fg: var(--on-amber); --hov-line: var(--amber-hi);
}

/* ── the review bar: what is inside, before anything is sent ───────────────────────────────────
   Bottom-LEFT, which is the only corner of this viewport nothing else claims: top-left is the armed
   banners, top-centre `.map-empty`, top-right the source badge, bottom-centre `.map-trunc` and
   bottom-right the zoom stack. It is capped at the viewport's own width minus a margin and every
   part of it wraps, so a 320px phone gets a taller bar and never a sideways scroll. */
.map-selbar {
  position: absolute; z-index: 18; left: 0.5rem; bottom: 0.5rem;
  width: min(22rem, calc(100% - 1rem)); max-height: 62%; overflow-y: auto;
  display: flex; flex-direction: column; gap: 0.4rem;
  padding: 0.5rem 0.6rem;
  background: var(--panel); border: 1px solid var(--line-amber); border-radius: var(--radius);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  font-family: var(--ui); color: var(--text);
}
.map-selbar .msb-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.5rem; }
.map-selbar .msb-t { font-size: 0.8rem; color: var(--amber-hi); overflow-wrap: anywhere; }
.map-selbar .msb-p { font-family: var(--mono); font-size: 0.7rem; color: var(--muted); overflow-wrap: anywhere; }
.map-selbar .msb-chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.map-selbar .msb-chip {
  padding: 0.08rem 0.45rem; border: 1px solid var(--line); border-radius: var(--pill);
  background: var(--inset); color: var(--text); font-size: 0.68rem; line-height: 1.6;
  overflow-wrap: anywhere;
}
.map-selbar .msb-chip.msb-none { color: var(--muted); border-style: dashed; }
.map-selbar .msb-note { margin: 0; font-size: 0.68rem; line-height: 1.5; color: var(--muted); overflow-wrap: anywhere; }
.map-selbar .msb-acts { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.map-selbar .msb-b {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.24rem 0.55rem; font-size: 0.72rem; line-height: 1.4; white-space: nowrap;
}
.map-selbar .msb-b .ico { width: 13px; height: 13px; flex: none; }

/* The two arming buttons on the screen bar. `map-arm-safe` states both halves in one go, exactly as
   `#map-drag[aria-pressed="true"]` does above — the difference between them is the colour, and the
   colour is the whole message: alarm means this writes to the running world, amber means it does
   not. */
#map-select[aria-pressed="true"] {
  border-color: var(--amber); background: rgba(255, 106, 26, 0.18); color: var(--amber-hi);
  --hov-bg: rgba(255, 106, 26, 0.28); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}

/* ── the layer panel's header row ──────────────────────────────────────────────────────────────
   It folds with the list it describes, because it describes that list and nothing else. */
.map-filters.collapsed .map-lbar { display: none; }
.map-lbar {
  display: flex; flex-direction: column; gap: 0.45rem;
  /* ALIGNED WITH THE LIST IT DESCRIBES. It used to carry 0.15rem at the sides against the layer
     list's 0.6rem, so the summary and its four pills sat hard against both edges of the column
     while everything under them was inset — the owner's *"je to tam nalepene na okraje a neni to
     hezke"*. The number is the list's, not a new one: two paddings that mean "the inside of this
     panel" and disagree is how a column comes to look like two panels. */
  padding: 0.55rem 0.6rem 0.6rem;
  border-bottom: 1px solid var(--line-2); margin-bottom: 0;
}
.map-lbar .mlb-sum {
  margin: 0; font-family: var(--ui); font-size: 0.7rem; line-height: 1.55; color: var(--muted);
  overflow-wrap: anywhere;
}
.map-lbar .mlb-btns { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.map-lbar .mlb-b {
  padding: 0.2rem 0.6rem;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--pill);
  color: var(--muted); font-family: var(--ui); font-size: 0.68rem; line-height: 1.5;
  overflow-wrap: anywhere; text-align: left;
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: var(--line-amber);
}
/* "Clear filters" is the one of the four that undoes something the reader chose, so it is the one
   that carries a colour — and it is only on screen when there is something to clear. */
.map-lbar .mlb-clear {
  color: var(--amber-hi); border-color: var(--line-amber);
  --hov-bg: rgba(255, 106, 26, 0.16); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}
/* A layer whose badge reads `3/148`. The badge is the only place the difference between "this layer
   holds 148 things" and "this layer is drawing 3 of them" can be seen at a glance, so it is marked
   rather than left to be spotted by reading the slash. */
.map-layer .map-count.mc-narrowed {
  color: var(--amber-hi); background: rgba(255, 106, 26, 0.14);
  box-shadow: inset 0 0 0 1px var(--line-amber);
}

/* ── LIVE OR SAVED, said per layer ─────────────────────────────────────────────────────────────
   The badge over the map answers for the whole screen; these answer for one section of the list,
   which is the case the badge cannot cover — a list where the bases are minutes old and the players
   are not. Small, lower-case and beside the layer's own name, because it is a caveat on that name
   rather than a status of its own; the colour is what makes a mixed list readable at a glance. */
.ml-src {
  padding: 0 0.35em; border-radius: var(--pill);
  font-family: var(--ui); font-size: 0.58rem; line-height: 1.7;
  letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap;
  vertical-align: 0.08em;
}
/* Stated as a pair in one rule each — the ground and the ink together, never one of them tinted
   over whatever happened to be underneath. */
.ml-src-live  { background: rgba(84, 201, 138, 0.16); color: #a9dc86; }
.ml-src-saved { background: rgba(230, 178, 58, 0.16); color: #e4c079; }

/* "There is no saved copy of this", which is a different sentence from a mod refusing and from an
   empty world. It borrows `.ml-off`'s shape and softens its edge: a refusal is something to act on,
   this is the ordinary state of a stopped server and dressing it as a fault would be the "reads as
   a fault" wording this whole pass is about, in CSS. */
.ml-off-nosave { border-style: dashed; opacity: 0.9; }

/* The zone editor with nothing running. The screen stays exactly as it is — every row, every
   number, every rule — and only the controls go dead, because being unable to change a thing is not
   a reason to stop showing it. The sentence at the top is what turns a screenful of greyed inputs
   from "broken" into "read-only, and here is why". */
.zed-stopped-note {
  margin: 0 0 0.9rem; padding: 0.45rem 0.6rem;
  background: rgba(230, 178, 58, 0.10); border: 1px solid rgba(230, 178, 58, 0.35);
  border-radius: var(--radius); color: #e4c079;
}
.zed-body.zed-stopped .zed-sec input:disabled,
.zed-body.zed-stopped .zed-sec select:disabled { opacity: 0.75; cursor: default; }

/* The same sentence, on a marker's card, under the one disabled button. A `title` is unreachable
   without a mouse, and on this panel most readers have none. */
.mp-act-why {
  margin: 0.4rem 0 0; font-family: var(--ui); font-size: 0.68rem; line-height: 1.5;
  color: var(--muted); overflow-wrap: anywhere;
}

/* ── a counter pill NEVER shrinks below its own digits ────────────────────────────────────────
   Both of these live in a flex row and both declare `min-width`, which OVERRIDES a flex item's
   default `min-width: auto` — the one thing that was stopping them being squeezed. So the pill
   could be shrunk to 1.6em by anything that took width beside it and the third digit was simply
   clipped: `152` came out as `15`, with no ellipsis and nothing wrong-looking about it. Found by
   the render rig on the day a source chip was added to that header, which is what took the width;
   the fault was always there and needed only one more thing in the row.

   `flex: none` is the fix rather than a bigger `min-width`, because the number has no ceiling: a
   live layer can report five figures, and a width chosen for four is the same bug later. */
.ml-sec-h .ml-sec-n, .map-layer .map-count { flex: none; }
/* …and the TITLE is what gives the width up instead, which is the right one to shorten: it is a
   word the reader already knows from the layer list, and the count is the thing they came for. */
.ml-sec-h { min-width: 0; }

/* ════════════════════════════════════════════════════════════════════════
   THE ZONE WORKSPACE — the game's own zone editor, on the panel

   Two lists on the left, one panel on the right, and Exit / Apply / Reset
   along the bottom: the shape of SCUM's own screen, because an admin who
   has set a region up in game should recognise this one without being told
   anything. The class vocabulary is deliberately shared with the editor it
   replaces — `.zed-modal`, `.zed-body`, `.zed-grid`, `.zed-row`, `.zed-rl`
   — so one set of rules dresses both and the render rig goes on measuring
   the rows it was written to measure.

   Everything here obeys the two rules the panel is checked against: a
   grid minimum is `minmax(min(100%, N), …)` and never a bare length, and
   no `:hover` states one half of the pair — the trio travels on the
   ELEMENT so both halves move together by construction.
   ════════════════════════════════════════════════════════════════════════ */

.zw-modal { width: min(1100px, 100%); }
/* The two columns, and the scroll INSIDE them rather than on the modal: the
   lists are short and the matrix is not, and a modal that scrolls as one
   takes the region list off screen the moment somebody opens a group. */
.zw-main {
  display: grid;
  grid-template-columns: minmax(min(100%, 14rem), 17rem) minmax(0, 1fr);
  min-height: 0; flex: 1 1 auto;
}
.zw-side {
  min-width: 0; min-height: 0; overflow-y: auto;
  border-right: 1px solid var(--line); background: var(--inset);
  padding: 0.6rem 0.5rem 0.9rem;
}
.zw-modal .zed-body { min-height: 0; }
/* One column on a phone, with the lists first: picking what to edit comes
   before editing it, and a 14rem column beside a matrix is neither. */
@media (max-width: 760px) {
  .zw-main { grid-template-columns: minmax(0, 1fr); }
  .zw-side { border-right: 0; border-bottom: 1px solid var(--line); max-height: 40vh; }
}

/* ── the two lists ──────────────────────────────────────────────────────── */
/* The read-only sentence, in the column whose controls are dead. Same treatment as the long one at
   the top of the panel beside it, so the two read as one statement rather than two warnings. */
.zw-side-note {
  margin: 0 0 0.6rem; padding: 0.35rem 0.5rem;
  background: rgba(230, 178, 58, 0.10); border: 1px solid rgba(230, 178, 58, 0.35);
  border-radius: var(--radius); color: #e4c079;
}
.zw-list + .zw-list { margin-top: 0.9rem; padding-top: 0.7rem; border-top: 1px solid var(--line); }
/* The heading starts where the NAMES under it start, not where the rows' colour bars do — the same
   sum, written as the sum, so moving the bar or the gap moves both together instead of leaving a
   number here that used to be right. */
.zw-lh { display: flex; align-items: center; gap: 0.4rem; padding: 0 0.2rem 0.4rem calc(3px + 0.45rem); }
.zw-lh h4 {
  margin: 0; flex: 1; min-width: 0;
  font-size: 0.74rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--amber);
}
/* `3/100` — the count and the ceiling together, the way the game prints them.
   `flex: none` because the number has no ceiling of its own and a width chosen
   for three digits is the same clipping bug later. */
.zw-count {
  flex: none; padding: 0 0.4em; border-radius: var(--pill);
  background: var(--panel-2); color: var(--muted);
  font-family: var(--mono); font-size: 0.68rem; line-height: 1.7;
}
.zw-add {
  flex: none; width: 1.5rem; height: 1.5rem; padding: 0; border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--amber-hi);
  font-size: 1rem; line-height: 1;
  --hov-bg: var(--panel-2); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}
.zw-rows { display: flex; flex-direction: column; gap: 0.15rem; }
/* The priority-order paragraph under the regions. It is a caption on the list, and it was sitting
   0.5rem under the last row at the same indent as the rows themselves — one more block of text in a
   column of blocks of text, with nothing saying it had stopped being a row. A rule and its own
   indent make it an aside. `.zw-rows +` rather than a bare `.zed-note` so the "this server has no
   regions" line, which is INSIDE the list, is not dressed as a footnote to itself. */
.zw-list .zw-rows + .zed-note {
  margin: 0.6rem 0 0; padding: 0.55rem 0.2rem 0 calc(3px + 0.45rem);
  border-top: 1px solid var(--line-2);
}
.zw-rows > .zed-note { margin: 0.15rem 0 0; padding: 0 0.2rem 0 calc(3px + 0.45rem); }
.zw-row {
  display: flex; align-items: stretch; gap: 0.15rem; min-width: 0;
  border-radius: var(--radius); border: 1px solid transparent;
}
.zw-row.on { border-color: var(--line-amber); background: rgba(255, 106, 26, 0.10); }
/* A row with something unsent on it. A left edge rather than a colour on the
   text: the name is the thing being read, and re-tinting it would fight the
   selection highlight that is already carrying a meaning. */
.zw-row.zw-dirty { box-shadow: inset 2px 0 0 var(--standby); }
.zw-pick {
  flex: 1; min-width: 0; display: flex; align-items: stretch; gap: 0.45rem;
  padding: 0.3rem 0.4rem 0.3rem 0; text-align: left;
  background: transparent; border: 0; color: var(--text); font-size: 0.78rem;
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: transparent;
}
.zw-nm { flex: 1; min-width: 0; align-self: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zw-meta { flex: none; align-self: center; color: var(--muted); font-size: 0.64rem; }
/* The configuration's colour, which is what the game paints the region with and
   what this map paints it with.
   A full-height BAR at the row's own left edge, which is what SCUM's list draws and what this was
   copied from. It was a 0.75rem rounded square with a dark ring, floating in the middle of the row
   between the padding and the name — and a small pale square with a dark outline, at the start of
   a row, IS a checkbox to anyone glancing at it. Three of them down the configurations list read as
   three unticked boxes, which is half of "the switches are white": a configuration with no colour
   of its own falls back to `--faint`, so the "unticked" state was literally the common one. A bar
   cannot be mistaken for a control, and it says the same thing in more of it. */
.zw-sw {
  flex: none; align-self: stretch; width: 3px; border-radius: 1px;
  background: var(--faint);
}
.zw-ord { flex: none; display: flex; flex-direction: column; justify-content: center; }
.zw-arrow, .zw-x {
  flex: none; padding: 0 0.3rem; background: transparent; border: 0; color: var(--muted);
  font-size: 0.6rem; line-height: 1;
  --hov-bg: transparent; --hov-fg: var(--amber-hi); --hov-line: currentColor;
}
.zw-x { display: flex; align-items: center; font-size: 0.75rem; --hov-fg: var(--alarm); }
.zw-arrow:disabled, .zw-x:disabled, .zw-add:disabled { opacity: 0.4; cursor: default; }

/* ── the detail panel ───────────────────────────────────────────────────── */
.zw-head-sec { padding-bottom: 0.2rem; }
.zw-title-row { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.zw-title-row h4 { flex: 1; min-width: 0; margin: 0; overflow-wrap: anywhere; }
.zw-ix { flex: none; font-family: var(--mono); font-size: 0.7rem; color: var(--muted); }
/* A rule that is being SHOWN rather than set — the region panel's copy of what
   its configuration says. Right-aligned in the same slot the control occupies on
   the editing screen, so the two read as the same table. */
.zw-ro { flex: 0 0 auto; max-width: 45%; text-align: right; font-size: 0.74rem; color: var(--muted); }
.zw-warn {
  padding: 0.45rem 0.6rem; border-radius: var(--radius);
  background: rgba(230, 178, 58, 0.10); border: 1px solid rgba(230, 178, 58, 0.35); color: #e4c079;
}
/* What the writer COMPARED, sitting immediately above the button that acts on it.
   Deliberately NOT `.zw-warn`: the warning is the refusal, and this is the
   evidence that makes one press an informed write rather than a confident one.
   It reads as an aside so the amber block above it keeps its weight, and both
   halves are declared here so a hover or theme change cannot take one without
   the other — the `check-contrast` rule, applied to a resting state. */
.zw-allow-note {
  margin: 0.45rem 0 0.5rem; padding: 0.45rem 0.6rem; border-radius: var(--radius);
  background: var(--panel-2, rgba(255, 255, 255, 0.04));
  border: 1px solid var(--line); color: var(--text);
  font-size: 0.72rem; line-height: 1.5;
}

/* ── a region's fields, laid out the way the game lays them out ──────────────
   Label on the left, the control (or the PAIR of controls) on the right, one
   row each, in the game's own order: Shape, Center, Radius (km),
   Configuration. That is what SCUM's region panel draws and what an admin who
   has used it will look for.

   This replaces a two-column `auto-fit` grid, which is the wrong tool for it:
   "Centre X" and "Centre Y" are ONE field with two boxes, and an auto-fit grid
   has no way to know that — at one width it put them side by side and at
   another it put Y under the shape stepper, which reads as two unrelated
   numbers rather than as a coordinate.

   Both flex bases are a `flex: 1 1 <basis>` with `min-width: 0`, so the row
   collapses to a stacked label-over-control on a narrow column instead of
   pushing the panel sideways — the `minmax(min(100%, N), 1fr)` rule, in its
   flexbox spelling. */
/* ── the island's own numbers ───────────────────────────────────────────── */
/* Every label here is a SENTENCE from the mod carrying its own unit — "how far out the dropship
   comes in from, in centimetres" — rather than a two-word caption, so the row wraps instead of
   squeezing the control off the end of a phone. */
.zw-isl-row { flex-wrap: wrap; }
.zw-isl-val { font-family: var(--mono); color: var(--text); }
/* A row nobody can write is not a disabled control, so it does not look like one: the number where
   the box would be, and one word saying which kind of row it is. Both halves declared together, so
   a theme change cannot take one without the other. */
.zw-isl-tag {
  flex: none; padding: 0 0.45em; border-radius: var(--pill);
  background: var(--panel-2); color: var(--muted); font-size: 0.64rem; line-height: 1.7;
}
.zw-isl-v { flex: 0 0 auto; width: 6rem; max-width: 40%; }
.zw-isl-set { flex: none; padding: 0.25rem 0.6rem; font-size: 0.72rem; }
.zw-fields { display: flex; flex-direction: column; gap: 0.5rem; }
.zw-fr { display: flex; align-items: center; gap: 0.6rem; min-width: 0; flex-wrap: wrap; }
.zw-frl { flex: 1 1 7rem; min-width: 0; font-size: 0.78rem; color: var(--text); overflow-wrap: anywhere; }
.zw-frv { flex: 2 1 min(100%, 15rem); min-width: 0; display: flex; gap: 0.4rem; align-items: stretch; }
.zw-frv > * { flex: 1 1 0; min-width: 0; }
.zw-frv input, .zw-frv select {
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--text); font-family: var(--ui); font-size: 0.82rem; padding: 0.34rem 0.45rem;
  line-height: 1.5;
}
.zw-frv input:focus, .zw-frv select:focus { border-color: var(--amber); outline: none; }
/* The second radius box on a CIRCLE. It is disabled and empty on purpose — the
   game shows it that way, and a hidden box would leave a reader who steps the
   shape to Rectangle hunting for where the number went. Muted ink on the same
   inset fill, stated as a pair so neither half can be tuned on its own. */
.zw-frv input:disabled, .zw-frv select:disabled {
  background: var(--inset); color: var(--muted); border-color: var(--line-2); cursor: default;
}
.zw-fr .zw-step { flex: 1 1 0; min-width: 0; }

/* The shape stepper: two arrows and the word between them, like the game's.
   It is a `.zed-f` cell like the four text boxes beside it, and it has to be the SAME HEIGHT as
   them or the grid row has two baselines — which is what "Name" and "Shape" had, seven pixels
   apart, side by side on the first row of the screen. The padding and the type size are the ones
   `.zed-f input` uses, stated here rather than inherited because the stepper is three elements and
   the input is one. */
.zw-step { display: flex; align-items: stretch; gap: 0.25rem; }
.zw-step button {
  flex: none; padding: 0.34rem 0.5rem; background: var(--panel-2);
  border: 1px solid var(--line); color: var(--muted); font-size: 0.7rem; line-height: 1.5;
  --hov-bg: var(--panel-2); --hov-fg: var(--amber-hi); --hov-line: var(--line-amber);
}
.zw-step-v {
  flex: 1; min-width: 0; text-align: center; padding: 0.34rem 0.3rem;
  background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius);
  font-size: 0.82rem; line-height: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── the three-way control ──────────────────────────────────────────────── */
/* `flex: 0 1 auto` with `min-width: 0`, and the two together are load-bearing rather than tidy.
   With `flex-shrink: 0` the control claims its MAX-CONTENT width — the three words side by side —
   and a wrapping parent obliges by making the line that wide. Measured at a true 310px viewport:
   14px of sideways scroll in English and 82px in German, where "Eigene Ignorieren Erlauben
   Blockieren" is half again as long. Shrinking is what lets its own `flex-wrap` do the wrapping. */
.zw-tri {
  flex: 0 1 auto; min-width: 0; max-width: 100%;
  display: flex; flex-wrap: wrap; gap: 0.15rem; align-items: center;
}
.zw-t {
  flex: none; padding: 0.14rem 0.42rem; border-radius: var(--radius);
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  font-family: var(--ui); font-size: 0.68rem; line-height: 1.5; white-space: nowrap;
  --hov-bg: var(--inset); --hov-fg: var(--amber-hi); --hov-line: var(--line-amber);
}
/* Both halves in one rule, always. A pressed state that changed only the fill
   would leave the muted label on amber, which is the exact defect
   `check-contrast` exists for. */
.zw-t.on {
  background: var(--amber); border-color: var(--amber); color: var(--on-amber);
  --hov-bg: var(--amber-hi); --hov-fg: var(--on-amber); --hov-line: var(--amber-hi);
}
.zw-t:disabled { opacity: 0.35; cursor: default; }
/* `Custom` is a READING and never a value — the mod refuses it as an argument
   and says so — so it is not a button at all. It looks like one because it sits
   in the row the game puts it in, and it cannot be pressed because there is
   nothing to press it to. */
span.zw-t.zw-custom { cursor: default; opacity: 0.6; }
span.zw-t.zw-custom.on {
  opacity: 1; background: rgba(230, 178, 58, 0.18); border-color: rgba(230, 178, 58, 0.5); color: #e4c079;
}
/* "Not set" goes to the LEFT of the three, not after them.
   It is the odd one out on a row, and putting it last moved every button on
   that row about 45px left of the rows that DO carry a rule — so sixty-one
   rows had two button columns in them, which is the first thing an eye reads
   as a mistake. The game's are all in one column. `order` rather than moving
   it in the markup, because the reading order (the rule, then its state) is
   right as it is for anyone listening to the page rather than looking at it. */
.zw-unset { order: -1; font-size: 0.62rem; color: var(--faint); }

/* ── the game's own fourteen sections ────────────────────────────────────────
   SCUM's zone screen is a stack of collapsible sections, each with its name on
   the left, the four words on the right and a triangle at the end, and the
   rules under it indented as plain label/control rows. This is that, and the
   captions in it are the game's own strings out of `zone-screen.json`.

   The header is a `<summary>` rather than a div with a button in it, so the
   whole strip is the fold's own control and the keyboard reaches it for free.
   `list-style: none` plus the ::-webkit- rule removes the browser's marker; the
   triangle is drawn instead, on the right, where the game draws it. */
.zw-secs { margin: 0.4rem 0 0; display: flex; flex-direction: column; gap: 0.25rem; }
.zw-sec { border: 1px solid var(--line); border-radius: var(--radius-md); overflow: hidden; }
.zw-sec > summary {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0;
  padding: 0.35rem 0.5rem; cursor: pointer; background: var(--inset);
  --hov-bg: var(--panel-2); --hov-fg: var(--text); --hov-line: currentColor;
}
.zw-sec > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }
.zw-sec > summary::-webkit-details-marker { display: none; }
.zw-sec > summary { list-style: none; }
.zw-sec-n {
  flex: 1 1 6rem; min-width: 0; font-size: 0.8rem; font-weight: 600;
  color: var(--text); overflow-wrap: anywhere;
}
/* The triangle, after the four words, pointing down when the section is open —
   the game's own affordance and position. `flex: none` so it never competes
   with the words for room on a narrow column. */
.zw-sec > summary::after {
  content: '▸'; flex: none; color: var(--muted); font-size: 0.7rem; line-height: 1;
}
.zw-sec[open] > summary::after { content: '▾'; }
.zw-sec > .zed-rows { padding: 0.45rem 0.6rem 0.55rem 1rem; gap: 0.25rem; }
/* A section drawn as VERDICTS on a region's panel. Same table, no controls —
   so the strip is quieter, because there is nothing on it to press. */
.zw-secs-ro .zw-sec > summary { background: transparent; }
.zw-secs-ro .zw-sec { border-color: var(--line-2); }
/* The four words on a section header keep to the right and wrap as a block
   rather than breaking between the buttons — `.zw-tri` already shrinks, this
   only stops the header's own `flex-wrap` from splitting them off one at a
   time on a phone. */
.zw-secall { flex: 0 1 auto; justify-content: flex-end; }

/* ── an instigator whose slot ORDER this build could not recover ─────────────
   A per-instigator state, never a mode: the fold is marked so it can be picked out of sixteen at a
   glance, and every other group in the matrix draws and works exactly as it would. Both halves of
   the pair stated in one rule each, so nothing here is a tint over whatever happened to be under
   it — the shape `check-contrast` exists to enforce. */
.zw-inst-unknown > summary { color: #e4c079; --hov-bg: transparent; --hov-fg: #f0d49a; --hov-line: currentColor; }
.zw-inst-unknown > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }
.zw-inst-unknown { border-color: rgba(230, 178, 58, 0.35); }
/* …and the same words where a rule's value would have been, so a row reads as answered rather than
   as blank. It sits in the slot the three-way control occupies, like every other read-only value. */
.zw-ro.zw-unknown { color: #e4c079; }

/* The group header: one control for a whole instigator-against-receiver pair,
   which is what the mod's own `damageall` verb addresses. */
.zw-ghead {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0;
  margin: 0.35rem 0 0.3rem; padding: 0.25rem 0.35rem;
  background: var(--inset); border-radius: var(--radius);
}
.zw-gl { flex: 1; min-width: 0; font-size: 0.72rem; color: var(--muted); overflow-wrap: anywhere; }
.zw-pair {
  margin: 0.3rem 0 0.3rem 0.4rem; padding: 0.2rem 0.4rem;
  border-left: 2px solid var(--line-2);
}
.zw-pair > summary {
  cursor: pointer; display: flex; gap: 0.4rem; align-items: baseline;
  font-size: 0.74rem; padding: 0.1rem 0;
  --hov-bg: transparent; --hov-fg: var(--amber); --hov-line: currentColor;
}
.zw-pair > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }
.zw-pair-n { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.zw-inst > summary .dim, .zw-pair > summary .dim { flex: none; color: var(--faint); font-size: 0.66rem; }

/* ── colour and the setting bits ────────────────────────────────────────── */
.zw-swatches { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; margin: 0.35rem 0 0; }
.zw-swatch {
  flex: none; width: 1.3rem; height: 1.3rem; padding: 0; border-radius: var(--radius);
  border: 1px solid rgba(0, 0, 0, 0.5);
  /* The fill IS the control, and it arrives inline, so hover must not paint over
     it — the affordance is the ring. */
  --hov-bg: transparent; --hov-fg: var(--text); --hov-line: var(--amber-hi);
}
.zw-swatch.on { box-shadow: 0 0 0 2px var(--amber-hi); }
.zw-swatch:disabled { opacity: 0.45; cursor: default; }
/* "or any colour" is a SENTENCE about the control to its right, sitting in a row of eight swatches
   0.3rem apart — so at the same 0.3rem it read as the caption of the swatch on its LEFT, which is a
   different colour entirely. A rule and a little air make it the start of its own thing instead;
   the row wraps, so on a narrow column the rule simply disappears with the wrap. */
.zw-anycol {
  display: flex; align-items: center; gap: 0.4rem; font-size: 0.66rem; color: var(--muted);
  margin-left: 0.25rem; padding-left: 0.65rem; border-left: 1px solid var(--line);
}
.zw-anycol input {
  width: 2rem; height: 1.3rem; padding: 0;
  border: 1px solid var(--line); border-radius: var(--radius); background: none; cursor: pointer;
  --hov-bg: transparent; --hov-fg: var(--text); --hov-line: var(--amber-hi);
}
.zw-anycol input:disabled { opacity: 0.45; cursor: default; }
.zw-checks { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; margin: 0.35rem 0 0; }
.zw-check { display: flex; align-items: center; gap: 0.4rem; font-size: 0.76rem; min-width: 0; cursor: pointer; }
/* The same 15px tick every other checkbox in this panel is (`.bpf-bool input`, `.adm-perm input`,
   `.eco-pick input`) — the colour comes from the base rule at the top of this file now, and this is
   only the size, so a zone tick and a bridge tick are the same object. */
.zw-check input[type="checkbox"] { width: 15px; height: 15px; margin: 0; flex: none; cursor: pointer; }
.zw-check input:disabled { cursor: default; }
.zw-check input:disabled + span { color: var(--muted); }
.zw-filter { margin: 0.5rem 0 0.2rem; color: var(--muted); font-size: 0.72rem; }

/* ── a chip, not a paragraph ─────────────────────────────────────────────
   The owner's second complaint was the volume of prose — *"vsude davas moc
   textu… kdo to vsechno ma cist"*. The sentences were added for real reasons and
   the reasons hold; what changed is the DELIVERY. A fact that fits in four words
   is a chip, and the paragraph behind it is the chip's own `title`.

   `zw-hastip` marks a line that carries more than it says. It is deliberately
   quiet — a dotted underline and the help cursor, nothing that competes with a
   control — because it is an offer rather than a warning. It also wraps a
   DISABLED control where one needs a tooltip: a disabled element receives no
   pointer events at all, so its own `title` never opens, and the state whose
   reason somebody needs is exactly that one. */
.zw-facts { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 0.5rem 0 0; }
.zw-fact {
  display: inline-flex; align-items: center; padding: 0.1rem 0.4rem;
  border: 1px solid var(--line); border-radius: 999px;
  font-size: 0.66rem; color: var(--muted); cursor: help; min-width: 0;
  overflow-wrap: anywhere;
}
.zw-hastip { cursor: help; text-decoration: underline dotted var(--line-2); text-underline-offset: 0.2em; }
.zw-hastip[title]:hover { text-decoration-color: var(--amber); }
span.zw-hastip { display: inline-flex; text-decoration: none; }

/* ── what a REGION inherits: read only, folded, and only what is set ─────
   The region panel owns where a region is; the configuration owns the rules.
   This is the digest between them, so it is a `<details>` whose summary carries
   the counts and whose rows are one verdict each — never a control. */
/* The region's copy is a `<div>` of sections now, not a `<details>` — the game prints
   them straight down the page. The box rules only apply to the fallback shape, which
   is still a fold, so they are scoped to it. */
details.zw-inherit { margin: 0.5rem 0 0; border: 1px solid var(--line); border-radius: var(--radius-md); padding: 0.35rem 0.6rem; }
details.zw-inherit > summary { cursor: pointer; font-size: 0.74rem; color: var(--muted); padding: 0.15rem 0; }
details.zw-inherit > summary { --hov-bg: transparent; --hov-fg: var(--amber); --hov-line: currentColor; }
details.zw-inherit > summary:hover { background: var(--hov-bg); color: var(--hov-fg); }
.zw-inherit h5 { margin: 0.5rem 0 0.25rem; font-size: 0.7rem; color: var(--muted); }
.zw-inherit .zed-row .zw-ro { max-width: 60%; }
/* The one control on that panel that leads anywhere: the configuration itself. */
.zw-goto { display: inline-flex; align-items: center; gap: 0.4rem; }

/* ── the footer ─────────────────────────────────────────────────────────── */
.zw-foot {
  flex: none; padding: 0.6rem 1rem 0.8rem; border-top: 1px solid var(--line);
  background: var(--panel); max-height: 34vh; overflow-y: auto;
}
.zw-foot-acts, .zw-foot-more { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.zw-foot-more { margin-top: 0.5rem; }
.zw-apply:disabled { opacity: 0.45; cursor: default; }
/* What is waiting, in the reader's own words, one line each. It is the whole
   argument for Apply: a screen that batches has to show what it is holding, or
   the button is a promise nobody can check. */
.zw-pending {
  margin: 0.6rem 0 0; padding: 0 0 0 1.1rem; max-height: 11rem; overflow-y: auto;
  font-size: 0.72rem; line-height: 1.6; color: var(--text);
}
.zw-pending li { overflow-wrap: anywhere; }

/* The change Apply stopped on, and the game's own sentence about it. Both halves are declared here
   rather than only the colour: the check-contrast rule is that a rule painting a surface and its
   text must pick two colours somebody can tell apart, and a red word on the panel's own ground is
   exactly where that goes wrong. The marker is a rule down the left rather than a fill, so the row
   stays readable at the 0.72rem this list is set in. */
.zw-pending li.zw-pend-bad {
  margin: 0.25rem 0; padding: 0.3rem 0 0.3rem 0.5rem;
  border-left: 2px solid rgba(214, 92, 76, 0.75);
  background: rgba(214, 92, 76, 0.08);
  color: var(--text);
}
.zw-pend-why {
  display: block; margin-top: 0.15rem;
  font-size: 0.95em; line-height: 1.5; color: #e8a294;
}

/* ── the three controls under the map ───────────────────────────────────── */
/* The way into the editor sits above the three, and is not behind the arm: opening it READS, and
   with the server stopped that reading is the whole of what an owner can do here. */
/* Zero horizontal margin, for the reason written beside `.map-zone-edit`: `.map-layers` already has
   0.6rem of padding and adding 0.75rem on top of it put these three-quarters of a centimetre inside
   the layer cards they sit under.
   And they read LEFT TO RIGHT like everything else in the column. `button` centres its contents,
   which is right for a dialog's Apply and wrong for a full-width row in a stack of full-width rows:
   with the labels centred, "Circle region", "Rectangle region" and "Remove region" each began at a
   different x, under five layer rows that all begin at the same one. The game's own map does the
   same thing — its three region buttons are icon-then-label, hard left. */
.map-zone-open {
  display: flex; align-items: center; justify-content: flex-start; gap: 0.4rem;
  margin: 0.5rem 0 0; font-size: 0.72rem;
}
.map-zone-acts { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.4rem 0 0; }
.map-zone-acts button {
  flex: 1 1 auto; min-width: 0; justify-content: flex-start;
  font-size: 0.7rem; padding: 0.25rem 0.5rem;
}
/* Armed: the next click on the map places one. Both halves stated together — a
   fill and the ink that has to read on it. */
.map-zone-acts button.armed {
  background: rgba(255, 106, 26, 0.18); border-color: var(--amber-hi); color: var(--amber-hi);
  --hov-bg: rgba(255, 106, 26, 0.18); --hov-fg: var(--amber-hi); --hov-line: var(--amber-hi);
}
/* The two shapes as shapes, the way the game labels its own pair. A glyph would
   have needed a sprite id that does not exist, and a missing one draws NOTHING. */
.mza-g { display: inline-block; width: 0.6rem; height: 0.6rem; border: 1px solid currentColor; vertical-align: -0.05em; }
.mza-c { border-radius: 50%; }

/* ── the backup list, and the one button in this product that replaces a save ─────────────────────
   Deliberately tiny: it borrows `.data-table` and `.table-wrap` whole, so the only new rules are
   the three kinds of thing a row has to SAY. No width is declared anywhere — `.table-wrap` already
   scrolls, and a fixed width over 320px in a modal is the defect `check-mobile` exists for. */
.bk-bad { color: var(--alarm); }
.bk-warn { color: var(--amber-hi, var(--amber)); }
/* A path or an error message: the one place here where the exact characters matter, and where
   wrapping mid-path is better than a modal that scrolls sideways. */
.bk-mono { font-family: var(--mono, monospace); font-size: 0.72rem; word-break: break-all; }
.bk-note { margin-top: 0.8rem; font-size: 0.78rem; }
.bk-table td:last-child { text-align: right; white-space: nowrap; }
/* A disabled Restore is a row whose backup has no save in it. It keeps its title, which is the
   sentence saying so, so the reason is one hover away rather than nowhere. */
.bk-table button[disabled] { opacity: 0.45; cursor: not-allowed; }

/* ══ SQUADS ═══════════════════════════════════════════════════════════════════════════════════════
   The Players screen and the player card with a squad in them, so almost everything here is
   BORROWED rather than declared: the table is `.data-table.roster-table`, the card is the `.pd-*`
   skeleton, the property list is `propertyHtmlAdmin`'s own markup. What is left is the handful of
   things a squad has and a player does not — a crest in a table cell, an online count that is two
   readings, a rank of four words, and a log line that has to name WHO it is about.

   Nothing here declares a width over 320px and nothing hides a control behind `!important` in a
   touch query: both are the defects `check-mobile` exists for, and the table already sits in a
   `.table-wrap` that scrolls sideways on purpose. */

/* The crest column. A fixed 1.8rem so 20 rows of crests share one left edge with each other AND
   with the name beside them — the mask is square and must not be stretched, which is why both
   halves are pinned rather than one. Same reasoning as `.pd-crest` above it. */
.squad-table th.sq-crest-h, .squad-table td.sq-crest-cell { width: 1.8rem; text-align: center; }
.sq-crest-sm { width: 1.35rem; height: 1.35rem; border-radius: 2px; object-fit: contain; vertical-align: middle; }
.squad-table td.sq-crest-cell .ico { width: 1.1rem; height: 1.1rem; color: var(--faint); }
.squad-table td.c-sq-name { font-weight: 600; color: var(--text); }
.squad-table td.c-sq-id { font-size: 0.85rem; color: var(--muted); }
/* ⚠ **ELEVEN COLUMNS AND NOT ONE NARROW RULE.** Crest, squad, id, boss, members, online, score,
   kills, deaths, fame and last seen, all of them still there at 320px — so the SQUAD NAME, which is
   the only reason anybody reads this table, rendered in 61px with 59% of it on screen, and three of
   the captions above it were cut as well.
   Columns are DROPPED rather than squeezed, which is the order Players and the audit trail already
   use and the reason it is safe here too: every value that goes lives on the squad's own card, and
   the row opens it. Least useful first, and the name and the two live figures are what survive. */
@media (max-width: 1100px) {
  .squad-table th.c-sq-fame, .squad-table td.c-sq-fame,
  .squad-table th.c-sq-deaths, .squad-table td.c-sq-deaths { display: none; }
}
@media (max-width: 900px) {
  .squad-table th.c-sq-kills, .squad-table td.c-sq-kills,
  .squad-table th.c-sq-id, .squad-table td.c-sq-id { display: none; }
}
@media (max-width: 720px) {
  .squad-table th.c-sq-score, .squad-table td.c-sq-score,
  .squad-table th.c-sq-seen, .squad-table td.c-sq-seen { display: none; }
}
@media (max-width: 560px) {
  .squad-table th.c-sq-boss, .squad-table td.c-sq-boss { display: none; }
  /* With room to spare the name stops eliding and wraps instead: a squad can be called anything,
     including one unbroken word, which is what `overflow-wrap` is here for. */
  .squad-table td.c-sq-name { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere; }
}

/* The online count. Two readings meet in this cell — the save's inference and the running game's
   answer — and the ◉ beside it is the map's own `.mp-live`, deliberately, so "this is live" looks
   the same wherever this panel says it. */
.sq-on { font-weight: 700; }
.sq-on.on { color: var(--online); }
.sq-on.off { color: var(--muted); }
/* Somebody in this squad is being shot at right now. It is the one thing on this screen worth
   interrupting a scan for, so it is the alarm colour and nothing else is. */
.sq-danger { color: var(--alarm); font-weight: 700; }

/* The chip above the table: which half answered. `.roster-intel-chip` already draws the box. */
.sq-chip-live { color: var(--online); }
.sq-chip-saved { color: var(--muted); }

/* ── the squad card ─────────────────────────────────────────────────────────────────────────── */
/* The hero's crest stands where a player's avatar does and at the same size, so the two cards line
   up when they are opened one after the other. */
.sq-hero-crest { flex: none; width: 3.2rem; height: 3.2rem; display: flex; align-items: center; justify-content: center; }
.sq-hero-crest .pd-crest-lg { width: 3.2rem; height: 3.2rem; }
.sq-hero-crest .ico { width: 2.2rem; height: 2.2rem; color: var(--faint); }

/* Members. Wider rows than `.pd-members` because each carries three numbers as well as a name —
   `min()` rather than a bare length, which is the rule `check-mobile` enforces: a bare 260px inside
   a card that hides its overflow clips the right-hand edge of every row on a phone. */
.sq-members { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 0.4rem; margin-bottom: 0.6rem; }
.sq-member {
  display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left;
  padding: 0.45rem 0.6rem; background: var(--inset); border: 1px solid var(--line);
  border-radius: var(--radius); color: var(--text); font-family: var(--ui);
  --hov-bg: var(--hover-bg); --hov-fg: var(--text); --hov-line: var(--line-amber);
}
.sq-m-main { display: flex; flex-direction: column; min-width: 0; gap: 0.1rem; }
/* A sentence with a separator in it — `down or dead · last seen Jul 5, 2026, 6:28 PM` — so it
   wraps at every width rather than only on a phone. The cards are laid out at a 260px minimum, so
   between a phone and a desktop there is a whole band where this was cut to 40% of itself and the
   media query below did not reach. A rule that is right on one side of a breakpoint and wrong on
   the other is a rule aimed at the breakpoint instead of at the text. */
.sq-m-state { font-size: 0.68rem; color: var(--muted); white-space: normal; overflow-wrap: anywhere; }
.sq-m-nums {
  margin-left: auto; display: flex; flex-direction: column; align-items: flex-end;
  font-family: var(--mono); font-size: 0.68rem; color: var(--muted); line-height: 1.35; white-space: nowrap;
}
/* ⚠ **A `min-width: 0` BESIDE A `nowrap` MEANS THE NAME PAYS FOR THE NUMBERS.** `.sq-m-main` is the
   only thing on this row that can give, and `.sq-m-nums` never gives at all, so at a true 320px the
   member's NAME rendered in 5px with 12% of it on screen and the line under it in 18px with 7%. A
   card whose whole purpose is naming a squad member, naming nobody.

   On a phone the figures take their own line, laid out across it rather than stacked, and both
   texts above them are free to wrap. Nothing on the card is elided at any width. */
@media (max-width: 560px) {
  .sq-member { flex-wrap: wrap; row-gap: 0.25rem; }
  .sq-m-main { flex: 1 1 auto; min-width: 8ch; }
  .sq-m-main .pd-m-name, .sq-m-state {
    white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere;
  }
  .sq-m-nums {
    margin-left: 0; flex: 1 1 100%; flex-direction: row; flex-wrap: wrap;
    align-items: baseline; gap: 0.15rem 0.7rem;
  }
}

/* The squad's own two texts. Prose the players wrote, so it wraps rather than ellipsising — the
   whole point of showing it is reading it. */
.sq-text { margin-bottom: 0.9rem; }
.sq-text-h { font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 600; margin-bottom: 0.25rem; }
.sq-text-b { background: var(--inset); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.6rem 0.7rem; white-space: pre-wrap; overflow-wrap: anywhere; }

/* Raid protection, in the three states the reader has to tell apart at a glance. Each states its
   own colour and nothing else — there is no fill to disagree with. */
.sq-raid { font-weight: 600; }
.sq-raid-protected { color: var(--online); }
.sq-raid-scheduled { color: var(--amber-hi, var(--amber)); }
.sq-raid-raidable { color: var(--alarm); }
.sq-src-live { color: var(--online); }

/* Who a log line is about. The player card's Logs tab has no such column — every line there is
   about the one person — so this is the one real difference between the two lists. */
.sq-log-who { white-space: nowrap; }

/* ── the ways IN ────────────────────────────────────────────────────────────────────────────────
   A squad named anywhere in this panel is a link to its card, and all three spellings are the
   *ghost* shape from the design-system block: no fill, the colour change IS the affordance, both
   halves retuned on the element so they travel together. Overriding one of them in a `:hover` is
   the defect `check-contrast` exists for. */
.sq-name-link, .pd-squad-open, .rr-squad-link {
  background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer;
  --hov-bg: transparent; --hov-fg: var(--amber-hi); --hov-line: currentColor;
}
.sq-name-link, .rr-squad-link { display: inline-flex; align-items: center; gap: 0.3rem; min-width: 0; }
.rr-squad-link { font-size: 0.85rem; border-bottom: 1px dotted var(--muted); }
/* The card's Squad heading is the whole row, so it needs the row's own layout back — `.pd-squad-head`
   is a flex row and a <button> is not one by default. */
.pd-squad-open { display: flex; width: 100%; }
.pd-squad-go { margin-left: auto; color: var(--muted); }
.pd-squad-open:hover .pd-squad-go { color: var(--amber-hi); }
/* The boss's name in the card's ID grid — the same ghost, inside a `.pd-iv`. */
.sq-member-link {
  background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer;
  border-bottom: 1px dotted var(--muted);
  --hov-bg: transparent; --hov-fg: var(--amber-hi); --hov-line: currentColor;
}
