/* ============================================================================
   VOID REACH — command console theme
   Deep-space blues, glass panels, cyan primary accent. 8px spacing grid.
   Vendored Orbitron + Rajdhani (SIL OFL, public/fonts/, no CDN — same
   same-origin-only precedent as public/js/vendor/three/) for display/body
   type; everything else stays unicode glyphs + canvas starfield.
   ========================================================================== */

/* Orbitron 700 (display: HUD title, h1/h2, nav labels) + Rajdhani 400/500/
   600/700 (body: replaces system sans everywhere). Base Latin (U+0000-00FF)
   files — see README "Vendored third-party" for source + license. */
@font-face {
  font-family: "Orbitron";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/orbitron-700.woff2") format("woff2");
}
@font-face {
  font-family: "Rajdhani";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/rajdhani-400.woff2") format("woff2");
}
@font-face {
  font-family: "Rajdhani";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/rajdhani-500.woff2") format("woff2");
}
@font-face {
  font-family: "Rajdhani";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/rajdhani-600.woff2") format("woff2");
}
@font-face {
  font-family: "Rajdhani";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/rajdhani-700.woff2") format("woff2");
}
/* Rajdhani's base Latin file above is missing Latin Extended-A — which
   includes Hungarian's double-acute ő/ű (very common letters: "előtt",
   "győzelem"). Same font-family+weight, second @font-face restricted to
   the extended range — the browser picks whichever file actually has
   the glyph it needs, per character, with no visual seam for players
   who only ever hit the base range. */
@font-face {
  font-family: "Rajdhani";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/rajdhani-400-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: "Rajdhani";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/rajdhani-500-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: "Rajdhani";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/rajdhani-600-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: "Rajdhani";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/rajdhani-700-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;
}
/* Orbitron ships ONLY the base-Latin subset on Google Fonts — no
   Cyrillic, no Latin Extended-A at all (unlike Rajdhani above, there is
   no "-ext" file to add). So headings using --font-display need a full
   fallback for both Russian/Ukrainian text AND Hungarian's ő/ű. Exo 2
   (SIL OFL, same vendoring precedent) covers both in one fallback
   family, keeping every non-base-Latin glyph in ONE consistent style
   instead of spilling into the raw system sans per character. */
@font-face {
  font-family: "Exo 2 Fallback";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/fonts/exo2-cyrillic-600.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "Exo 2 Fallback";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/fonts/exo2-latinext-600.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;
}

:root {
  --bg0: #05070f;
  --bg1: #090e1c;
  --bg2: #0d1526;
  --bg3: #121d33;

  --panel: rgba(13, 21, 38, 0.78);
  --panel-solid: #0d1526;
  /* owner: "még közel sem úgy néz ki mint a referencia oldal" — every
     position/size in the Galaxy rebuild was matching exactly, but the
     whole game still read as visibly flatter than the reference because
     of THIS token, not any one component: `galaxy-map-concept.css`'s
     `--border`/`--border-strong` (used on nearly every bordered element
     there — panels, sidenav, chips, tabs) are both lighter-hued AND far
     more opaque (0.34/0.5) than these were (0.14/0.35) — a ~2.4x
     visibility difference on the single most-used border color in the
     entire game, compounding across every panel/chip/table border into a
     genuinely different overall "weight". Matched to the reference's
     exact values. HIGH BLAST RADIUS — this token is used game-wide, not
     just on the Galaxy page, so this pass intentionally re-verified
     multiple other pages (not just Galaxy) for regressions. */
  --panel-border: rgba(130, 195, 240, 0.34);
  --panel-border-hi: rgba(79, 216, 255, 0.5);

  --txt: #cfe2f4;
  --txt-dim: #7d93ad;
  --txt-faint: #4b5f78;

  --accent: #4fd8ff;
  --accent-soft: rgba(79, 216, 255, 0.12);
  --accent2: #7a9fff;
  --ok: #3ddc97;
  --warn: #ffb02e;
  --danger: #ff4d5d;
  --danger-soft: rgba(255, 77, 93, 0.12);

  --fc-combine: #4fd8ff;
  --fc-traders: #ffd166;
  --fc-hegemony: #ff5d6c;
  --fc-vayu: #69e08a;

  --mono: ui-monospace, "Cascadia Mono", Consolas, "Courier New", monospace;
  --sans: "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-display: "Orbitron", "Exo 2 Fallback", var(--sans);
  --font-body: "Rajdhani", "Exo 2 Fallback", var(--sans);

  --sp: 8px;
  --radius: 6px;
  --radius-lg: 10px;
  --hud-h: 60px;
  /* B6 (UI audit — global activity strip): 0 by default (strip absent), set
     to the strip's real height by `body.has-activity` (main.js toggles that
     class off activitystrip.js's own visibility decision) whenever there is
     at least one pending build/train/fleet item to show. Every fixed-offset
     rule that sits below the HUD (incoming banner, #shell, #toasts) folds
     this into its own `calc()` so the strip both reserves real layout space
     when present and leaves a clean zero-height gap when it isn't. */
  --activity-h: 0px;
  --nav-w: 196px;
  /* #chat-dock's fixed height + its own bottom offset (321px + 11px, both
     literal galaxy-map-concept.css numbers) — #page reserves this much
     extra bottom padding so the (now permanently open, no more collapsed
     state — owner: "a chat sem felnyitható ott, hanem statikus") dock
     never covers the tail end of a page's content (A1's original bug:
     it was overlapping the Production & Stocks table's bottom rows, back
     when the dock was only 38px tall collapsed — the reserve just has to
     scale with the dock's real height now that it's always this tall). */
  --chat-dock-h: 332px;

  --shadow-panel: 0 8px 24px rgba(2, 4, 10, 0.5);
  --shadow-glow: 0 0 18px rgba(79, 216, 255, 0.22);

  /* v0.44.0 "HOLO GUI" kit (docs/PHASE6-UI-CHROME.md §5i): the diagonal
     corner-cut size shared by panels/headers/buttons, so the whole kit's
     chamfer reads as one consistent unit. */
  --chamfer: 14px;
  --chamfer-sm: 9px;
}

/* ------------------------------------------------------------------ reset */

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body { height: 100%; }

/* Console-themed scrollbars (default OS scrollbars are another "this is a
   web page" tell) — Firefox via scrollbar-color, everything else via the
   WebKit pseudo-elements. Kept thin/understated, not another glow effect. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--panel-border-hi) transparent;
}
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--panel-border-hi);
  border-radius: 6px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--accent); background-clip: padding-box; }

/* Settings > Display > UI scale (public/js/ui/prefs.js). `zoom` (not
   transform:scale) so the change triggers a real layout reflow — panels and
   cards genuinely resize and re-wrap through the existing responsive
   breakpoints instead of just repainting at a different visual size. Default
   'comfortable' has no attribute and is a pure no-op. */
html[data-ui-scale="compact"] { zoom: 90%; }
html[data-ui-scale="large"]   { zoom: 115%; }

body {
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.45;
  color: var(--txt);
  /* owner: "másik nézetben (colony, shipyard, overview stb.) is látnom
     kellene a nebula ködöt és a kiterjesztett hátteret, ahogy a Galaxy
     nézetben is" — the Galaxy/System/Planet tiers' own nebula backdrop
     (`/img/space/nebula_backdrop.jpg`, same file `.map-stage::before`
     uses) is now the SHARED app-wide key art here too, replacing the old
     planetary-horizon photo every non-map page showed instead. Same dark
     scrim gradients as before for panel-text readability — that part of
     the original rationale (owner: "don't want it to feel like a browser
     game" — a photo/art background instead of a flat procedural
     gradient) still holds, only the specific image changed. `center /
     cover` matches `.map-stage::before`'s own positioning exactly, so
     the SAME image reads consistently whether you're looking at it
     through the Galaxy page's dedicated layer or this shared one.
     (Briefly dropped during the unified-shell pass, then reverted —
     owner: "az helyes volt, a Galaxis nézet a túl világos" — this shared
     background was correct all along; the actual brightness complaint is
     the Galaxy Map's OWN additional nebula layer stacking on top of it,
     not this one.) */
  background:
    radial-gradient(1200px 700px at 75% -10%, rgba(37, 66, 128, 0.22), transparent 60%),
    radial-gradient(900px 600px at -10% 110%, rgba(64, 33, 94, 0.16), transparent 60%),
    linear-gradient(180deg, rgba(5, 7, 15, 0.45) 0%, rgba(5, 7, 15, 0.7) 55%, var(--bg0) 95%),
    url("/img/space/nebula_backdrop.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
  overflow: hidden;
}

/* Premium HUD Phase 3: the login screen — the app's actual first impression,
   before anyone has a colony to look at — gets its own dedicated key art
   (a spiral galaxy + planet, generated for exactly this purpose but never
   wired in) instead of the shared horizon photo every in-game screen uses.
   Same scrim technique as the base `body` rule above, just swapping the
   image and re-centering on the galaxy core rather than a horizon line.
   `main.js`'s route() toggles this class — only while #/login itself is
   mounted, not the whole public/unauthed route group. */
body.login-screen {
  background:
    radial-gradient(1200px 700px at 75% -10%, rgba(37, 66, 128, 0.22), transparent 60%),
    radial-gradient(900px 600px at -10% 110%, rgba(64, 33, 94, 0.16), transparent 60%),
    linear-gradient(180deg, rgba(5, 7, 15, 0.35) 0%, rgba(5, 7, 15, 0.65) 55%, var(--bg0) 95%),
    url("/img/keyart-login.png");
  background-position: center 40%;
}
/* `.login-wrap` was only horizontally centered (`margin: 0 auto`) with a
   fixed `padding-top: 5vh` standing in for vertical position — not truly
   centered, and the gap above/below the card varied with viewport height.
   `#page` is a shared scroll container reused by every route (flex:1,
   overflow-y:auto), so this is scoped to `body.login-screen` rather than
   changing `#page` globally. `(1,1,1)` specificity beats the plain
   `#page`/`@media #page` rules above regardless of source order, so no
   `!important` needed. The two-class fix already hides `.map-side-dash`/
   `#chat-dock` on this route (see hud-2026.css), but `#page`'s own
   padding reserves for them still applied unconditionally and would have
   skewed the centering — zeroed here to match. `overflow-y: auto` is kept
   so a very short viewport still scrolls instead of clipping the card. */
body.login-screen #page {
  display: flex; align-items: center; justify-content: center;
  padding-right: calc(var(--sp) * 3); padding-bottom: calc(var(--sp) * 3);
}
body.login-screen .login-wrap { padding-top: 0; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-shadow: 0 0 8px rgba(79, 216, 255, 0.55); }

::selection { background: rgba(79, 216, 255, 0.28); }

/* scrollbars */
* { scrollbar-width: thin; scrollbar-color: rgba(79, 216, 255, 0.25) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: rgba(79, 216, 255, 0.2); border-radius: 4px; }
*::-webkit-scrollbar-track { background: transparent; }

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.dim { color: var(--txt-dim); }
.r { text-align: right; }
.hidden { display: none !important; }
.pad-8 { padding: var(--sp); }
.ok-text { color: var(--ok); }
.warn-text { color: var(--warn); }
.danger-text { color: var(--danger); }
.accent-link { color: var(--warn); font-weight: 600; }

.noscript-block {
  position: fixed; inset: 40% 20%; text-align: center;
  color: var(--warn); font-size: 20px; letter-spacing: 0.2em;
}

/* ------------------------------------------------------------ boot splash */

/* Full-screen intro shown from the static HTML (paints before any JS) so the
   app resolves from a designed reveal instead of a blank flash. main.js's
   boot() adds .boot-done to fade it out, then removes the node. z-index above
   the ambient scanline layer (5001) so nothing bleeds over it. */
#boot-splash {
  position: fixed; inset: 0; z-index: 6000;
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(600px 400px at 50% 42%, rgba(79, 216, 255, 0.08), transparent 70%),
    var(--bg0);
  transition: opacity 0.5s ease;
}
#boot-splash.boot-done { opacity: 0; pointer-events: none; }
.boot-inner {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  animation: boot-rise 0.7s ease-out both;
}
.boot-logo {
  font-size: 46px; color: var(--accent);
  text-shadow: 0 0 24px rgba(79, 216, 255, 0.8);
  animation: pulse-glow 3s ease-in-out infinite;
}
.boot-title {
  font-family: var(--font-display); font-weight: 700;
  font-size: 28px; letter-spacing: 0.32em; color: var(--txt-dim);
}
.boot-title b { color: var(--txt); font-weight: 700; }
.boot-sub {
  font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.4em;
  text-transform: uppercase; color: var(--txt-faint);
}
.boot-bar {
  width: 200px; max-width: 60vw; height: 2px; margin-top: 6px;
  background: rgba(79, 216, 255, 0.12); overflow: hidden; border-radius: 2px;
}
.boot-bar i {
  display: block; height: 100%; width: 40%;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: boot-scan 1.1s ease-in-out infinite;
}
@keyframes boot-rise {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes boot-scan {
  0% { transform: translateX(-120%); }
  100% { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
  .boot-inner { animation: none; }
  .boot-bar i { animation: none; width: 100%; }
}

/* -------------------------------------------------------------- starfield */

#starfield {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
}

/* Ambient console vignette — ties the whole viewport into the "holographic
   console" identity instead of just individual panels. Sits above every
   overlay (z-index 5000) but is near-invisible by design, so it never
   fights readability. (A companion scanline-sweep layer, body::before,
   shipped alongside this in v0.36.0 and was removed in v0.42.0 — the
   owner found the continuous moving light band distracting.) */
body::after {
  content: ""; position: fixed; inset: 0; z-index: 5000; pointer-events: none;
  /* Pushed out to 80% (was 55%): at 55% the darkening zone reached deep into
     the main content column, sitting on top of and visibly muting any panel
     chrome near its own edges — caught live: the v0.38.0 corner-bracket
     frame's glow read "faded" on a near-full-width panel because this
     vignette was dimming it from above (position:fixed, z-index 5000, so it
     paints over #page regardless of the panel's own stacking). Now only the
     true screen corners/margins darken; opacity trimmed too for headroom. */
  background: radial-gradient(ellipse at center, transparent 80%, rgba(2, 3, 8, 0.35) 100%);
}
/* ------------------------------------------------------------------- HUD */

#hud {
  position: fixed; top: 0; left: 0; right: 0; height: var(--hud-h); z-index: 40;
  display: flex; align-items: center; gap: calc(var(--sp) * 2);
  padding: 0 calc(var(--sp) * 2);
  /* v0.57.4 briefly retuned this to match .panel-framed's teal #0a1e2a base
     instead — owner preferred the ORIGINAL deeper blue ("jobban beleolvadt
     az univerzum témába" — blends better into the universe theme) and asked
     for the reverse: keep the new panel-framed chamfer shape/layout, but
     recolor `--panel-fill` itself (below) to this deep blue instead. HUD is
     back to its original `--bg1`-family navy; now the two are unified on
     THIS color rather than the teal one. */
  background-color: rgba(9, 14, 28, 0.94);
  background-image:
    linear-gradient(180deg, rgba(9, 14, 28, 0.96), rgba(9, 14, 28, 0.88)),
    url("/img/panels/panel-topbar-strip.png");
  /* The strip art's own top "tab" band (hex micro-texture + thin cyan
     underline, ~top fifth of the 1344x768 source) is the part that fits
     a 60px-tall bar — cropped in via an oversized background-size +
     top-anchored position rather than stretching the whole frame body. */
  background-size: 100% auto, 100% 320px;
  background-position: center, top;
  background-repeat: no-repeat, no-repeat;
  border-bottom: 1px solid var(--panel-border);
  backdrop-filter: blur(6px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
}

body.unauthed #hud,
body.unauthed #sidenav,
body.unauthed #chat-dock,
body.unauthed #incoming-banner,
body.unauthed .map-action-dock,
body.unauthed .map-side-dash { display: none; }

.hud-brand { display: flex; align-items: center; gap: var(--sp); white-space: nowrap; }
.hud-logo {
  color: var(--accent); font-size: 22px;
  text-shadow: 0 0 12px rgba(79, 216, 255, 0.8);
  animation: pulse-glow 4s ease-in-out infinite;
}
@keyframes pulse-glow {
  0%, 100% { text-shadow: 0 0 6px rgba(79, 216, 255, 0.5); }
  50% { text-shadow: 0 0 16px rgba(79, 216, 255, 0.95); }
}
.hud-title {
  font-family: var(--font-display); font-size: 15px; letter-spacing: 0.30em;
  color: var(--txt-dim); font-weight: 700;
}
.hud-title b { color: var(--txt); font-weight: 700; }

.hud-label {
  display: block; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--txt-faint);
}

/* Owner: the "COLONY" label and the dropdown below it read as glued
   together, and the group sat low/left in the HUD row. `.hud-colony` was
   never given its own layout rule before — it just fell back to default
   block-stacking, no gap, no centering. A real flex column fixes both asks
   at once: `gap` separates label from button, `align-items: center`
   centers the (short, all-caps) label over the wider button instead of
   pinning it flush-left, and centering the group vertically as a genuine
   flex column (rather than two independently-flowing blocks) is what
   reads as "higher" — it's no longer bottom-weighted by the label's own
   line-box. */
/* owner (pixel-exact rebuild): was a flex COLUMN (a stacked "COLONY" label
   above the dropdown) — galaxy-map-concept.css's `.colony-pill` is a
   single-line pill, no label. The wrapper is now just a passthrough. */
.hud-colony { display: flex; align-items: center; }

/* Custom colony-switcher dropdown (main.js) — replaces a native <select>,
   one of the clearest "this is a web page" tells. Reskinned to match
   `.colony-pill` exactly: full pill radius, faint white fill instead of
   an accent-tinted gradient, an amber star icon ahead of the name (was a
   plain rounded-rect button with no icon at all). */
.hud-select { position: relative; max-width: 220px; }
.hud-select-btn {
  display: flex; align-items: center; gap: 8px;
  width: 100%; max-width: 220px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--panel-border-hi);
  border-radius: 999px;
  color: var(--txt);
  font-family: var(--font-body); font-size: 14.5px; font-weight: 600;
  padding: 7px 12px 7px 10px;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.colony-star-ic { color: var(--fc-traders); flex: none; font-size: 14px; }
.hud-select-btn > span:nth-child(2) {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hud-select-caret { margin-left: auto; }
.hud-select-btn:hover { border-color: var(--accent); }
.hud-select-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hud-select-caret { color: var(--accent); font-size: 12px; flex: none; transition: transform 0.15s; }
.hud-select-btn[aria-expanded="true"] .hud-select-caret { transform: rotate(180deg); }
/* Owner (UI kit reference sheet, "7. CONTEXT MENU & DROPDOWN"): a chamfered
   corner + glow border reads as the same HOLO GUI dropdown as the
   reference instead of a plain rounded browser-ish menu. Single-layer
   clip-path (not the full `.panel-framed` double-layer technique) is
   enough here since this already has a real opaque `--panel-solid`
   background — no accent/fill split needed for a plain border+glow. */
.hud-select-list {
  position: absolute; top: calc(100% + 4px); left: 0; min-width: 100%; width: max-content; max-width: 320px;
  list-style: none;
  background: var(--panel-solid);
  border: 1px solid var(--panel-border-hi);
  border-radius: 0;
  clip-path: polygon(0 0, calc(100% - var(--chamfer-sm)) 0, 100% var(--chamfer-sm), 100% 100%, 0 100%);
  box-shadow: var(--shadow-panel), 0 0 16px rgba(79, 216, 255, 0.1);
  padding: 4px;
  z-index: 45;
  max-height: 60vh; overflow-y: auto;
}
.hud-select-opt {
  padding: 6px 8px;
  clip-path: polygon(0 0, calc(100% - 6px) 0, 100% 6px, 100% 100%, 0 100%);
  font-size: 15px; color: var(--txt-dim);
  cursor: pointer; white-space: nowrap;
  transition: background 0.1s, color 0.1s;
}
.hud-select-opt:hover { background: var(--accent-soft); color: var(--txt); }
.hud-select-opt.active { color: var(--accent); font-weight: 600; box-shadow: inset 2px 0 0 var(--accent); }
.hud-select-opt.needs-attention::after {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--warn); margin-left: 6px; vertical-align: middle;
}

select, input[type="text"], input[type="password"], input[type="number"] {
  /* HOLO GUI kit (§5i): reference inputs are plain bordered rects (no
     chamfer — that accent is reserved for panels/buttons/headers, an input
     is dense enough already), just a brighter, more deliberate cyan-tinted
     border than the old barely-there panel-border grey. */
  background: rgba(4, 10, 16, 0.75);
  border: 1px solid rgba(79, 216, 255, 0.3);
  border-radius: var(--radius);
  color: var(--txt);
  padding: 6px 9px;
  font-family: var(--font-body);
  font-size: 16px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
/* docs/art/ref-GUI/context_dropdown.png ("7. CONTEXT MENU & DROPDOWN")
   shows every dropdown with a small cyan chevron, but every native <select>
   in the game (fleet dispatch mission/target pickers, market offer forms,
   colony building/unit pickers, alliance/season/quest forms — every page
   still has several) was still rendering the browser's own OS arrow glyph,
   the one clearly-unstyled control left after §5i's input/checkbox pass.
   `appearance:none` + a hand-drawn SVG chevron (accent-colored, matches
   `.hud-select-caret`'s ▾ elsewhere) closes that gap in one shared rule
   instead of touching every individual page. */
select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 28px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%234fd8ff' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 11px 7px;
  cursor: pointer;
}
select:disabled { cursor: not-allowed; }
select:focus, input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(79, 216, 255, 0.18);
}
select option { background: var(--bg2); color: var(--txt); }
/* v0.57.8: pin placeholder contrast explicitly — browsers otherwise apply
   their own default opacity (Firefox ~54%) on top of --txt, which reads
   noticeably dimmer/darker than every other label in the HUD. */
input::placeholder { color: var(--txt-dim); opacity: 1; }

/* HOLO GUI kit (§5i): the reference kit's dark octagon checkbox / ringed
   radio with a bright cyan fill when checked — native controls render as
   plain OS widgets today, one of the clearest "this is a webpage" tells.
   `appearance:none` clears the OS chrome so the border/background rules
   above (and this rule's own clip-path/radius) fully take over; ::after
   paints the checked-state fill only (empty otherwise), keeping unchecked
   controls visually quiet. */
input[type="checkbox"], input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  width: 17px; height: 17px;
  padding: 0;
  position: relative;
  vertical-align: middle;
  cursor: pointer;
  flex: none;
}
input[type="checkbox"] {
  clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 4px 100%, 0 calc(100% - 4px));
}
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"]:checked, input[type="radio"]:checked {
  border-color: var(--accent);
  box-shadow: 0 0 6px rgba(79, 216, 255, 0.55);
}
input[type="checkbox"]:checked::after, input[type="radio"]:checked::after {
  content: '';
  position: absolute; inset: 4px;
  background: var(--accent);
  box-shadow: 0 0 4px rgba(79, 216, 255, 0.8);
}
input[type="radio"]:checked::after { border-radius: 50%; }

/* docs/art/ref-GUI/slider.png ("13. SLIDERS & INPUT")'s pill-shaped ON
   toggle, distinct from the square checkbox above — settings.js's
   toggleRow()/audioChannelRow() checkboxes are genuine on/off SWITCHES
   (3D models, fleet arcs, per-notification-kind mute, channel mute...),
   not a multi-select CHECKBOX, so they get this `.toggle-switch` class
   instead of the checkmark-square treatment. Same `<input
   type="checkbox">` markup — pure CSS reskin (`appearance:none` already
   applies from the shared rule above; this just overrides shape/thumb via
   a more specific selector), zero JS logic changes. */
input[type="checkbox"].toggle-switch {
  clip-path: none;
  width: 38px; height: 20px;
  border-radius: 999px;
  background: rgba(20, 28, 45, 0.8);
  border: 1px solid var(--panel-border-hi);
  transition: background 0.15s, border-color 0.15s;
}
input[type="checkbox"].toggle-switch::after {
  content: '';
  position: absolute; inset: auto; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--txt-dim);
  box-shadow: none;
  transition: left 0.15s, background 0.15s, box-shadow 0.15s;
}
input[type="checkbox"].toggle-switch:checked {
  background: rgba(79, 216, 255, 0.28);
  border-color: var(--accent);
  box-shadow: none;
}
input[type="checkbox"].toggle-switch:checked::after {
  left: 20px;
  background: var(--accent);
  box-shadow: 0 0 6px rgba(79, 216, 255, 0.7);
}

/* HUD resources */
/* Owner reported the SAME raster-frame stretch bug 3 TIMES running (baked-
   content strip, then width/height SVG attrs + background-size, then a
   real <img> + object-fit — each verified "fixed" in this session's own
   test tab, each still broken live) — rebuilt from scratch on a raster-
   free design per the owner's own "ha kell a nulláról készítsd el" ("build
   it from scratch if needed"). No image asset of any kind now: the outer
   frame is the SAME double-layer clip-path chamfer technique every OTHER
   framed element in this game already uses reliably (`.panel-framed`,
   `.ov-box`, `.map-side-card`'s siblings, etc.) — CSS clip-path scales to
   any box size with zero stretch ambiguity, unlike a background-image or
   an <img>'s object-fit, so this class of bug is structurally impossible
   here. `.hud-res` no longer needs a separate frame wrapper — the chamfer
   lives directly on it.
   Round 2 (owner: "make it similar to the command dock style and color...
   the frame is too wide... the background changed to greyish, originally
   dark blue"): two real issues. (1) `--panel-accent`/`--panel-fill` were
   my own guessed hex colors lifted from the reference SVG (#00E0FF cyan,
   #101a26/#0a0f16 navy) instead of this game's OWN established design
   tokens — every other chamfered panel in the game (including the command-
   dock's own aesthetic) reads as "dark blue" specifically because it's
   built from `--accent`/`--bg1`/`--bg2` via `color-mix()`, not arbitrary
   hex; the mismatch is exactly what read as "greyish" next to everything
   else. Switched to the SAME formula `.panel-framed` itself uses (copied
   verbatim below), so this now matches the game's one true "premium HUD"
   palette instead of a second, slightly-off one. (2) `flex:1` let this
   grow to fill 100% of the space between the colony switcher and the
   right-side stats, MUCH wider than its 4-5 chips need — capped with
   `max-width` instead; `.hud-right` gets its own `margin-left:auto` (was
   relying entirely on this element's flex-grow to land at the true right
   edge) so the two are no longer coupled. */
/* v0.60.27 (owner: "upgrade the resource indicator to look like the 1st
   image" — a screenshot of this game's OWN `.ov-stat-bar`, Overview's
   Points/Rank/Colonies/etc bar). Dropped the octagon chamfer + double-layer
   fill panel (the v0.60.9-era build below, kept only in history) entirely
   in favor of copying `.ov-stat-bar`'s own flat, transparent, glowing-
   underline + corner-notch chrome verbatim — same family as the Fleet tab
   bar look already established there. Resource icons/values/layout
   (`.hud-res-cell` etc.) are untouched below, only this outer frame
   changed. Centering/width (`position:absolute; left/top:50%`, the
   580px/`max-width` cap) is unrelated to the frame style and stays as-is
   from the earlier "align it to the middle" round. */
.hud-res {
  display: flex; gap: 0; justify-content: center;
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  /* Owner: "a számok kilógnak a panelből, nagyítsd meg, mert több
     számjegy is lesz majd" (the numbers stick out of the panel, enlarge
     it, more digits are coming) — real overflow, not just a tight look:
     this bar had a FIXED (not just min) 580px width, so its 4 cells were
     flex-shrunk down to their 122px min-width floor regardless of actual
     content width, and rate text like "+38882/h" visually bled past each
     cell's own box. Widened bar + cell floor together (below) so cells
     actually have the room their own min-width promises, with slack left
     for more digits as production numbers grow further.
     Round 2 (owner: "szélesítsd ki az egyes elemeket, mert kilógnak a
     karakterek a végén") — widening the FIXED width alone wasn't enough:
     `.hud-res-main` still had `min-width:0` (removed above), letting cells
     shrink back below content size whenever the fixed 660px total wasn't
     quite enough for a given account's actual numbers. Switched `width`
     to `min-width` here too — the bar now grows to fit its content's real
     needs (still centered, still capped by max-width on narrow viewports)
     instead of clipping back down to one fixed budget. */
  min-width: 660px; max-width: calc(100vw - 600px);
  min-height: 44px;
  /* Owner (again): "align the edges to the corners, there is a space
     between the red area and the frame's line" — same bug class as the
     earlier "frame's line should touch the red background" fix, reborn
     because this rebuild introduced fresh padding on .hud-res itself. The
     "red area" is .res-danger's inset glow, which is bounded by the CELL
     box — any padding here holds the cells (and their glow) back from the
     frame edges. padding:0 lets .hud-res-cell's own padding (5px 14px)
     handle internal breathing room while the cells themselves reach the
     frame's border-bottom/corner ticks exactly, like before. */
  padding: 0;
  border-bottom: 1px solid var(--panel-border-hi);
  box-shadow: 0 1px 6px rgba(79, 216, 255, 0.15);
}
.hud-res::before, .hud-res::after {
  content: ''; position: absolute; bottom: -1px; width: 10px; height: 6px;
  pointer-events: none;
}
.hud-res::before { left: 0; border-bottom: 1px solid var(--accent); border-left: 1px solid var(--accent); }
.hud-res::after { right: 0; border-bottom: 1px solid var(--accent); border-right: 1px solid var(--accent); }
.hud-res-cell {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  padding: 5px 14px; min-width: 150px;
  transition: box-shadow 0.2s;
}
.hud-res-cell:not(:first-child) { border-left: 1px solid #1F2A3A; }
.hud-res-cell.res-danger {
  box-shadow: 0 0 10px rgba(255, 77, 93, 0.25) inset;
}
.hud-res-cell.res-danger .hud-res-val { color: var(--danger); }
/* Top-bar capacity tiering (brief §3): green <70% (default), yellow 70-90%,
   red >90% or actually full / net-negative rations (unchanged .res-danger). */
.hud-res-cell.res-warn {
  box-shadow: 0 0 10px rgba(255, 176, 46, 0.18) inset;
}
.hud-res-cell.res-warn .hud-res-val { color: var(--warn); }
/* Owner: "the resource icon... now it has like an extra blue/gray layer on
   it?!" — this used to nest its OWN mini chamfered chip (a second
   --panel-accent/--panel-fill double-layer) inside each `.hud-res-cell`,
   which itself used to be individually chamfered too — a deliberate
   "chip within a chip" look. Now that the outer per-cell chamfer is gone
   (one shared frame around the whole bar instead, see `.hud-res`), a lone
   surviving mini-frame around just the icon reads as an orphaned extra
   layer rather than a nested one. Simplified to a bare flex container —
   just centers the icon image, no background/border of its own. */
.res-icon-box {
  width: 30px; height: 30px; flex: none;
  display: flex; align-items: center; justify-content: center;
}
.res-icon-box .ic-img { height: 20px; max-width: 20px; }
/* Owner: "szélesítsd ki az egyes elemeket, mert kilógnak a karakterek a
   végén" (widen the individual elements, the characters stick out at the
   end) — real bug my earlier verification missed: `min-width:0` lets
   this flex child shrink BELOW its content's natural width whenever the
   fixed-width `.hud-res` bar doesn't have enough room; `.hud-res-val-row`
   holds value+rate SIDE BY SIDE (not stacked), so once shrunk, that
   row's own text visually overflows the shrunk box while the box itself
   still measures as "not overflowing its cell" — exactly why my earlier
   `.hud-res-main`-bounding-rect check gave a false negative. Dropping
   `min-width:0` (default is `auto`, which refuses to shrink below
   content) fixes the actual glyph overflow; `.hud-res`'s own `width`
   switched from fixed to `min-width` (below) so the bar itself grows to
   fit instead of clipping back down to a fixed budget. */
.hud-res-main { display: flex; flex-direction: column; flex: 1; gap: 2px; }
.hud-res-val-row { display: flex; align-items: baseline; gap: 7px; }
.hud-res-val { font-size: 16px; font-weight: 700; line-height: 1; }
.hud-res-rate { font-size: 11.5px; }

.res-ic { font-style: normal; }
.res-alloys .res-ic, .res-ic.res-alloys { color: #9fb6d8; }
.res-crystal .res-ic, .res-ic.res-crystal { color: #6fd3ff; }
.res-isotopes .res-ic, .res-ic.res-isotopes { color: #c58bff; }
.res-rations .res-ic, .res-ic.res-rations { color: #69e08a; }

/* Authored PNG icon art (components.js iconImg) — occupies the same inline
   footprint as the glyph it replaces; the glyph fallback shows if the img errors. */
.ic { display: inline-flex; align-items: center; justify-content: center; line-height: 1; vertical-align: -0.2em; }
.ic-img { height: 1.35em; width: auto; max-width: 1.8em; object-fit: contain; display: block; }
.ic-fb { font-style: normal; }
.unit-ic .ic-img, .q-ic .ic-img { height: 1.55em; max-width: 2em; }
.faction-glyph .ic-img, .faction-tag .ic-img { height: 1.3em; max-width: 1.7em; }
.faction-glyph-lg .ic-img { height: 2.4em; max-width: 3em; }

.bar {
  height: 3px; background: rgba(79, 216, 255, 0.12);
  border-radius: 2px; overflow: hidden; margin-top: 3px;
}
.bar-fill {
  height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--accent2), var(--accent));
  border-radius: 2px;
  transition: width 0.4s linear;
}
.bar-fill.bar-danger, .bar.bar-danger .bar-fill { background: var(--danger); }
.bar-fill.bar-warn, .bar.bar-warn .bar-fill { background: var(--warn); }
.hud-bar { width: 100%; }

.hud-right { display: flex; align-items: center; gap: calc(var(--sp) * 2); white-space: nowrap; margin-left: auto; }
.hud-stat { text-align: right; }
.hud-stat .mono { font-size: 15px; }
.hud-user { display: flex; align-items: center; gap: 6px; font-weight: 600; }

.faction-glyph { font-style: normal; }
.fc-combine { color: var(--fc-combine); }
.fc-traders { color: var(--fc-traders); }
.fc-hegemony { color: var(--fc-hegemony); }
.fc-vayu { color: var(--fc-vayu); }

/* --------------------------------------------------------- incoming banner */

/* Owner: "alert_panel.svg ... find out where to implement" — wired onto the
   incoming-attack banner, converting it from a full-width top strip into a
   floating alert card using that generated frame (public/img/panels/
   alert-panel.svg — chamfered body, glowing corner brackets, baked pulse-
   ring icon) as a background image. The frame's own title/body/action text
   was stripped from the SVG copy since all of it is real per-attack data
   here (target, origin, live ETA countdown), not static mockup copy — see
   updateBanner() in main.js. Non-uniform `100% 100%` background-size is
   deliberate (unlike the aspect-ratio-locked console-dock): this frame has
   no per-slot hotspots that need pixel alignment, just chrome, so it can
   freely stretch to fit however tall the real (variable-length) text runs. */
#incoming-banner {
  position: fixed;
  top: calc(var(--hud-h) + var(--activity-h) + var(--sp) * 2);
  right: calc(var(--sp) * 2);
  z-index: 45;
  width: 360px; max-width: calc(100vw - var(--nav-w) - var(--sp) * 4);
  min-height: 92px;
  display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp);
  padding: 13px 16px 12px 68px;
  background: url('/img/panels/alert-panel.svg?v=2') no-repeat center / 100% 100%;
  animation: banner-throb 1.8s ease-in-out infinite;
}
@keyframes banner-throb {
  0%, 100% { filter: drop-shadow(0 0 5px rgba(255, 77, 93, 0.2)); }
  50% { filter: drop-shadow(0 0 15px rgba(255, 77, 93, 0.5)); }
}
.ib-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.ib-kicker { color: #FF4D4D; font-size: 12px; font-weight: 700; letter-spacing: 0.16em; }
.ib-title { color: var(--txt); font-size: 17px; font-weight: 700; }
.ib-desc { color: #8AA2B8; font-size: 13px; }
.banner-count { color: #fff; font-weight: 700; }
.ib-action {
  flex: none; align-self: flex-end;
  padding: 6px 14px; border: 1px solid rgba(255, 77, 93, 0.7); border-radius: 2px;
  color: #FF4D4D; font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  text-decoration: none; white-space: nowrap;
  transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.ib-action:hover { background: rgba(255, 77, 93, 0.18); color: #fff; box-shadow: 0 0 10px rgba(255, 77, 93, 0.4); }

/* ---------------------------------------------------- global activity strip
   B6 (UI audit): a thin persistent band directly under the HUD, on every
   authenticated page, listing everything the player is currently waiting on
   — active build/train jobs (per colony) and fleet movements (own outgoing/
   returning + incoming hostiles) — each a live countdown that navigates on
   click. Populated by public/js/ui/activitystrip.js; main.js toggles
   `body.has-activity` (which drives --activity-h above) whenever it has at
   least one item, so the strip fully collapses (zero height, no dead gap)
   the instant nothing is pending. Deliberately NOT a .panel-framed/.btn —
   those chamfered kit elements ignore border-color/background overrides
   (recolor only via --panel-accent/--panel-fill), which this thin strip of
   many small chips has no use for; plain rounded chips instead. */

body.has-activity { --activity-h: 32px; }

#activity-strip {
  position: fixed; top: var(--hud-h); left: 0; right: 0; height: var(--activity-h);
  z-index: 38; /* below the incoming-banner's 39 (the two never overlap: the
                  banner starts exactly where this strip's reserved band ends) */
  overflow: hidden;
  background: linear-gradient(180deg, rgba(13, 21, 38, 0.92), rgba(13, 21, 38, 0.82));
  border-bottom: 1px solid var(--panel-border);
}
#activity-strip.hidden { display: none; }
body.unauthed #activity-strip { display: none; }

.activity-strip-inner {
  height: 100%; display: flex; align-items: center; gap: 6px;
  padding: 0 var(--sp); overflow-x: auto; overflow-y: hidden;
  scrollbar-width: thin;
}

.activity-chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  padding: 3px 10px; border-radius: 999px; white-space: nowrap;
  font-size: 13px; line-height: 1.2; color: var(--txt);
  background: rgba(79, 216, 255, 0.07);
  border: 1px solid var(--panel-border);
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.activity-chip:hover { background: rgba(79, 216, 255, 0.14); border-color: var(--panel-border-hi); }
.act-ic { font-size: 14px; }
.act-extra { font-size: 12px; }
.act-eta { font-weight: 600; }

/* per-kind accents — same tokens fleet.js's own st-outbound/st-returning
   classes already use, so a "training" chip here and the Shipyard's own
   train-queue countdown read as the same color language. */
.activity-chip.act-train .act-ic { color: var(--accent2); }
.activity-chip.act-fleet .act-ic { color: var(--warn); }
.activity-chip.act-incoming {
  background: var(--danger-soft); border-color: rgba(255, 77, 93, 0.4); color: #ffb9c0;
}
.activity-chip.act-incoming .act-ic { color: var(--danger); }
.activity-chip.act-incoming .act-eta { color: var(--danger); }

/* ------------------------------------------------------------------ shell */

#shell {
  position: fixed; top: calc(var(--hud-h) + var(--activity-h)); bottom: 0; left: 0; right: 0; z-index: 10;
  display: flex;
}
body.unauthed #shell { top: 0; }

/* owner's own reference mockup (public/dev/galaxy-map-concept.html/.css,
   the actual hand-built/edited source they want followed, not just the
   PNG kit sheets) — v11 comment there: "csak ilyen nagy fix panelekben
   tudsz gondolkozni? ... miért kell leérni a bal oldali sávnak az
   aljáig?" (why does the left bar have to reach the bottom?) — its
   `.sidenav` is a natural-content-height box, NOT stretched to the full
   shell height. The live `#shell` is `display:flex` (default
   `align-items:stretch`), which was silently force-stretching `#sidenav`
   to fill the full viewport height even though nothing here ever set an
   explicit height on it. `align-self:flex-start` opts this one flex
   child out of that stretch — `#page` (the other child) keeps the
   default stretch it actually needs to scroll correctly. */
#sidenav {
  width: var(--nav-w); flex: none; align-self: flex-start;
  display: flex; flex-direction: column; gap: 2px;
  padding: calc(var(--sp) * 2) var(--sp);
  /* Reverted with #hud above — back to the original deep-blue --bg1 family;
     .panel-framed's --panel-fill is what got recolored to match THIS
     (hue only — the opacity below is a separate axis, see next comment). */
  /* owner (pixel-exact rebuild): reference `.sidenav`'s own background is
     rgba(7,10,20,.55)→rgba(7,10,20,.3) — noticeably more translucent/
     glassy than this was (.85→.65), letting the starfield bleed through
     visibly. Same hue as the deep-blue decision above, opacity matched to
     the reference's own numbers. */
  background: linear-gradient(180deg, rgba(9, 14, 28, 0.55), rgba(9, 14, 28, 0.3));
  /* Unified-shell prototype: same #2D3B55 accent as #page's own bezel/
     #chat-dock/.map-side-dash, so the sidenav reads as part of the SAME
     console housing instead of a separately-bordered strip. */
  border-right: 1px solid #2D3B55;
  border-bottom: 1px solid #2D3B55;
  border-radius: 0 0 var(--radius-lg) 0;
  backdrop-filter: blur(4px);
}

.nav-link {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: var(--radius);
  color: var(--txt-dim);
  font-size: 14px; letter-spacing: 0.14em; text-transform: uppercase;
  border: 1px solid transparent;
  transition: all 0.15s;
  position: relative;
}
.nav-link:hover {
  color: var(--txt); background: var(--accent-soft); text-shadow: none;
}
/* Owner: "a bal side bar is még a régi" — round 2. A single corner-cut on
   the active row alone read as too subtle to register as "the new GUI" at
   a glance (owner still flagged it as old afterward). Promoted the active
   state to a real bordered/glow bar (visible border-color, not just an
   inset box-shadow) and — the bigger change — gave every `.nav-ic` glyph
   its own small HUD chip (`--panel-accent`/`--panel-fill` double-layer,
   same technique as `.panel-framed`/`.map-dock-icon`) so the icon column
   reads as real HUD chrome even on inactive rows, not just plain unstyled
   glyphs. Round 3 (owner: "levágott sarkú HUD-chip, nagyon csúnya, ne
   legyen levágva") — the chamfered corner-cut (`clip-path` octagon, same
   language as the action-dock/panel-framed) read as ugly at this small
   size on both the active row's bar and the icon chip; dropped BOTH to a
   plain rounded rect (`border-radius`) instead — no cut corners anywhere
   in the sidenav now, just a soft-cornered glowing chip/bar. */
.nav-link.active {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--panel-border-hi);
  border-radius: var(--radius);
  box-shadow: inset 2px 0 0 var(--accent), 0 0 14px rgba(79, 216, 255, 0.16);
}
.nav-ic {
  --panel-accent: rgba(79, 216, 255, 0.28);
  --panel-fill: rgba(79, 216, 255, 0.05);
  position: relative; isolation: isolate;
  width: 24px; height: 24px; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; text-align: center;
  border-radius: 6px;
  transition: all 0.15s;
}
.nav-ic::before, .nav-ic::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
}
.nav-ic::before { background: var(--panel-accent); z-index: -2; }
.nav-ic::after { inset: 1px; background: var(--panel-fill); z-index: -1; }
.nav-link:hover .nav-ic {
  --panel-accent: var(--panel-border-hi);
  color: var(--accent);
  box-shadow: 0 0 10px rgba(79, 216, 255, 0.25);
}
.nav-link.active .nav-ic {
  --panel-accent: var(--accent);
  --panel-fill: color-mix(in srgb, var(--accent) 22%, var(--bg0));
  color: var(--accent);
  box-shadow: 0 0 12px rgba(79, 216, 255, 0.45);
}
@media (prefers-reduced-motion: reduce) {
  .nav-ic { transition: none; }
}

.nav-badge {
  margin-left: auto;
  min-width: 18px; padding: 1px 5px;
  background: var(--danger); color: #fff;
  font-size: 12px; font-weight: 700; text-align: center;
  border-radius: 9px;
}

/* Quest marker (v0.30) — "go here next" pointer, distinct from .nav-badge's
   unread COUNT: a single reusable dot main.js relocates onto whichever
   sidenav link the next unclaimed directive points at. Sits by the icon
   (top-left) so it never collides with a count badge on the far right. */
.nav-quest-dot {
  position: absolute; top: 4px; left: 22px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px 1px rgba(79, 216, 255, 0.85);
  animation: quest-dot-pulse 1.8s ease-in-out infinite;
  pointer-events: none;
}
@keyframes quest-dot-pulse {
  0%, 100% { transform: scale(0.85); opacity: 0.75; }
  50% { transform: scale(1.15); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .nav-quest-dot { animation: none; }
}

/* C10 (UI audit) — colony quick-switch strip at the sidenav's bottom.
   #sidenav is a flex column, so margin-top:auto pins the strip below the
   nav links without absolute positioning. Chips are small squares (not the
   HOLO chamfer — at 26px a clip-path chamfer reads as visual noise) using
   the same active/attention language as .nav-link / .hud-select-opt:
   active = accent text + inset accent edge, attention = --warn dot. */
.nav-colonies {
  margin-top: auto;
  display: flex; flex-wrap: wrap; gap: 5px;
  padding: 8px 2px 2px;
  border-top: 1px solid var(--panel-border);
}
.nav-colonies.hidden { display: none; }
.nav-colony-chip {
  position: relative;
  width: 26px; height: 26px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(9, 14, 28, 0.6);
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  color: var(--txt-dim);
  font-size: 14px; font-weight: 700; line-height: 1;
  cursor: pointer;
  transition: all 0.15s;
}
.nav-colony-chip:hover { color: var(--txt); background: var(--accent-soft); }
.nav-colony-chip.active {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
}
/* home marker — same plain ★ glyph the HUD dropdown bakes into its label */
.nav-colony-chip-home {
  position: absolute; top: -4px; left: -3px;
  font-size: 10px; color: var(--gold, #e8c268);
  pointer-events: none;
}
/* attention dot — same passive needsAttention signal as .hud-select-opt's,
   but corner-positioned (the chip has no room for an inline dot) */
.nav-colony-chip.needs-attention::after {
  content: '';
  position: absolute; top: -3px; right: -3px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--warn);
  box-shadow: 0 0 4px rgba(255, 179, 71, 0.8);
}

/* C12 (UI audit) — one-line shortcut discoverability hint under the strip */
.nav-hint {
  padding: 6px 2px 0;
  font-size: 12px; letter-spacing: 0.08em;
  color: var(--txt-dim); opacity: 0.65;
  white-space: nowrap;
}

/* C12 — shortcut help overlay body (openOverlay size:'panel') */
.shortcut-help-grid {
  display: grid; grid-template-columns: auto 1fr; gap: 6px 14px;
  align-items: center;
  padding: 4px 2px;
}
.shortcut-key {
  justify-self: start;
  min-width: 22px; padding: 2px 7px;
  background: rgba(9, 14, 28, 0.8);
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  color: var(--accent);
  font-family: var(--mono, monospace); font-size: 14px; font-weight: 700;
  text-align: center;
}
.shortcut-label { font-size: 14px; color: var(--txt-dim); }

#page {
  flex: 1; min-width: 0;
  overflow-y: auto; overflow-x: hidden;
  padding: calc(var(--sp) * 3);
  /* A1: reserve room for #chat-dock (fixed, bottom-left) on top of the
     normal bottom gap, so the last rows of any table/panel never sit
     underneath it — same reserve now also covers `.map-action-dock`
     (bottom-center, global as of the "consistent GUI on every tab" pass),
     which is shorter (~140px max) than the chat-dock reserve already is. */
  padding-bottom: calc(var(--sp) * 3 + var(--chat-dock-h));
  scroll-behavior: smooth;
  /* Unified-shell prototype (owner: "darabos a HUD/GUI" — the page reads
     as a pile of separately-framed boxes instead of one console housing
     with a single screen in the middle). This inset shadow gives #page
     itself the "screen bezel" — one continuous border around the whole
     dynamic-content region — using the same #2D3B55/#101a26 family
     already established as the flat-panel language (#chat-dock,
     .map-side-card). box-shadow (not border) so it doesn't add to the
     scrollable content box and stays fixed to the viewport during
     internal scroll, same trick .ov-stat-bar's glow already relies on. */
  box-shadow: inset 0 0 0 1px #2D3B55, inset 0 0 24px rgba(0, 0, 0, 0.25);
}
/* `.map-side-dash` (global as of the "consistent GUI on every tab" pass)
   is a right-edge overlay, same 1120px cutoff its own `display:none`
   breakpoint uses — reserve the width only where the sidebar is actually
   visible, so page content never sits underneath it on any route. */
@media (min-width: 1121px) {
  #page { padding-right: calc(313px + var(--sp) * 5); }
}

/* ----------------------------------------------------------------- panels */

.panel {
  /* "Glass console" treatment (owner: don't want this to feel like a
     browser game) — a subtle top-to-bottom gradient fill for depth instead
     of a flat tint, a soft outer drop shadow for real elevation off the
     background art, and a faint inset top highlight (::before below) like
     a light catching the top bezel of a physical panel. */
  background: linear-gradient(180deg, rgba(21, 33, 58, 0.82), rgba(9, 14, 26, 0.86));
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  padding: calc(var(--sp) * 2);
  margin-bottom: calc(var(--sp) * 2);
  backdrop-filter: blur(10px);
  box-shadow:
    var(--shadow-panel),
    0 1px 0 rgba(255, 255, 255, 0.03) inset;
  position: relative;
}
.panel::before {
  /* The thin bright bezel line along the top edge, seeded from the Wenrexa
     reference kit — pure CSS so it stays crisp at any zoom/DPI (a raster
     9-slice import would blur). Rounds with the panel's own corners. */
  content: '';
  position: absolute; top: 0; left: 12px; right: 12px; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(125, 226, 255, 0.55), transparent);
  pointer-events: none;
}
/* v0.44.0 "HOLO GUI" kit (docs/PHASE6-UI-CHROME.md §5i): owner supplied a
   reference UI-kit screenshot (chamfered/cut panel corners, a bright glow
   border, a separate darker header strip) after 6 rounds fighting raster
   corner-bracket art (§5-§5h) — adopted that language wholesale, panels
   AND buttons AND inputs/checkboxes (owner: "Teljes UI kit" — full kit,
   not just panels). `clip-path: polygon(...)` cuts the SAME two opposite
   corners (top-right, bottom-left) on every chamfered element — one
   consistent diagonal-cut signature across the whole HUD, matching the
   reference. `--chamfer`/`--chamfer-sm` tokens (root :root block) keep
   the cut size consistent between panels and buttons.

   v0.58.0: reworked from `border` + `clip-path` + a corner-wedge patch to
   a double-layer clip-path FILL (see the rule and the comment right
   below it) after an owner-reported "missing edges" bug — a real
   diagonal gap at the two cut corners in some browsers/GPUs. Combining
   `border` with `clip-path` on one box is a genuine cross-browser
   rasterization seam (the border stroke and the clip mask are computed
   in separate passes that don't always agree exactly at the diagonal
   cut) — a DIFFERENT defect than every wedge bug this file used to
   chase below, and one this comment's old "structurally impossible"
   claim never actually covered. */
.panel-framed {
  /* owner (screenshot comparison, "generálj tökéletes profi frameket...
     legyen prémium a játék"): `docs/art/ref-GUI/frame_kit.png` ("1. FRAME
     KIT (9-SLICE SYSTEM)") shows every corner/border piece as a genuine
     GLOWING gradient — brighter where light "hits" it, fading elsewhere —
     not a flat single-color line. `--panel-accent` here used to be a flat
     `var(--accent)` (one solid color for the whole border ring); a
     diagonal gradient across the same variable produces the identical
     "thin lit ring" this technique already draws (see `::before`'s own
     comment — no structural change needed, `background` already accepts
     a gradient just as well as a flat color) but reads as genuinely lit
     rather than a flat plastic outline. Applies everywhere this token
     isn't already overridden to something else — the single highest-
     reach lever for this ask, short of generating real border art. */
  --panel-accent: linear-gradient(
    135deg,
    color-mix(in srgb, var(--accent) 55%, transparent),
    color-mix(in srgb, var(--accent) 100%, transparent) 35%,
    color-mix(in srgb, var(--accent) 40%, transparent) 65%,
    color-mix(in srgb, var(--accent) 90%, transparent)
  );
  --panel-border-w: 2px;
  /* v0.57.4: owner preferred the original deeper blue over this teal base
     ("jobban beleolvadt az univerzum témába" — blends better into the
     universe theme) — recolored from the bespoke #0a1e2a/#050e14 pair to
     the same --bg2/--bg1 tokens the starfield body background and the
     HUD/sidenav shell already use, so panels, chrome, and the space behind
     them all read as one color family. Shape/layout (the chamfer, border,
     wedge accents) is untouched — only the fill hue changed. */
  --panel-fill: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg2) 90%, var(--bg0)),
    color-mix(in srgb, var(--bg1) 94%, var(--bg0))
  );
  position: relative;
  isolation: isolate;
  background: transparent;
  border: none;
  /* Sharp corners everywhere (not just the 2 chamfered ones) — a radius
     on the OTHER two corners created a visible mismatch against the
     header's own flush square corner sitting right on top of it (owner:
     "bal felül furcsa" — top-left looked wrong). clip-path doesn't
     respect border-radius at all, so a radius here only ever affected
     those 2 uncut corners inconsistently with the rest of the shape. */
  border-radius: 0;
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer)) 0, 100% var(--chamfer),
    100% 100%, var(--chamfer) 100%, 0 calc(100% - var(--chamfer))
  );
  box-shadow: 0 0 22px color-mix(in srgb, var(--panel-accent) 28%, transparent), var(--shadow-panel);
  padding: 24px;
}
/* `::before` = the full-box border-color layer; `::after` = an inset-by-
   `--panel-border-w` fill layer painted on top of it, so only a thin ring
   of `::before` shows around the edge — that ring IS the border, with no
   `border` property involved anywhere, so there's no separate stroke
   render pass left to fall out of step with the clip mask (the actual
   fix for the "missing edges" bug — see the comment above `.panel-framed`
   for the full story). `clip-path: inherit` on both just reuses the
   parent's own polygon rather than repeating the six coordinates twice.
   Both layers must be OPAQUE — `--panel-fill` is baked via `color-mix()`
   against `--bg0` rather than a translucent `rgba()` — since `::after`
   now sits on top of `::before`, not on top of the page; real
   transparency here would let the border color bleed through the fill.
   `isolation: isolate` + negative `z-index` on both (below) means every
   REAL child (the `.sec-h` header strip, panel content) sits above them
   automatically via ordinary stacking — no more explicit z-index
   tug-of-war needed against the header the way the old wedge required
   (its `z-index: 2` fight with `.sec-h`/`.ov-head` is gone; the surviving
   `z-index: 1` on those two is now redundant-but-harmless, left as-is
   rather than risk missing some other reason it was there). `.panel::
   before` (the base bezel-line rule, ~line 587) also targets this same
   `::before` — `.panel-framed` is always combined with `.panel` — but
   `inset: 0` here fully redeclares the box geometry so nothing from that
   earlier rule leaks through (the old `left: 12px` collision this file
   used to document is structurally impossible now, not just patched). */
.panel-framed::before,
.panel-framed::after {
  content: '';
  position: absolute;
  clip-path: inherit;
  pointer-events: none;
}
.panel-framed::before {
  /* v0.57.10: every `.panel-framed` is also a plain `.panel`, and
     `.panel::before`'s bezel-line rule sets `height: 1px` on that same
     pseudo-element. `inset: 0` here never touched `height`, so that 1px
     survived the cascade — the full-box accent/border layer this rule is
     SUPPOSED to be was actually rendering as a 1px-tall strip, and the
     inherited chamfer clip-path (sized for the real panel height) then
     chewed most of its width away right at the corners it was cutting.
     Explicit `height: auto` lets `inset: 0`'s top/bottom fully control
     the box again, restoring the intended full-height accent layer (and
     with it, a border that actually reaches every edge/corner). */
  inset: 0;
  height: auto;
  background: var(--panel-accent);
  z-index: -2;
}
.panel-framed::after {
  inset: var(--panel-border-w);
  background: var(--panel-fill);
  z-index: -1;
}
/* The reference's separate, slightly-darker title strip — `sectionHeader()`
   already renders `.sec-h` as a panel's first child everywhere, so this
   targets it directly rather than adding new markup. `margin: -24px -24px
   16px` pulls it back out to the panel's own (pre-clip) edges since
   `.panel-framed`'s padding pushed it inward like any other child.
   Deliberately NOT its own separate clip-path/wedge anymore — that was
   the actual root cause of the corner bugs above: two independently
   chamfered shapes overlapping at the same physical corner never quite
   nested cleanly. The header is now a plain rectangle; the PARENT
   panel's own `clip-path` already clips any child's rendering to its
   chamfered shape (the same mechanism as `overflow: hidden`, just to a
   custom polygon instead of a box), so the header's square corner gets
   auto-trimmed to match, and the panel's own (now-on-top, `z-index: 2`)
   wedge fills the visible notch — one single unambiguous cut instead of
   two competing ones. */
/* `.panel.panel-framed > .sec-h:first-child` (both classes compounded on
   the same selector, not just `.panel-framed >`) deliberately outranks
   the sticky-header rule below (`.panel > .sec-h:first-child`) on
   specificity (4 selectors vs. 3) rather than relying on source order —
   both match the same element since every `.panel-framed` is also a
   `.panel`, and a same-specificity tie would otherwise flip depending on
   which rule happens to appear later in the file. */
.panel.panel-framed > .sec-h:first-child {
  /* Bug (owner: "jobb felül nyitva maradt" persisted after the z-index
     fix): this rule stopped declaring `position` at all after the
     clip-path/wedge removal above, so the property fell through to the
     UNRELATED pre-existing sticky-header rule (`.panel > .sec-h:first-
     child`, further down this file) — which sets `position: sticky` +
     `z-index: 2`. That z-index TIES with `.panel-framed::before`'s own
     `z-index: 2`, and equal z-index ties resolve by tree order — the
     header, painting later, won the tie and covered the wedge again.
     `position: relative` here (re-)overrides the sticky rule's `position`
     for framed-panel headers specifically (unaffected panels keep their
     sticky behavior) — still needed. `z-index: 1` no longer fights a
     wedge for it (.panel-framed's border/fill pseudo-elements sit at
     negative z-index now, so any real child stacks above them by
     default) but is left in place as harmless belt-and-suspenders. */
  position: relative;
  z-index: 1;
  background: linear-gradient(180deg, rgba(79, 216, 255, 0.16), rgba(79, 216, 255, 0.05));
  border: none;
  border-bottom: 1px solid rgba(79, 216, 255, 0.45);
  margin: -24px -24px 16px;
  padding: 10px 18px 10px calc(18px + var(--chamfer));
  font-family: var(--font-display);
}
/* `border-color`/`background` here are for plain `.panel` (still a real
   border+background). The `--panel-accent`/`--panel-fill` overrides are
   for the `.panel-framed` combo (e.g. `panel panel-danger panel-framed`,
   used for Danger Zone / disband confirmations), whose own border/fill
   are now painted by pseudo-elements instead — see `.panel-framed`
   above. `--panel-fill` is baked opaque via `color-mix()`, not a
   translucent `rgba()`, for the same reason `.panel-framed`'s own
   `--panel-fill` is: it paints on top of `--panel-accent`, not the page. */
.panel-danger {
  border-color: rgba(255, 77, 93, 0.45);
  background: rgba(38, 13, 21, 0.66);
  --panel-accent: rgba(255, 77, 93, 0.45);
  --panel-fill: color-mix(in srgb, rgb(38, 13, 21) 66%, var(--bg0));
}
.panel-warn {
  border-color: rgba(255, 176, 46, 0.4);
  --panel-accent: rgba(255, 176, 46, 0.4);
}

/* Colony Operations tab body (colony.js — colonyOperationsPanel): the
   content that used to be 7 separate small `.panel` boxes below the Village
   Scene now lives here, one at a time, switched by the `.tabs`/`.tab` bar
   above it (same component leaderboard.js/settings.js already use). Up to 7
   tabs is more than the shared `.tab { flex: 1 }` default was ever sized
   for — same `flex: 0 0 auto` override .lb-tabs already uses, plus a
   horizontal scroll strip (rather than wrapping to a second row) so a
   narrow/mobile viewport stays usable instead of clipping tabs off-screen
   (body{} has overflow:hidden, so an unconstrained overflow here would
   silently hide — not just crowd — the last tab or two). */
.colony-ops-tabs { overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; }
.colony-ops-tabs .tab { flex: 0 0 auto; padding: 5px 10px; white-space: nowrap; font-size: 14px; }
.colony-ops-body { padding-top: 6px; }
/* Owner (zero-scroll pass): the Village Scene above this panel now shrinks
   to leave it real room (see .village-scene's own max-height comment), but
   that room is still tight on a normal-height viewport — compact whatever
   table a given ops tab renders (Production & Stocks, Defense Status, etc.)
   the same amount leaderboard.js's `.lb-page` got, scoped to this panel only. */
.colony-ops-body .table th { padding: 4px 8px; }
.colony-ops-body .table td { padding: 3px 8px; }
.colony-ops-body .table { font-size: 14px; }
/* Matches the Village Scene's own edge-to-edge bleed right above it (owner:
   "the panel should also use the max width") — identical left/right
   negative-margin trick, see .village-scene's comment in this file. Panel
   keeps its own border/radius/padding; only its outer width changes. */
.colony-ops-panel {
  width: calc(100% + var(--sp) * 6);
  margin-left: calc(var(--sp) * -3);
  margin-right: calc(var(--sp) * -3);
}

/* Owner: "a button-row panel, with each building's small icon; clicking
   pops up a detailed window" — replaces the old tabbed Colony Operations
   panel below the Village Scene. Same edge-to-edge bleed width as that
   panel had (matches the scene above it).
   v0.60.29 (owner: "make sure they fit in the panel without scrolling") —
   this used to be a horizontal-scroll strip (`overflow-x: auto`, same
   `.colony-ops-tabs` pattern used for many-tab bars elsewhere), but once
   the buttons got wider (the immediately-prior "make the buttons a bit
   larger" round) the full 24-building set no longer fit in one row on a
   normal viewport — a real, visible scrollbar, not just a narrow-viewport
   fallback anymore. Switched to `flex-wrap: wrap` — matches this whole
   project's established zero-scroll doctrine (pagination/wrapping over
   internal scrollboxes) better than a horizontal scrollbar ever did
   anyway. The panel has no fixed height, so it grows to fit however many
   rows the icons wrap into. */
.bld-icon-row-panel {
  /* Owner: "make the building selection panel same color style as the rest
     of the panels" — was still inheriting `.panel-framed`'s DEFAULT bright
     glowing cyan-gradient border + navy fill, which now clashes with every
     other panel on screen (the sidebar dashboard cards + Directives/Season
     stat chips) all using the muted `info_chip.svg`-derived palette instead
     (flat `#1F2A3A` border, opaque `#101a26`→`#0a0f16` fill, glow reserved
     for small corner-tick decals only). Overriding just the 2 custom
     properties `.panel-framed::before/::after` already read recolors this
     panel to match without touching its own custom bottom-corner clip-path
     (the "swap" shape from an earlier round, untouched here). */
  --panel-accent: #1F2A3A;
  --panel-fill: linear-gradient(180deg, #101a26 0%, #0a0f16 100%);
  width: calc(100% + var(--sp) * 6);
  margin-left: calc(var(--sp) * -3);
  margin-right: calc(var(--sp) * -3);
  padding: 8px;
  /* Owner: "cut the right bottom edge and fulfil the right top edge (swap
     basically)" — `.panel-framed`'s inherited default cuts top-right +
     bottom-left. Overriding just this panel's own `clip-path` (same
     specificity as `.panel-framed`'s, wins on source order since this
     rule comes later) swaps the RIGHT side only: top-right now sharp,
     bottom-right now cut, left side untouched (bottom-left stays cut).
     `.panel-framed::before/::after` both use `clip-path: inherit`, so
     they automatically pick up this shape too — no separate override
     needed there. */
  clip-path: polygon(
    0 0, 100% 0, 100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%, var(--chamfer) 100%, 0 calc(100% - var(--chamfer))
  );
  /* Bug fix (owner screenshot: rightmost "Territories" card overhanging past
     this panel's own edge onto the village scene above): root cause is
     hud-2026's dock-magnify proximity lift (public/js/ui/dock-magnify.js) —
     it grows the hovered .bld-icon-btn via `transform: translateY() scale()`
     (up to translateY(-7px) scale(1.14), confirmed measured), which never
     touches flex layout so it never displaces neighbours, but ALSO never
     stays inside the button's own flex slot. A middle button's growth just
     bleeds harmlessly into a neighbour's padding; an edge button (first or
     last in the row — Territories always sorts last) has no neighbour on
     its outer side, so it bleeds past the strip's own padding and this
     panel's edge into whatever sits next to it (here, the scene above).
     `overflow: hidden` on the one shared ancestor clips that escape at the
     panel boundary for every item uniformly, without touching the magnify
     effect itself (still visible mid-row) or colony.js. */
  overflow: hidden;
}
/* v0.60.30 (owner: "make sure the building fit to 1 line, no scrolling")
   — reverses the v0.60.29 flex-wrap fix: wrapping avoided a horizontal
   scrollbar but let the row grow to 2 rows (25th item, Territories,
   spilling alone onto its own second line) — not what "fit to 1 line"
   asks for. `flex-wrap:nowrap` + each button as `flex:1 1 0; min-width:0`
   (instead of a fixed 82px) makes every button share the strip's actual
   available width EQUALLY — the whole set always fits exactly one row by
   construction, at any viewport width or building count, with no
   scrollbar and no wrap. Buttons compress on narrower screens instead of
   overflowing; `.bld-icon-label`'s existing text-overflow:ellipsis
   already handles a narrow button's name gracefully. */
.bld-icon-strip {
  display: flex; flex-wrap: nowrap; gap: 6px;
}
.bld-icon-btn {
  --panel-accent: var(--panel-border);
  --panel-fill: var(--bg1);
  --hud-chamfer: 7px;
  /* dock-magnify.js writes --dock-mag per-frame (0..1, peaking on the exact
     card under the pointer, easing to 0 at the edge of its pull radius) but
     only mounts under the hud-2026 skin. --hb (hover) is the plain-CSS
     fallback for when that skin is opted out. --holo = the louder of the
     two, so the glow below works identically either way without a
     specificity fight between a JS-driven value and a :hover rule. */
  --dock-mag: 0;
  --hb: 0;
  --holo: max(var(--dock-mag), var(--hb));
  position: relative; isolation: isolate;
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 10px 5px 8px;
  background: transparent; border: none; cursor: pointer;
  clip-path: polygon(
    0 0, calc(100% - var(--hud-chamfer)) 0, 100% var(--hud-chamfer),
    100% 100%, var(--hud-chamfer) 100%, 0 calc(100% - var(--hud-chamfer))
  );
  transition: box-shadow 0.15s;
  z-index: 1;
}
/* Owner: "itt valahogy jobban emeljük ki... holo szerű módszerrel, hogy
   éppen melyik kártyán van a kurzor" — the old hover (a flat 10px cyan
   box-shadow) barely read next to dock-magnify's own scale/lift, and
   didn't distinguish the ONE card actually under the cursor from its
   proximity-lifted neighbours. Real :hover lifts z-index so the hovered
   card's escaping glow draws over its neighbours instead of getting
   clipped/covered by them mid-sweep. */
.bld-icon-btn:hover { z-index: 5; --hb: 1; }
.bld-icon-btn::before, .bld-icon-btn::after {
  content: ''; position: absolute; clip-path: inherit; pointer-events: none;
}
.bld-icon-btn::before { inset: 0; background: var(--panel-accent); z-index: -2; }
.bld-icon-btn::after {
  inset: 1px; background: var(--panel-fill); z-index: -1;
  box-shadow:
    0 0 calc(6px + var(--holo) * 22px) rgba(89, 213, 242, calc(var(--holo) * 0.85)),
    inset 0 0 calc(4px + var(--holo) * 12px) rgba(89, 213, 242, calc(var(--holo) * 0.4));
  outline: 1px solid rgba(89, 213, 242, var(--holo));
  outline-offset: -1px;
}
/* Travelling scan band, same visual language as .rail-holo-scan — fades in
   with --holo, so it reads as "this one is live" rather than a static
   decoration painted on every card. */
.bld-icon-scan {
  position: absolute; left: 1px; right: 1px; height: 40%; top: -40%;
  z-index: 0; pointer-events: none;
  opacity: var(--holo);
  background: linear-gradient(180deg, transparent, rgba(89, 213, 242, .45), transparent);
  animation: bld-icon-scan-sweep 1.4s linear infinite;
}
@keyframes bld-icon-scan-sweep { to { top: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .bld-icon-scan { animation: none; opacity: 0; }
}
.bld-icon-btn.bld-icon-active { --panel-accent: var(--ok); }
.bld-icon-btn.bld-icon-locked { opacity: 0.55; }
.bld-icon-glyph { display: flex; align-items: center; justify-content: center; height: 32px; }
.bld-icon-glyph .ic-img { height: 29px; max-width: 32px; }
.bld-icon-label {
  font-size: 12px; line-height: 1.2; text-align: center; color: var(--txt-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.bld-icon-badge { font-size: 12px; color: var(--accent); }
.bld-icon-btn.bld-icon-locked .bld-icon-badge { color: var(--txt-faint); }
.bld-icon-btn-territories { --panel-accent: rgba(255, 209, 102, 0.55); }
.bld-icon-btn-territories .bld-icon-glyph { font-size: 21px; color: var(--fc-traders); }

/* Folded-in colony-wide sections inside a building's detail popup (owner:
   Production & Stocks on every popup; Research/Defense/Charter/Repair on
   whichever building governs that system) — a plain label divider, not a
   full `.sec-h` (that reads as a page-section header, too heavy inside an
   already-titled popup). */
.b-fold-head {
  margin: 14px 0 6px; padding-top: 10px; border-top: 1px solid var(--panel-border);
  font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--txt-dim);
}
.b-fold-section { margin: 14px 0 0; padding-top: 10px; border-top: 1px solid var(--panel-border); }
.b-fold-section > summary { font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }

.hoverable { transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s; }
.hoverable:hover {
  border-color: var(--panel-border-hi);
  box-shadow: 0 0 18px rgba(79, 216, 255, 0.10), 0 4px 20px rgba(0, 0, 0, 0.4);
  transform: translateY(-1px);
}

.sec-h {
  display: flex; align-items: baseline; gap: var(--sp);
  font-size: 13px; font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--txt-dim);
  margin: calc(var(--sp) * 2) 0 var(--sp);
  padding-bottom: 4px;
  border-bottom: 1px solid rgba(79, 216, 255, 0.08);
}
.panel > .sec-h:first-child {
  margin-top: 0;
  /* Owner: a panel's own title shouldn't scroll away with its body — this is
     the ONE header selector shared by every panel (Defense Status,
     Production & Stocks, Construction Queue, etc.), so pinning it here fixes
     it everywhere at once. Only actually activates inside a panel whose
     WRAPPING box scrolls (the freeform layout customizer's `overflow-y:auto`
     panels) — harmless no-op elsewhere. Extends into the panel's own side
     padding (negative margin + matching padding) so scrolled content can't
     peek out the sides once stuck.
     v2 (owner: looked like a separate box pasted onto the panel, not
     aligned/blended with it): the backing now matches the panel's OWN
     gradient tone (nearly opaque, not flat solid) with the same blur, and
     the box-shadow is gone — it was the main "boxed" cue since it stayed
     visible even when the header wasn't actually scrolled under anything. */
  position: sticky; top: 0; z-index: 2;
  margin-left: calc(var(--sp) * -2); margin-right: calc(var(--sp) * -2);
  padding-left: calc(var(--sp) * 2); padding-right: calc(var(--sp) * 2);
  padding-bottom: calc(var(--sp) * 2);
  background: rgba(21, 33, 58, 0.92);
  backdrop-filter: blur(10px);
}
/* v3 (owner: still looked "pasted on," specifically over the Village Scene's
   bright painted photo — a hard-edged box reads fine against another
   panel's own dark gradient, but is stark against a busy colorful image).
   A soft fade strip below the header dissolves that hard edge into whatever
   is actually behind it (photo or gradient alike) instead of cutting off
   abruptly — reads as a natural vignette, not a box border. */
.panel > .sec-h:first-child::after {
  content: '';
  position: absolute; left: 0; right: 0; top: 100%; height: 22px;
  background: linear-gradient(180deg, rgba(21, 33, 58, 0.55), transparent);
  pointer-events: none;
}

/* ---- collapsible (native <details>/<summary>, "compact UI" preview) ---- */
.collapsible-summary {
  display: flex; align-items: center; gap: var(--sp);
  cursor: pointer; user-select: none; list-style: none;
  font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--txt-dim);
  padding: 2px 0;
}
.collapsible-summary::-webkit-details-marker { display: none; }
.collapsible-summary::before {
  content: '▸'; display: inline-block; color: var(--accent);
  transition: transform 0.15s; font-size: 12px;
}
.collapsible[open] > .collapsible-summary::before { transform: rotate(90deg); }
.collapsible-summary:hover { color: var(--txt); }
.collapsible-body { padding-top: var(--sp); }

.page-head {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: var(--sp);
  margin-bottom: calc(var(--sp) * 2);
}
.page-head h2 {
  font-family: var(--font-display);
  font-size: 19px; font-weight: 700; letter-spacing: 0.10em; text-transform: uppercase;
  color: var(--txt);
  text-shadow: 0 0 18px rgba(79, 216, 255, 0.25);
  display: flex; align-items: center; gap: var(--sp);
}
.page-head-right { display: flex; align-items: center; gap: var(--sp); }
.coords-link { font-size: 15px; }

/* ---------------------------------------------------------------- buttons */

.btn {
  --btn-accent: var(--accent);
  --btn-border-w: 1.5px;
  --btn-fill: linear-gradient(
    180deg,
    color-mix(in srgb, var(--btn-accent) 16%, var(--bg0)),
    color-mix(in srgb, var(--btn-accent) 5%, var(--bg0))
  );
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  position: relative;
  isolation: isolate;
  padding: 7px 16px 7px calc(16px + var(--chamfer-sm) * 0.6);
  background: transparent;
  border: none;
  border-radius: 2px;
  /* HOLO GUI kit (§5i): the same corner-cut signature as `.panel-framed`,
     scaled down (`--chamfer-sm`) for a control this size. */
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer-sm)) 0, 100% var(--chamfer-sm),
    100% 100%, var(--chamfer-sm) 100%, 0 calc(100% - var(--chamfer-sm))
  );
  color: var(--accent);
  font-family: var(--font-display); font-size: 14px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  cursor: pointer;
  transition: all 0.15s;
  user-select: none;
  /* A tactile bezel: a bright top edge + a soft dark drop below, so buttons
     read as physical raised controls over the background art rather than
     flat clickable text (owner: don't want this to feel like a browser
     game). */
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.08) inset,
    0 2px 6px rgba(0, 0, 0, 0.35);
}
/* Same double-layer clip-path fill as `.panel-framed` (see its comments
   for the full rationale) instead of `border` + `clip-path` + a wedge —
   scaled to `--chamfer-sm`/`--btn-border-w`. `--btn-fill` must stay
   opaque (`color-mix()`, not `rgba()`) since it paints on top of
   `--btn-accent`, not the page. */
.btn::before, .btn::after {
  content: '';
  position: absolute;
  clip-path: inherit;
  pointer-events: none;
}
.btn::before { inset: 0; background: var(--btn-accent); z-index: -2; }
.btn::after { inset: var(--btn-border-w); background: var(--btn-fill); z-index: -1; }
.btn:hover:not(:disabled) {
  --btn-fill: color-mix(in srgb, var(--btn-accent) 24%, var(--bg0));
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.1) inset,
    0 0 14px rgba(79, 216, 255, 0.5);
  text-shadow: none;
}
.btn:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) inset;
}
.btn:disabled { opacity: 0.35; cursor: not-allowed; }

.btn-primary {
  --btn-fill: linear-gradient(
    180deg,
    color-mix(in srgb, var(--btn-accent) 30%, var(--bg0)),
    color-mix(in srgb, var(--btn-accent) 12%, var(--bg0))
  );
  color: #dff6ff;
}
.btn-ghost { --btn-accent: var(--panel-border); color: var(--txt-dim); }
.btn-ghost:hover:not(:disabled) { color: var(--accent); --btn-accent: var(--panel-border-hi); }

/* Map toolbar — a `.page-head` immediately above a `.map-stage` (galaxy/
   system/planet tiers) sits directly on the nebula backdrop instead of the
   flat page background every other `.page-head` has, so its normal opaque
   `.btn` fill reads as a hard UI bar cutting across the artwork (owner:
   "the whole heading + button row panel should be transparent" — v0.60.13).
   Fading both clip-path fill layers via `opacity` (not touching --btn-fill/
   --btn-accent) keeps every existing accent/active/hover color untouched —
   it just becomes see-through, plus a blur so the row still reads over a
   busy sky. `:has()` picks this up on all three map tiers with no JS change. */
.page-head:has(+ .map-stage) .btn::before { opacity: 0.32; }
.page-head:has(+ .map-stage) .btn::after {
  opacity: 0.5;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.btn-sm { padding: 4px 9px; font-size: 13px; }
.btn-block { width: 100%; margin-top: var(--sp); }
.danger-hover:hover:not(:disabled) {
  color: var(--danger) !important;
  --btn-accent: rgba(255, 77, 93, 0.5) !important;
  box-shadow: 0 0 10px rgba(255, 77, 93, 0.25) !important;
}
.slot-btn.discouraged { opacity: 0.5; }

/* ----------------------------------------------------------------- tables */

.table-scroll { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: 15px; }
.table th {
  text-align: left; padding: 6px 10px;
  font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--txt-faint); font-weight: 600;
  border-bottom: 1px solid var(--panel-border);
}
.table th.r { text-align: right; }
.table td { padding: 8px 10px; border-bottom: 1px solid rgba(79, 216, 255, 0.06); vertical-align: middle; }
.table tbody tr { transition: background 0.12s; }
.table tbody tr:hover { background: rgba(79, 216, 255, 0.05); }
.table tr.row-me { background: rgba(79, 216, 255, 0.08); box-shadow: inset 2px 0 0 var(--accent); }
.table tr.row-danger { background: rgba(255, 77, 93, 0.06); }
.table tr.row-npc { background: rgba(122, 159, 255, 0.04); }
/* v0.58.2: empire table (overview.js) — a colony flagged by the server's
   passive needsAttention signal (idle queue / near-capped storage /
   incoming attack) gets its own amber tint, distinct from the red
   .row-danger used for the incoming-hostiles table above. */
.table tr.row-attn { background: rgba(255, 176, 46, 0.05); box-shadow: inset 2px 0 0 var(--warn); }

.tag {
  display: inline-block; padding: 1px 7px; margin-left: 6px;
  font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  border-radius: 3px;
}
.tag-me { background: rgba(79, 216, 255, 0.18); color: var(--accent); }
.tag-prot { background: rgba(255, 176, 46, 0.15); color: var(--warn); }
.tag-npc { background: rgba(122, 159, 255, 0.16); color: var(--accent2); }
.tag-danger { background: var(--danger-soft); color: var(--danger); margin: 2px; }
.tag-gold { background: rgba(255, 176, 46, 0.15); color: var(--warn); margin: 2px; }

.medal.m-1 { color: #ffd166; }
.medal.m-2 { color: #cdd7e4; }
.medal.m-3 { color: #d1915a; }

.pager {
  display: flex; align-items: center; justify-content: center; gap: calc(var(--sp) * 2);
  padding-top: var(--sp);
}

/* -------------------------------------------------------------- countdowns */

.countdown { color: var(--accent); }
.countdown.danger-text { color: var(--danger); }
.countdown.ok-text { color: var(--ok); }

/* ------------------------------------------------------------- cost chips */

.cost-chips { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.cost-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 7px;
  background: rgba(5, 7, 15, 0.55);
  border: 1px solid var(--panel-border);
  border-radius: 4px;
  font-size: 14px;
  transition: border-color 0.2s, color 0.2s;
}
.cost-chip.unaffordable { border-color: rgba(255, 77, 93, 0.55); }
.cost-chip.unaffordable .mono { color: var(--danger); }
.cost-chip.free { color: var(--txt-faint); }
/* Phase 2c — amber aurium cost chip (the ⬧ high-tier gate, PHASE2C §6). */
.cost-chip.aurium-chip {
  color: var(--fc-traders);
  border-color: rgba(255, 209, 102, 0.42);
  background: rgba(255, 209, 102, 0.07);
}
.cost-chip.aurium-chip .res-ic { color: var(--fc-traders); text-shadow: 0 0 6px rgba(255, 209, 102, 0.5); }
.cost-chip.aurium-chip.unaffordable { border-color: rgba(255, 77, 93, 0.55); }
.cost-chip.aurium-chip.unaffordable .mono { color: var(--danger); }
/* Phase 2c — aurium-gated shipyard tier (the dreadnought card). */
.u-card-aurium { border-color: rgba(255, 209, 102, 0.32); }
.aurium-tier-badge {
  font-size: 12px; letter-spacing: 0.04em; color: var(--fc-traders);
  vertical-align: middle; white-space: nowrap;
}
.u-aurium-note { font-size: 13.5px; line-height: 1.4; margin: 2px 0; }

.unit-chips { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.unit-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 7px;
  background: rgba(5, 7, 15, 0.45);
  border: 1px solid rgba(122, 159, 255, 0.18);
  border-radius: 4px;
  font-size: 14px;
}
.unit-chip .unit-ic { color: var(--accent2); }
.unit-chip .qty { color: var(--txt); }
.unit-chips.compact .unit-chip { padding: 1px 5px; font-size: 13px; }

/* ------------------------------------------------------------ requirements */

.req-list { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
.req {
  font-size: 13px; padding: 1px 7px; border-radius: 3px; letter-spacing: 0.04em;
}
.req-met { color: var(--ok); background: rgba(61, 220, 151, 0.08); }
.req-unmet { color: var(--danger); background: var(--danger-soft); }

/* ------------------------------------------------------------ empty states */

.empty-state {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp);
  padding: calc(var(--sp) * 5) calc(var(--sp) * 2);
  color: var(--txt-dim); text-align: center;
}
.empty-glyph { font-size: 36px; color: var(--txt-faint); }
.empty-glyph.spin { animation: spin 1.6s linear infinite; color: var(--accent); }
@keyframes spin { to { transform: rotate(360deg); } }
.empty-msg { max-width: 420px; }

/* ------------------------------------------------------------------ toasts */

#toasts {
  position: fixed; top: calc(var(--hud-h) + var(--activity-h) + var(--sp)); right: calc(var(--sp) * 2);
  /* Above the overlay stack (1000+) AND the 3D orbit modal (2000) — a toast
     confirming an action taken inside an open overlay must stay visible. */
  z-index: 2600;
  display: flex; flex-direction: column; gap: var(--sp);
  max-width: 380px;
}
.toast {
  padding: 10px 14px;
  background: rgba(13, 21, 38, 0.95);
  border: 1px solid var(--panel-border-hi);
  border-left-width: 3px;
  border-radius: var(--radius);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
  font-size: 15px;
  cursor: pointer;
  animation: toast-in 0.25s ease-out;
}
@keyframes toast-in {
  from { opacity: 0; transform: translateX(30px); }
  to { opacity: 1; transform: none; }
}
.toast-out { opacity: 0; transform: translateX(30px); transition: all 0.4s; }
.toast-info { border-left-color: var(--accent); }
.toast-success { border-left-color: var(--ok); }
.toast-warn { border-left-color: var(--warn); }
.toast-error, .toast-danger { border-left-color: var(--danger); }

/* ------------------------------------------------------------------- login */

.login-wrap {
  max-width: 760px; margin: 0 auto; padding-top: 5vh;
}
.login-hero { text-align: center; margin-bottom: calc(var(--sp) * 3); }
.login-logo {
  font-size: 44px; color: var(--accent);
  text-shadow: 0 0 30px rgba(79, 216, 255, 0.9);
  animation: pulse-glow 4s ease-in-out infinite;
}
.login-title {
  font-size: 32px; font-weight: 300; letter-spacing: 0.42em; margin-top: var(--sp);
  color: var(--txt);
}
.login-title b { font-weight: 700; color: var(--accent); }
.login-sub { color: var(--txt-dim); letter-spacing: 0.08em; margin-top: 4px; }

/* Server selector: only one world exists (the alpha test, ×50) — the other
   speed tiers are shown locked so a returning visitor can see the roadmap
   ("more worlds later") rather than wondering why there's only one option.
   Purely informational client-side; nothing here talks to a per-world
   backend yet (there's only one server to point at). */
.server-select {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  margin-top: var(--sp); margin-bottom: calc(var(--sp) * 2);
}
.server-chip {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 14px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: rgba(9, 14, 28, 0.6);
  font-family: var(--mono, monospace);
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
  color: var(--txt-dim);
}
.server-chip .sc-tag {
  font-size: 9px; font-weight: 700; letter-spacing: 0.1em;
  color: var(--txt-faint); text-transform: uppercase;
}
.server-chip.active {
  border-color: var(--accent); color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 0 10px rgba(79, 216, 255, 0.3);
}
.server-chip.active .sc-tag { color: var(--accent); }
.server-chip.locked { opacity: 0.4; }
.server-chip.locked .sc-tag::before { content: '🔒 '; }

.login-panel { padding: 0; overflow: hidden; }
.login-body { padding: calc(var(--sp) * 3); display: flex; flex-direction: column; gap: var(--sp); }
.login-turnstile { display: flex; justify-content: center; }

/* Phase 2 HUD chrome (docs/PHASE6-UI-CHROME.md §5d): a corner-bracket
   accent for the tab strip's bottom rail. A generated raster 9-slice frame
   was tried first and discarded — `.tabs` is just a thin flex row with a
   1px border-bottom (confirmed against the real CSS, not the source art's
   assumed shape), so a texture stretched to ~40px tall would blur badly.
   Pure CSS instead, matching `.panel::before`'s own established rationale
   ("stays crisp at any zoom/DPI, a raster import would blur" — same call,
   same reasoning, applied to a second thin-line case). Small angled corner
   ticks at both ends + a soft glow on the rail itself. */
.tabs {
  display: flex; position: relative;
  border-bottom: 1px solid var(--panel-border-hi);
  box-shadow: 0 1px 6px rgba(79, 216, 255, 0.15);
}
.tabs::before, .tabs::after {
  content: ''; position: absolute; bottom: -1px; width: 10px; height: 6px;
  border-bottom: 1px solid var(--accent); pointer-events: none;
}
.tabs::before { left: 0; border-left: 1px solid var(--accent); }
.tabs::after { right: 0; border-right: 1px solid var(--accent); }
.tab {
  flex: 1; padding: 12px;
  background: transparent; border: none; cursor: pointer;
  color: var(--txt-dim);
  font-family: inherit; font-size: 14px; font-weight: 700;
  letter-spacing: 0.2em; text-transform: uppercase;
  transition: all 0.15s;
}
.tab:hover { color: var(--txt); }
.tab.active {
  color: var(--accent);
  background: var(--accent-soft);
  box-shadow: inset 0 -2px 0 var(--accent);
}

/* ---------------------------------------------------------- Settings menu */
.settings-root { display: flex; flex-direction: column; gap: var(--sp); }
.settings-panel-host { display: flex; flex-direction: column; gap: var(--sp); }
.settings-tab-panel { display: flex; flex-direction: column; gap: 4px; }
.settings-subhead { margin-top: 8px; }
.settings-row {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px; border-bottom: 1px solid var(--panel-border);
}
.settings-row:last-child { border-bottom: none; }
.settings-row-label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.settings-row-hint { padding-left: 26px; }
.settings-scale-seg button { padding: 6px 16px; }
.settings-btn-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.settings-lang-list { display: flex; flex-direction: column; gap: 2px; }
.settings-lang-row {
  flex-direction: row; align-items: center; justify-content: flex-start; gap: 10px;
  background: transparent; border: 1px solid transparent; border-radius: var(--radius);
  cursor: pointer; text-align: left; width: 100%; color: var(--txt);
}
.settings-lang-row:hover { border-color: var(--panel-border-hi); }
.settings-lang-row.is-active { background: var(--accent-soft); border-color: var(--panel-border-hi); }
.settings-lang-flag { font-size: 18px; line-height: 1; }

/* Audio tab: toggle + volume-slider row (settings.js audioChannelRow). */
.settings-audio-row { gap: 6px; }
.settings-vol-row {
  display: flex; align-items: center; gap: 10px;
  padding-left: 26px; /* aligns under the label text, past the checkbox */
  transition: opacity 0.15s;
}
.settings-vol-row.is-off { opacity: 0.4; }
.settings-vol-pct { color: var(--txt-dim); min-width: 3.2em; text-align: right; }
/* Owner (UI kit reference sheet, "13. SLIDERS & INPUT"): a filled track +
   a chamfered thumb reads as part of the same HOLO GUI family as the
   checkbox (which already gets this clip-path treatment, see
   `input[type="checkbox"]` above) instead of a bare native slider. `--fill`
   is set by settings.js on init + every `input` event (`::-webkit-slider-
   runnable-track` has no native fill pseudo-element to hook, unlike
   Firefox's `::-moz-range-progress`, which DOES paint automatically). */
.settings-vol-slider {
  --fill: 0%;
  flex: 1 1 auto; max-width: 260px; height: 5px;
  appearance: none; -webkit-appearance: none;
  background: linear-gradient(to right,
    var(--accent) 0%, var(--accent) var(--fill),
    var(--panel-border) var(--fill), var(--panel-border) 100%);
  border-radius: 3px; outline: none;
  cursor: pointer;
}
.settings-vol-slider::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 15px; height: 15px;
  clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 4px 100%, 0 calc(100% - 4px));
  background: var(--accent); box-shadow: 0 0 8px var(--accent-soft);
  cursor: pointer; transition: transform 0.1s;
}
.settings-vol-slider::-webkit-slider-thumb:hover { transform: scale(1.15); }
.settings-vol-slider::-moz-range-thumb {
  width: 15px; height: 15px; border: none;
  clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 4px 100%, 0 calc(100% - 4px));
  background: var(--accent); box-shadow: 0 0 8px var(--accent-soft);
  cursor: pointer;
}
.settings-vol-slider::-moz-range-track { height: 5px; background: var(--panel-border); border-radius: 3px; }
.settings-vol-slider::-moz-range-progress { height: 5px; background: var(--accent); border-radius: 3px; }
.settings-vol-slider:disabled { cursor: not-allowed; }
.settings-vol-slider:disabled { background: var(--panel-border); }
.settings-vol-slider:disabled::-webkit-slider-thumb { background: var(--txt-dim); box-shadow: none; }
.settings-vol-slider:disabled::-moz-range-thumb { background: var(--txt-dim); box-shadow: none; }
.settings-danger { border-color: var(--danger, #d9534f); }

.form-label {
  font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--txt-faint); margin-top: var(--sp);
}
.login-body input[type="text"], .login-body input[type="password"] { padding: 10px 12px; font-size: 16px; }
.form-error {
  color: var(--danger); background: var(--danger-soft);
  border: 1px solid rgba(255, 77, 93, 0.35);
  border-radius: var(--radius); padding: 8px 12px; font-size: 15px;
}

.faction-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp);
}
.faction-card {
  padding: calc(var(--sp) * 1.5);
  background: rgba(5, 7, 15, 0.5);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: all 0.15s;
}
.faction-card:hover { border-color: var(--panel-border-hi); box-shadow: 0 0 14px rgba(79, 216, 255, 0.12); }
.faction-card.picked { border-width: 1px; transform: translateY(-1px); }
.faction-card.fc-combine.picked { border-color: var(--fc-combine); box-shadow: 0 0 16px rgba(79, 216, 255, 0.35); }
.faction-card.fc-traders.picked { border-color: var(--fc-traders); box-shadow: 0 0 16px rgba(255, 209, 102, 0.3); }
.faction-card.fc-hegemony.picked { border-color: var(--fc-hegemony); box-shadow: 0 0 16px rgba(255, 93, 108, 0.3); }
.faction-card.fc-vayu.picked { border-color: var(--fc-vayu); box-shadow: 0 0 16px rgba(105, 224, 138, 0.3); }
.faction-glyph-lg { font-size: 24px; }
.faction-card.fc-combine .faction-glyph-lg, .faction-card.fc-combine .faction-name { color: var(--fc-combine); }
.faction-card.fc-traders .faction-glyph-lg, .faction-card.fc-traders .faction-name { color: var(--fc-traders); }
.faction-card.fc-hegemony .faction-glyph-lg, .faction-card.fc-hegemony .faction-name { color: var(--fc-hegemony); }
.faction-card.fc-vayu .faction-glyph-lg, .faction-card.fc-vayu .faction-name { color: var(--fc-vayu); }
.faction-name { font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; font-size: 15px; margin-top: 2px; }
.faction-tagline { color: var(--txt-dim); font-size: 14px; font-style: italic; margin: 2px 0 6px; }
.faction-special { font-size: 13.5px; color: var(--txt); }
.spec-label {
  font-size: 11px; font-weight: 700; letter-spacing: 0.16em;
  color: var(--warn); background: rgba(255, 176, 46, 0.12);
  padding: 1px 5px; border-radius: 3px;
}
.faction-grid-fallback { text-align: center; }

.faction-tag {
  display: inline-flex; gap: 5px; align-items: center;
  font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 3px 10px; border: 1px solid var(--panel-border); border-radius: 12px;
}

/* --------------------------------------------------------------- overview */

.stat-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--sp); margin-bottom: calc(var(--sp) * 2);
}
/* HOLO GUI kit: same corner-cut signature as .panel-framed/.btn, scaled
   with --chamfer-sm — replaces the older flat gradient/top-highlight-line
   look (owner: bring these in line with the rest of the new design).
   Same double-layer clip-path fill as .panel-framed (see its comments)
   instead of `border` + `clip-path` + a wedge — fixes the same
   "missing edges" cross-browser seam. */
.stat {
  --stat-accent: var(--accent);
  --stat-border-w: 1.5px;
  /* v0.57.4: same --bg2/--bg1 recolor as .panel-framed, same reason. */
  --stat-fill: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg2) 90%, var(--bg0)),
    color-mix(in srgb, var(--bg1) 94%, var(--bg0))
  );
  padding: calc(var(--sp) * 1.5);
  background: transparent;
  border: none;
  border-radius: 0;
  isolation: isolate;
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer-sm)) 0, 100% var(--chamfer-sm),
    100% 100%, var(--chamfer-sm) 100%, 0 calc(100% - var(--chamfer-sm))
  );
  box-shadow: 0 0 14px rgba(79, 216, 255, 0.18), 0 6px 18px rgba(0, 0, 0, 0.35);
  position: relative;
}
.stat::before, .stat::after {
  content: '';
  position: absolute;
  clip-path: inherit;
  pointer-events: none;
}
.stat::before { inset: 0; background: var(--stat-accent); z-index: -2; }
.stat::after { inset: var(--stat-border-w); background: var(--stat-fill); z-index: -1; }
.stat-label { font-size: 12.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--txt-dim); }
.stat-value { font-size: 19px; margin-top: 2px; }
/* v0.60.33 round 2 (owner: "E:/Stellar/docs/art/ref-GUI/generated-frames/
   info_chip.svg i asked you to use this and you did the previous frame,
   why?") — round 1 misidentified the "Population 42,150" reference image
   as matching `corner_brackets.svg` (rounded target-lock viewfinder
   brackets, transparent middle). Reading the actual asset the owner named
   shows it's really `info_chip.svg`: a FULLY chamfered chip (all 4
   corners cut, `M12,1 H188 L199,12 V44 L188,55 H12 L1,44 V12 Z` — an
   octagon, not a rounded-corner rect), opaque dark fill (`#0F1622` @ 0.9),
   `#1F2A3A` body stroke, and — the detail round 1 missed entirely — small
   GLOWING accent brackets on only 2 of the 4 corners (top-left,
   bottom-right — the same diagonal-opposite-corner convention
   `.panel-framed` already uses elsewhere in this game), not all 4.
   Corrected below: real octagon clip-path (same 8-point polygon `.hud-res`
   uses for its own all-4-corner chamfer), opaque fill matching the SVG's
   own hex almost exactly, and 2 new small corner-glow SVGs (not 4) —
   `info-chip-corner-{tl,br}.svg`, straight L-brackets at the SVG's own
   `#2A88FF` blue (distinct from the `#00E0FF` cyan every other reference
   frame this session has used) — same fixed-size-decal technique, just
   correctly matching THIS reference's actual 2-corner-only accent. */
/* Owner: "looks good, apply here too" (Season page) — reused via a 2nd
   scoping class rather than generalizing to every `.stat-strip` (Overview/
   Alliance/Player weren't asked for). */
.dir-stat-strip .stat, .season-stat-strip .stat {
  clip-path: polygon(
    var(--chamfer-sm) 0, calc(100% - var(--chamfer-sm)) 0,
    100% var(--chamfer-sm), 100% calc(100% - var(--chamfer-sm)),
    calc(100% - var(--chamfer-sm)) 100%, var(--chamfer-sm) 100%,
    0 calc(100% - var(--chamfer-sm)), 0 var(--chamfer-sm)
  );
  box-shadow: none;
  border: 1px solid #1F2A3A;
  background-color: rgba(15, 22, 34, 0.9);
  background-image:
    url(/img/ui/info-chip-corner-tl.svg), url(/img/ui/info-chip-corner-br.svg);
  background-repeat: no-repeat;
  background-origin: border-box;
  background-position: top left, bottom right;
  background-size: 22px 22px, 22px 22px;
}
.dir-stat-strip .stat::before, .dir-stat-strip .stat::after,
.season-stat-strip .stat::before, .season-stat-strip .stat::after {
  content: none;
}

.colony-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: calc(var(--sp) * 2);
}
.colony-card { display: flex; flex-direction: column; gap: var(--sp); margin-bottom: 0; }
.colony-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp); }
.colony-name { font-size: 17px; font-weight: 700; letter-spacing: 0.04em; }
.home-star { color: var(--warn); }
.mini-res { display: flex; gap: 6px; flex-wrap: wrap; }
.mini-res-cell {
  display: inline-flex; gap: 4px; align-items: center;
  font-size: 14px; padding: 2px 8px;
  background: rgba(5, 7, 15, 0.5); border-radius: 4px;
}
/* v0.58.2: capacity-aware tint for a single resource pill (empire table) —
   same green(default)/amber/red tiers as the HUD's .hud-res-cell.res-warn/
   res-danger (main.js tickHUD), just generalized off that HUD-only selector
   so any `.mini-res-cell` (or future resource pill) can opt in. */
.mini-res-cell.res-warn { color: var(--warn); }
.mini-res-cell.res-danger { color: var(--danger); }
.queue-line { display: flex; gap: var(--sp); align-items: baseline; font-size: 14.5px; flex-wrap: wrap; }
.q-label {
  flex: none; width: 76px;
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--txt-faint);
}
.colony-card-actions { display: flex; gap: var(--sp); margin-top: auto; padding-top: var(--sp); }

/* ---- overview empire table (v0.58.2, UI-audit A5/B7) ----
   Page-scoped (`.ov-` prefix) so none of this leaks onto the shared
   .table/.mini-res/.queue-line classes used elsewhere.
   Owner: "replace the Empire overview's points, rank etc. panel style to
   fleet command's style bar" — dropped the `.panel.panel-framed` chamfer/
   fill box entirely (JS no longer applies those classes) in favor of the
   SAME flat, transparent, glowing-underline look `.tabs` (Fleet's own
   Movements/Dispatch/Farm Lists bar) already uses — same border-bottom +
   box-shadow glow, same corner-notch pseudo-elements, no filled panel
   behind the content. Chips keep their own thin vertical dividers (the
   informational content — Points/Rank/Colonies/etc — was never a set of
   clickable tabs, so `.tab`'s own hover/active states don't apply, just
   its outer bar chrome). */
.ov-stat-bar {
  display: flex; flex-wrap: wrap; align-items: center;
  position: relative;
  padding: 10px calc(var(--sp) * 1.5);
  margin-bottom: calc(var(--sp) * 2);
  border-bottom: 1px solid var(--panel-border-hi);
  box-shadow: 0 1px 6px rgba(79, 216, 255, 0.15);
}
.ov-stat-bar::before, .ov-stat-bar::after {
  content: ''; position: absolute; bottom: -1px; width: 10px; height: 6px;
  border-bottom: 1px solid var(--accent); pointer-events: none;
}
.ov-stat-bar::before { left: 0; border-left: 1px solid var(--accent); }
.ov-stat-bar::after { right: 0; border-right: 1px solid var(--accent); }
.ov-stat-chip {
  display: flex; align-items: baseline; gap: 6px;
  padding: 5px calc(var(--sp) * 1.5);
  border-right: 1px solid rgba(79, 216, 255, 0.1);
}
.ov-stat-chip:last-child { border-right: none; }
.ov-stat-chip .stat-label { font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--txt-dim); white-space: nowrap; }
.ov-stat-chip .stat-value { font-size: 15.5px; margin-top: 0; }

/* Table + Co-Pilot card sit side by side on wide screens; flex-wrap drops
   the (narrower) Co-Pilot card below the table once the row can't fit both
   — same responsive idea the old .colony-grid used, without a fixed grid
   forcing a fragile column count. */
.ov-lower { display: flex; flex-wrap: wrap; gap: calc(var(--sp) * 2); align-items: flex-start; }
/* Copilot is a small fixed-ish card (flex-grow:0 — it never claims more than
   its own basis); the table gets 100% of whatever's left (flex-grow:1),
   which matters because its `min-width: 900px` (below) needs real room to
   avoid squeezing the Actions column into an invisible-scrollbar situation.
   v0.60.13 (owner screenshot: Actions column's colony-link button clipped,
   forcing a scrollbar) — `min-width: 0` let flexbox shrink this panel BELOW
   the table's own 900px floor instead of wrapping; the table just silently
   overflowed into `.table-scroll`'s internal scrollbar on any viewport where
   table(900) + copilot(300) + gap didn't fit. Matching this panel's
   min-width to the table's real floor makes `.ov-lower`'s flex-wrap trigger
   correctly instead — the co-pilot card drops to its own full-width row
   below the table on medium viewports, and the table renders at its full
   900px with no clipped/scrolled Actions column. Side-by-side layout on
   wide screens is unchanged. */
/* Unified-shell prototype: no longer a .panel/.panel-framed box (that
   competed with #page's own new outer bezel for the same role) — just a
   thin glowing top divider, same corner-notch language .ov-stat-bar
   already established right above this section, so "Colonies" reads as
   the next readout inside the one screen rather than another framed box. */
.ov-empire-panel {
  flex: 1 1 640px; min-width: 900px;
  position: relative;
  padding-top: var(--sp);
  border-top: 1px solid var(--panel-border-hi);
}
.ov-empire-panel::before, .ov-empire-panel::after {
  content: ''; position: absolute; top: -1px; width: 10px; height: 6px;
  border-top: 1px solid var(--accent); pointer-events: none;
}
.ov-empire-panel::before { left: 0; border-left: 1px solid var(--accent); }
.ov-empire-panel::after { right: 0; border-right: 1px solid var(--accent); }
.ov-copilot-slot { flex: 0 0 300px; min-width: 260px; }

/* v0.60.14 (owner, Hungarian: "this row doesn't fit graphically at all,
   redesign it, be a creative web designer") — the Colonies list was a
   bare HTML `<table>`: flat rows, no chrome, sitting inside a page that's
   otherwise all chamfered "HOLO GUI" panels/buttons. Replaced the table
   with a CSS-grid list of individually chamfered "console entry" cards —
   one `.ov-colony-card` per colony, reusing `.panel-framed`'s own proven
   double-layer clip-path fill technique (a `::before` border layer + an
   inset `::after` fill layer, see that rule's own comment for the full
   cross-browser rationale) at `--chamfer-sm` scale instead of the full
   panel chamfer, since these rows are wide-and-short, not boxy. A colored
   status roundel (`.ov-colony-badge`) replaces the old plain "⚠" text
   badge — ⚠ amber for needsAttention, ★ cyan for the home colony, ⬡ cyan
   otherwise — so the row's left edge reads as a real status light, not
   decoration. Resource readouts (`.mini-res-cell`, shared with the older
   `.colony-grid` card view) get a pill background scoped to this list only
   so they read as stat chips instead of bare inline text. Column
   proportions mirror the old <colgroup> (15/24/17/13/12/19%) plus a new
   40px badge column up front. */
/* v0.60.14 fix: the head row and each colony card were built as
   SEPARATE `display:grid` containers, each auto-sizing its own columns
   from only its own content — the head labels and the card cells (whose
   Actions column is much wider once real buttons are in it) drifted out
   of alignment column-to-column. Fixed with subgrid: `.ov-colony-list`
   itself now owns the one real column template; the head row and every
   card set `grid-template-columns: subgrid` so they all share the exact
   same tracks (auto-sized from the WIDEST cell in each column across
   every row, same as a real <table> would do) while staying independent
   DOM elements — which is what lets each card still carry its own
   chamfer/clip-path/hover-glow. */
.ov-colony-list {
  display: grid;
  /* Owner: "the yellow alert isn't aligned well, covers the Colony name" —
     the 32px badge's own `margin-right` (tried first) turned out to be a
     dead end: a grid item's margin doesn't expand its own fixed-width
     track, so the badge simply overflowed its 40px column instead of
     pushing the name column over — measured gap stayed -1px either way.
     Widened the badge's own track instead (which actually moves the NEXT
     column's start position, giving real, not illusory, clearance) —
     40px→50px wasn't enough per a 2nd owner report ("move the Colony
     column more to the right"), bumped again to 64px for an unambiguous
     gap. */
  grid-template-columns: 64px minmax(140px, 1.15fr) minmax(210px, 1.7fr)
    minmax(120px, 1fr) minmax(100px, 0.85fr) minmax(90px, 0.8fr) auto;
  gap: 8px;
  min-width: 900px;
}
/* v0.60.15 (owner, Hungarian: "the idle label overlaps the icons on
   smaller screens") — grid items default to `min-width: auto`, which lets
   a child's INTRINSIC content width (e.g. `.ov-mini-res` forced to
   `flex-wrap: nowrap`, below) blow straight past its own track's width
   instead of shrinking/wrapping — the resource chips just overflowed
   rightward into the Build column's space, which is what actually reads
   as "idle" rendering on top of the resource icons. `min-width: 0` on
   every direct grid child is the standard fix (forces content to respect
   its track instead of dictating a wider one), paired with letting the
   resource row wrap onto a 2nd line within its own cell (below) instead
   of demanding to stay on one line. */
.ov-colony-grid-cols { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; }
.ov-colony-grid-cols > * { min-width: 0; }
.ov-colony-head-row {
  align-items: center; gap: calc(var(--sp) * 1.25);
  padding: 0 16px 4px;
  font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--txt-faint); font-weight: 600;
}
.ov-colony-head-row .r { text-align: right; }
.ov-colony-card {
  --panel-accent: var(--accent);
  --panel-border-w: 1.5px;
  --panel-fill: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg2) 92%, var(--bg0)),
    color-mix(in srgb, var(--bg1) 96%, var(--bg0))
  );
  position: relative; isolation: isolate;
  align-items: center; gap: calc(var(--sp) * 1.25);
  padding: 12px 18px;
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer-sm)) 0, 100% var(--chamfer-sm),
    100% 100%, var(--chamfer-sm) 100%, 0 calc(100% - var(--chamfer-sm))
  );
  transition: box-shadow 0.15s;
}
.ov-colony-card::before, .ov-colony-card::after {
  content: ''; position: absolute; clip-path: inherit; pointer-events: none;
}
.ov-colony-card::before { inset: 0; height: auto; background: var(--panel-accent); z-index: -2; }
.ov-colony-card::after { inset: var(--panel-border-w); background: var(--panel-fill); z-index: -1; }
.ov-colony-card:hover { box-shadow: 0 0 18px color-mix(in srgb, var(--panel-accent) 32%, transparent); }
.ov-colony-card.row-attn { --panel-accent: var(--warn); }
/* Owner: "the yellow alert isn't aligned well, covers the Colony name" —
   real bug: the badge's own 40px subgrid track and the name column sat
   with ~1px of true separation (verified via getBoundingClientRect —
   badge right edge and name-cell left edge were practically touching), so
   the 10px glow blur below bled well into the name text on top of that.
   Fixed at the track level (`.ov-colony-list`'s own grid-template-columns,
   40px→50px) rather than with margin here — a grid item's margin doesn't
   expand its own fixed track, so that was tried first and measured as a
   dead end (gap stayed -1px either way). */
.ov-colony-badge {
  width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; line-height: 1;
  background: color-mix(in srgb, var(--panel-accent) 16%, var(--bg0));
  border: 1.5px solid color-mix(in srgb, var(--panel-accent) 55%, transparent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--panel-accent) 35%, transparent);
  color: var(--panel-accent);
}
.ov-name-cell { min-width: 130px; }
.ov-name-line { display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
.colony-name-link { font-family: var(--font-display); font-size: 15.5px; font-weight: 700; letter-spacing: 0.02em; }
.attn-badge { color: var(--warn); font-size: 14px; }
/* Was `flex-wrap: nowrap` under the old fixed-percentage <table> columns,
   where the Resources column always had real committed width. The new
   grid's `minmax()` tracks can legitimately shrink narrower than that on
   smaller screens — nowrap then had nowhere to put the 4th resource chip
   except overflow rightward past its own column (see the `min-width: 0`
   comment above for the other half of this fix). Wrapping onto a 2nd
   line inside the SAME cell is what `.mini-res`'s own base rule already
   does everywhere else; this override just needs to stop opting out. */
.ov-mini-res { flex-wrap: wrap; row-gap: 4px; }
.ov-colony-card .mini-res-cell {
  padding: 3px 8px; border-radius: 999px;
  background: rgba(79, 216, 255, 0.06);
  border: 1px solid rgba(79, 216, 255, 0.14);
}
.ov-colony-card .mini-res-cell.res-warn { border-color: rgba(255, 176, 46, 0.3); background: rgba(255, 176, 46, 0.06); }
.ov-colony-card .mini-res-cell.res-danger { border-color: rgba(255, 77, 93, 0.3); background: rgba(255, 77, 93, 0.06); }
.ov-actions-cell { display: flex; gap: 6px; justify-content: flex-end; white-space: nowrap; }
/* Build/Train status cells: keep "⌘ Command Center → L14" etc. on one line —
   at the Build/Train columns' natural width the item name was wrapping
   mid-phrase onto 3 lines. The list already lives in .table-scroll, so
   letting these columns claim the width they need (and scrolling
   horizontally in the rare case that's not enough) reads far better than a
   ragged wrap. */
.ov-colony-card .queue-line { flex-wrap: nowrap; white-space: nowrap; }

/* ----------------------------------------------------------------- colony */

.rename-input { font-size: 17px; font-weight: 700; }

/* docs/art/ref-GUI/hud_components.png's "QUEUE ITEM" (icon in its own
   frame + name + progress bar + time, on a clearly bordered card) — this
   row (Colony's Construction Queue AND Shipyard's Training Queue both use
   it) had a border so faint (`0.08` alpha) it barely registered as a card
   at all. Brighter border + a left accent bar + the icon given its own
   small glowing chip (matching `.q-count-col`'s numbers staying quiet) —
   without going all the way to a full chamfer, since this repeats many
   times in a scrolling list and a chamfer-per-row would be visual noise. */
.queue-row {
  display: flex; align-items: center; gap: var(--sp);
  padding: 8px 10px; border-radius: var(--radius);
  background: rgba(5, 7, 15, 0.45);
  border: 1px solid var(--panel-border-hi);
  border-left: 2px solid var(--accent);
  margin-bottom: 6px;
}
.q-ic {
  font-size: 17px; color: var(--accent); width: 26px; height: 26px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border-radius: 6px; background: rgba(79, 216, 255, 0.08);
}
.q-name { flex: 1; min-width: 0; }
.q-count { margin-left: auto; }
.q-main { flex: 1; min-width: 0; }
.q-count-col { text-align: right; font-size: 14px; flex: none; }
.train-row .bar { margin-top: 5px; }

.unit-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: calc(var(--sp) * 2);
}
/* min-width:0 is load-bearing, not decorative: a grid item's default
   min-width is `auto` (its own content's min-content size), which lets a
   card with more/longer content (owner: the Dreadnought's aurium-tier
   badge + CAPITAL wing badge + counter-hint line + bigger stat numbers)
   force ITS OWN 1fr track wider than its siblings' — every track is
   nominally "1fr" but the one with the larger content floor wins extra
   space in the split, so cards visibly stopped matching width. This lets
   every card shrink to whatever the grid actually allocates it; the
   longer content wraps/reflows inside instead of stretching the column. */
.u-card { display: flex; flex-direction: column; gap: 6px; margin-bottom: 0; min-width: 0; }
/* align-items: center used to vertically re-center the ship icon inside
   this row every time the title block's own height changed (1 vs 2 lines
   of role text) — the icon's own top edge measurably shifted between
   17px and 23.7px depending on the unit, so the icons never lined up
   across cards even when everything else did. flex-start pins every
   icon's top edge to the row's own top, independent of how tall the
   title block next to it grows. */
.u-card-head { display: flex; align-items: flex-start; gap: var(--sp); }
.u-glyph {
  font-size: 24px; width: 38px; height: 38px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5, 7, 15, 0.6);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  color: var(--accent);
}
.b-title, .u-title { flex: 1; min-width: 0; }
.b-name, .u-name { font-weight: 700; font-size: 15.5px; }
.b-cat, .u-role { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; }
.b-level { font-size: 16px; color: var(--accent); }
.u-owned { font-size: 15px; color: var(--txt-dim); }
.b-desc { font-size: 14px; min-height: 2.6em; }
.b-cost-row { display: flex; align-items: center; gap: var(--sp); flex-wrap: wrap; }
.b-time { font-size: 13.5px; }
/* v0.57.11: owner — storage-cap buildings should say exactly how much the
   next level adds, not just its cost/time. */
.b-storage-line { font-size: 13.5px; display: flex; align-items: center; gap: 4px; margin-top: 2px; }
.b-storage-line .ic { width: 14px; height: 14px; }
.b-build-btn { margin-top: auto; }

.prod-table td { padding: 6px 10px; }

/* --------------------------------------------------------------- shipyard */

/* Owner: "itt a Dreadnought szét van csúszva a kártyán" (round 2) — every
   unit has EXACTLY 5 stats (attack/defense/speed/cargo/rations), but the
   old flex-wrap row let a card with wider numbers (the Dreadnought's
   3-digit attack/defense/cargo vs. everything else's 1-2 digits) wrap its
   5th chip onto an orphaned second line — at a narrow-enough card width,
   4 fit on row one and the 5th stranded itself alone below, while shorter-
   number cards stayed on one line right next to it. A fixed 5-column grid
   can't wrap at all: every card's stat row is always exactly one row,
   regardless of viewport width or how many digits a number has — cells
   just get tighter, they never spill to a second line. */
.u-stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
.u-stat {
  display: inline-flex; gap: 3px; align-items: center; justify-content: center;
  font-size: 13px; padding: 2px 4px;
  background: rgba(5, 7, 15, 0.5);
  border: 1px solid rgba(122, 159, 255, 0.14);
  border-radius: 4px;
  min-width: 0;
}
/* NOT text-overflow:ellipsis — a stat number is real data (owner-visible
   combat math), never something to silently hide behind "…". At the
   grid's absolute 280px floor a 3-digit stat can outgrow its 1/5 cell and
   spill past its own chip border; that's an acceptable rare edge (still
   fully readable) against actually hiding digits, which isn't. */
.u-stat .mono { white-space: nowrap; }
.u-stat-ic { color: var(--txt-faint); font-size: 13px; }
.stat-modded { border-color: rgba(255, 209, 102, 0.5); }
.stat-modded .mono, span.stat-modded { color: var(--fc-traders); }
.u-time { font-size: 13px; }

/* Owner: "a train gomb sem illeszkedik semmihez, elég kusza a panel
   kiosztás... ezek legyenek mind középre igazítva" — this used to be a
   ROW (stepper block + TRAIN button side by side, align-items:center).
   The stepper itself wraps its 6 controls onto 2 lines on most cards (the
   grid's 280px floor doesn't fit −/qty/+/+10/+100/max on one line), so
   centering TRAIN against that sibling centered it against the sibling's
   full WRAPPED height — it visibly floated between the two stepper rows,
   pinned to neither. A column instead: every row (the stepper block, the
   train button) stacks and is horizontally centered as its own unit, so
   TRAIN always sits on a clean row of its own beneath the stepper,
   whatever the stepper's own wrap state. */
.train-row-ctl { display: flex; flex-direction: column; align-items: center; gap: 6px; margin-top: auto; padding-top: 4px; }
.train-row-ctl .btn-primary { align-self: stretch; }
/* B9: stepper now also holds +10/+100 quick-amount buttons alongside −/+/max
   — wrap instead of overflow/squish on narrower cards (the grid's 280px
   minmax floor leaves too little room for six controls on one line).
   justify-content:center keeps BOTH wrapped rows centered as a block
   instead of hugging the card's left edge. */
.stepper { display: flex; align-items: center; justify-content: center; gap: 4px; flex-wrap: wrap; width: 100%; }
.qty-input { width: 76px; text-align: right; }
.qty-input.coord { width: 54px; }
.train-total-row { font-size: 14px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* The dreadnought's total-cost row carries a 4th chip (aurium) the other
   ~10 unit cards don't — scoped-tighter chips here (not the shared
   .cost-chip used game-wide) buy back the width so 4 still reads as one
   tidy row instead of the aurium chip alone dropping to its own line. */
.train-total .cost-chip { padding: 2px 6px; font-size: 13px; gap: 3px; }

/* owner (round 3): "remove that, and add a similar right side panel
   instead" — the in-page compact queue bar (`.queue-sticky`/`.queue-
   compact`, shipyard.js) is gone entirely; training + construction queue
   status now lives in a dashboard-sidebar card instead (`.sp-queue-*`
   below, command-shell.js's `paintQueues()`). Dead CSS removed with it.

/* B9: "Buildable / All" filter above the ship-class grid — same
   .map-viewseg skin as the map's 3D/Flat toggle and the colony page's
   Scene/List toggle (control language reused, not reinvented). */
.shipyard-filter-seg { margin-bottom: var(--sp); }
.shipyard-hidden-line { font-size: 14px; margin-top: var(--sp); }

input[type="number"]::-webkit-inner-spin-button { opacity: 0.4; }

/* ------------------------------------------------------------------ fleet */

/* v0.60.17 (zero-scroll pass): Movements/Dispatch/Farm Lists — one tab per
   box instead of all 3 always stacked (see fleet.js render()'s comment).
   Mirrors .dir-tabs/.colony-ops-tabs. */
.fleet-tabs { overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; margin-bottom: calc(var(--sp) * 2); }
.fleet-tabs .tab { flex: 0 0 auto; padding: 9px 16px; white-space: nowrap; }

.mission-badge { text-transform: uppercase; font-size: 14px; letter-spacing: 0.08em; }
/* docs/art/ref-GUI/hud_components.png ("STATUS BADGES": ACTIVE/IDLE/IN
   COMBAT/RETURNING as small colored dot+pill badges) — `.fleet-state` used
   to just be plain colored uppercase TEXT (`.dim` + a color override),
   the one HUD-component type from the kit this game never actually built.
   Real pill now: dot + tinted fill + border, same 3-state palette
   (--warn/--accent/--ok) the plain text already used, just given the
   kit's actual badge shape. Compound `.fleet-state.st-*` selectors (not
   bare `.st-*`) so this reliably beats the `.dim` utility class fleet.js
   still applies alongside it, regardless of stylesheet source order. */
.fleet-state {
  display: inline-flex; align-items: center; gap: 5px;
  margin-top: 3px; padding: 2px 8px 2px 6px;
  border-radius: 999px; border: 1px solid transparent;
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
}
.fleet-state::before {
  content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; box-shadow: 0 0 6px currentColor;
}
.fleet-state.st-returning { color: var(--warn); background: rgba(255, 176, 46, 0.12); border-color: rgba(255, 176, 46, 0.35); }
.fleet-state.st-outbound { color: var(--accent); background: rgba(79, 216, 255, 0.12); border-color: rgba(79, 216, 255, 0.35); }
.fleet-state.st-stationed { color: var(--ok); background: rgba(61, 220, 151, 0.12); border-color: rgba(61, 220, 151, 0.35); }
.route-cell { font-size: 14.5px; }
.route-arrow { font-size: 12px; }

/* Owner: "the target coordinates are not aligned with origin and mission
   dropdown field" — `align-items: end` bottom-aligned all 3 fields, but
   Target's own field is taller than Origin/Mission's (it has an extra
   coord-hint line below its inputs), so Origin/Mission's controls got
   pushed down past where Target's g/s/p boxes actually sit. All 3 fields
   share the same label height, so aligning by the TOP instead lines every
   control up right under its label regardless of what follows it. */
.send-grid {
  display: grid; grid-template-columns: 2fr 1.6fr 1fr;
  gap: calc(var(--sp) * 2); align-items: start;
}
.form-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.form-field select { width: 100%; }
.coord-inputs { display: flex; align-items: center; gap: 4px; }
.coord-hint { font-size: 13px; margin-top: 4px; line-height: 1.4; }
.coord-hint a { font-weight: 600; }

.unit-picker, .cargo-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: var(--sp);
}
.picker-row {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 8px;
  background: rgba(5, 7, 15, 0.4);
  border: 1px solid rgba(79, 216, 255, 0.08);
  border-radius: var(--radius);
}
.picker-name { flex: 1; min-width: 0; font-size: 14.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.picker-avail { font-size: 13px; }

.send-footer {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp);
  margin-top: calc(var(--sp) * 2); flex-wrap: wrap;
}
.cap-line { font-size: 14px; }
.send-hint { font-size: 14px; min-height: 1.2em; margin-top: 4px; }

/* ----------------------------------------------------------------- galaxy */

.galaxy-nav {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp);
  margin-bottom: calc(var(--sp) * 2); flex-wrap: wrap;
}
.gal-nav-group { display: flex; gap: 4px; }
.gal-jump { display: flex; align-items: center; gap: 6px; }
.slot-name { font-weight: 600; }
.npc-ic { color: var(--accent2); }
.slot-actions { white-space: nowrap; }
.slot-actions .btn { margin-left: 4px; }
/* Phase 10A (PHASE10-DEBRIS.md §6): the debris sub-line under a slot's
   designation — small/dim like every other secondary row detail here. */
.slot-debris-line { font-size: 12px; font-weight: 400; margin-top: 2px; }

/* ---------------------------------------------------------------- reports */

.reports-split {
  display: grid; grid-template-columns: minmax(300px, 380px) 1fr;
  gap: calc(var(--sp) * 2);
  align-items: start;
}
.reports-list-box, .reports-detail-box { margin-bottom: 0; }
.report-list { display: flex; flex-direction: column; gap: 2px; max-height: 62vh; overflow-y: auto; }
.report-item {
  display: flex; align-items: center; gap: var(--sp);
  padding: 8px 10px; border-radius: var(--radius);
  color: var(--txt-dim);
  border: 1px solid transparent;
  transition: all 0.12s;
}
.report-item:hover { background: var(--accent-soft); text-shadow: none; color: var(--txt); }
.report-item.unread { color: var(--txt); font-weight: 600; }
.report-item.unread::before {
  content: ""; width: 6px; height: 6px; flex: none;
  background: var(--accent); border-radius: 50%;
  box-shadow: 0 0 6px var(--accent);
}
.report-item.selected { border-color: var(--panel-border-hi); background: var(--accent-soft); }
.report-ic { flex: none; width: 20px; text-align: center; }
.report-ic.lg { font-size: 26px; width: 36px; }
.report-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 15px; }
.report-time { flex: none; font-size: 12.5px; }

.report-detail-head { display: flex; align-items: center; gap: var(--sp); margin-bottom: var(--sp); }
.rdh-main { flex: 1; min-width: 0; }
.rdh-main h3 { font-size: 17px; }

/* v0.60.6: "AI Supercomputer" voice HUD — a small readout that appears only
   while speechSynthesis is actually speaking a report, giving the read-aloud
   prototype a "computer is talking" presence instead of a silent button
   press. Bar heights are driven from JS (reports.js `speakReport`), not a
   fixed CSS loop, so the motion reacts to word boundaries rather than
   looping identically every time. */
.tars-voice-hud {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px; margin-bottom: var(--sp);
  background: rgba(79, 216, 255, 0.06);
  border: 1px solid rgba(79, 216, 255, 0.3);
  border-radius: 4px;
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent);
  opacity: 0; max-height: 0; overflow: hidden; padding-top: 0; padding-bottom: 0; margin-bottom: 0;
  transition: opacity 0.25s ease, max-height 0.25s ease, padding 0.25s ease, margin 0.25s ease;
}
.tars-voice-hud.active {
  opacity: 1; max-height: 40px; padding-top: 6px; padding-bottom: 6px; margin-bottom: var(--sp);
}
.tars-voice-glyph { font-size: 15px; animation: pulse-glow 1.3s ease-in-out infinite; }
.tars-voice-label { flex: none; white-space: nowrap; }
.tars-voice-bars { display: inline-flex; align-items: flex-end; gap: 2px; height: 16px; flex: 1; max-width: 160px; }
.tars-voice-bars i {
  display: block; flex: 1; height: 15%; min-height: 2px;
  background: var(--accent); border-radius: 1px;
  box-shadow: 0 0 4px rgba(79, 216, 255, 0.7);
  transition: height 110ms ease-out;
}

.report-body { display: flex; flex-direction: column; gap: var(--sp); }
.kv { display: flex; gap: var(--sp); align-items: baseline; flex-wrap: wrap; }
.kv-k {
  flex: none; width: 130px;
  font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
}
.raw-body {
  background: rgba(5, 7, 15, 0.6); padding: var(--sp); border-radius: var(--radius);
  font-size: 14px; overflow-x: auto;
}

.battle-verdict {
  text-align: center; padding: 10px;
  font-size: 16px; font-weight: 700; letter-spacing: 0.24em;
  border-radius: var(--radius);
}
.verdict-att { background: var(--danger-soft); color: var(--danger); border: 1px solid rgba(255, 77, 93, 0.35); }
.verdict-def { background: rgba(61, 220, 151, 0.08); color: var(--ok); border: 1px solid rgba(61, 220, 151, 0.3); }

.battle-sides { display: grid; grid-template-columns: 1fr 1fr; gap: calc(var(--sp) * 2); }
.battle-side {
  background: rgba(5, 7, 15, 0.4);
  border: 1px solid rgba(79, 216, 255, 0.08);
  border-radius: var(--radius);
  padding: var(--sp);
}
.side-head { display: flex; align-items: baseline; gap: var(--sp); margin-bottom: 6px; flex-wrap: wrap; }
.side-label {
  font-size: 11px; font-weight: 700; letter-spacing: 0.2em;
  color: var(--txt-faint);
}
.side-sub { font-size: 13px; margin-top: 6px; }
.forces-table td, .forces-table th { padding: 4px 8px; font-size: 14px; }
.battle-facts { display: flex; flex-direction: column; gap: 6px; }

/* ------------------------------------------------------------------- chat */

/* Owner: "kösd be az éles játékba az új GUI-t" (galaxy-map-concept mockup)
   — reuses the SAME double-layer chamfer technique as `.panel-framed`
   (--panel-accent/--panel-fill, see its own comment further up) rather
   than a parallel system, but with a custom polygon: only the top-left/
   top-right corners cut, none on the bottom — this panel is flush-docked
   to the real viewport bottom edge (`bottom: 0`, no visible bottom edge to
   even cut), unlike a free-floating panel where `.panel-framed`'s default
   2-opposite-corners cut makes sense. */
/* owner: "a chat és commander portrait még mindig a bal oldalon van [a
   referenciában]... miért nem használod a referencia kódokat?" — the
   actual source-of-truth reference isn't just the PNG kit sheets, it's
   the owner's own hand-built/edited mockup at public/dev/galaxy-map-
   concept.html/.css, which they positioned with the drag/resize editor
   themselves. That file's `.chat-dock { position:fixed; left:20px;
   bottom:11px; }` sits bottom-LEFT, right next to `.sidenav` — the live
   game had it bottom-RIGHT instead (this rule used to read `right:
   calc(var(--sp)*2)`), directly contradicting the reference. Mirrors the
   mockup's intent using the live game's own `--nav-w` token instead of a
   hardcoded pixel value (viewport sizes vary here, the mockup's canvas
   didn't). `.map-legend` (Galaxy page only) sits in this same corner and
   got bumped up to clear the now-left chat-dock — see its own comment. */
/* owner: "ugyanúgy pozícionálj minden egyes elemet... ahogy a bemásolt
   JSON fájlban kaptad" — galaxy-map-concept.css's `.chat-dock` is a
   764×321px box (the owner's own edit-mode export), noticeably wider
   than this dock's old 420px — room for the full 96px portrait + a real
   message log without feeling cramped. `min(764px, ...)` targets that
   exact width on any screen wide enough to fit it (matches the reference
   1:1 there) but clamps down gracefully on narrower ones instead of
   overflowing past the right-docked `.map-side-dash` — the same
   "kilógnak a képernyőről" overflow bug the owner's OWN edit session hit
   repeatedly (see `.chat-dock`/`.legend`/`.action-dock`'s comments in
   that file) if a fixed px width doesn't account for viewport width. */
/* owner (full rebuild): "NE a mostani megoldásokból indulj ki, írj át
   mindent olyanra, mint ami a referencia kódban szerepel... Minden egyes
   elemet pixel pontosan úgy helyezz el... A chat sem felnyitható ott,
   hanem statikus." — this whole block is a from-scratch rebuild against
   `public/dev/galaxy-map-concept.css`'s actual numbers, not an evolution
   of the previous pass. `#chat-dock` there is a fixed 764×321px box with
   NO collapse header at all (no `.chat-head`/`.collapsed`/caret — every
   trace of that mechanism, including its `#chat-head`/`.chat-head-ic`/
   `.chat-head-title`/`.chat-caret` CSS, is removed here, matching
   index.html's own rebuild of `#chat-dock` — it is now permanently open,
   same as every other fixed HUD element). `bottom:11px` (was flush `0`)
   and `height:321px` (was a variable max-height up to 420px) are the
   reference's own exact numbers. `min(764px, ...)` still clamps the
   WIDTH on narrow viewports (same overflow-safety rationale as before —
   a literal unclamped 764px is what the owner's own edit session had to
   repeatedly fix away from), since horizontal viewport width varies far
   more across real screens than this shell's vertical layout does. */
/* owner round 2: "most miért kicsinyítetted le a command dockot? nem
   kértem ilyet, az előző méret tökéletes volt. A chatet kell balrább
   helyezni, a command dock pedig tökéletesen a képernyő közepén
   helyezkedjen el" — the previous pass (below, kept for history) resolved
   the chat-dock/command-dock overlap by shrinking BOTH and repositioning
   the dock sequentially after the chat. Wrong fix: the dock goes back to
   its original 700px, TRUE center (`.map-action-dock`, see its own
   comment) — untouched by anything happening here. This element is the
   one that adapts instead: its own max-width is computed to end exactly
   at the (now fixed, centered) dock's actual left edge minus a gap, so
   overlap stays impossible by construction but the dock never has to
   shrink or move off-center to make room. `centerX - dockHalf` derives
   the dock's real left edge from ITS OWN centering formula (duplicated
   here deliberately, not shared via a CSS variable — keeping both sides
   of the boundary condition in one file each stays readable; if the
   dock's own centering formula changes, this needs the matching update). */
#chat-dock {
  /* v0.60.30 (owner: "same style as the command dock, same color
     palette") — was `var(--panel-border-hi)`, a bright semi-transparent
     cyan ring (rgba(79,216,255,0.5)) that reads as glowing/prominent next
     to the command dock's own much more muted outline. Read the ACTUAL
     colors baked into `console-dock.svg` (the command dock's own art):
     its outer body stroke is a flat, opaque, muted slate-blue (`#2D3B55`)
     — the vivid cyan (`#00E0FF`) is reserved there for small accent
     glows only (the top edge lines, the primary/active slot's own
     highlight), not the whole outline. Matching `#2D3B55` here instead of
     the bright cyan token brings the chat window down to the same
     understated register instead of visually competing with it as the
     brighter of the two docks.
     v0.60.31 (owner, screenshot: "i can tell it's not the same color for
     the panel background") — `--panel-fill` was a `color-mix(--bg2 90%,
     --bg0)` approximation, reasoned as "close enough" to the SVG's own
     `#101a26`→`#0a0f16` fill gradient by hand math (~a few RGB points
     off) — wrong call, the owner could see the real difference on
     screen even though the numbers were close. Replaced the approximation
     with the SVG's own literal hex gradient, an exact match instead of a
     nearby derived one. **Lesson: when the owner reports a color
     mismatch after I already "verified" it by hand-computing two values
     were numerically close, trust the screenshot over the math — small
     deltas that look negligible on paper (a few RGB points) can still
     read as a visible difference on an actual screen, especially between
     a flat CSS color and an image asset's own baked value. Use the
     source asset's literal value, don't approximate from nearby tokens,
     when exact-match fidelity is the actual ask.** */
  --panel-accent: #2D3B55;
  --panel-fill: linear-gradient(180deg, #101a26, #0a0f16);
  /* v0.60.30 (owner: "húzd beljebb, hogy egyvonalban legyen a felette lévő
     kártya szélével" — pull it in so it lines up with the edge of the
     card above it) — was `--nav-w + --sp*2` (16px past the sidenav), 8px
     short of `#page`'s own left padding (`--sp*3`, 24px) that the actual
     content grid above it sits inside. Matching that exactly (`--sp*3`)
     lines this dock's left edge up with the cards/panels above it instead
     of sitting 8px further left than everything else on the page. */
  position: fixed; left: calc(var(--nav-w) + var(--sp) * 3); bottom: 11px; z-index: 50;
  width: min(764px, calc((100vw + var(--nav-w) - 345px) / 2 - 350px - var(--nav-w) - var(--sp) * 4));
  height: 321px;
  isolation: isolate;
  background: transparent;
  border: none;
  /* Owner: "a frame legyen olyan mint a chat ablaké, egyesítsd a bannert
     és a chatet" (the frame should match the chat window's own style —
     merge the two panels). A LITERAL merge (banner as a 3rd column inside
     this same element) was tried first and reverted: this dock's own
     width formula above is deliberately narrow at most real desktop
     widths (see the ≤1805px fallback further down, already tuned to drop
     the portrait column once space gets tight) — stealing another ~210px
     for a banner column left `.chat-main` too narrow to hold its own tabs
     at any but the very widest viewports. Kept `#faction-banner` a
     SEPARATE element instead (butted flush against this dock, zero gap,
     identical height/baseline/colors — see its own rule below) so the two
     still read as one continuous panel without touching this dock's
     width-sensitive internal layout at all. Half of that seam-hiding trick
     lives here: the LEFT top corner is no longer chamfered (it's now an
     interior corner, not a true outer corner of the merged shape) — only
     the outer top-right corner still cuts. */
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer)) 0,
    100% var(--chamfer), 100% 100%, 0 100%
  );
  box-shadow: 0 -4px 30px rgba(0, 0, 0, 0.5);
}
#chat-dock::before, #chat-dock::after {
  content: ''; position: absolute; clip-path: inherit; pointer-events: none;
}
#chat-dock::before { inset: 0; height: auto; background: var(--panel-accent); z-index: -2; }
/* The other half of the seam-hiding trick: skip the LEFT inset so the fill
   runs flush to x=0 — no border sliver on the edge #faction-banner butts
   up against (still bordered on top/right/bottom, same as any panel). */
#chat-dock::after { inset: 1.5px 1.5px 1.5px 0; background: var(--panel-fill); z-index: -1; }

/* owner: "bal alul a chat mellé és a bal nav alá ... az adott banner
   zászlóját ... dinamikusan változzon fajonként", then "a frame legyen
   olyan mint a chat ablaké, egyesítsd a bannert és a chatet" — same
   #2D3B55/fill as #chat-dock right above (copy, not a var reference, so
   this box still has its own colors if the dock's ever change for an
   unrelated reason) and butted flush against its left edge (zero gap:
   `left + width` here lands exactly on `#chat-dock`'s own `left`) with a
   matching height/baseline, chamfer only on the OUTER top-left corner, and
   no border on the touching right edge (mirrors #chat-dock's own skipped
   LEFT inset) — the two elements paint as one continuous panel with a
   single seamless border around the outside, nothing doubled where they
   meet. Width = `--nav-w + --sp*2`, the exact gap between the sidenav's
   own inset and #chat-dock's (unmoved) left edge. */
#faction-banner {
  --panel-accent: #2D3B55;
  --panel-fill: linear-gradient(180deg, #101a26, #0a0f16);
  position: fixed; left: var(--sp); bottom: 11px; z-index: 50;
  width: calc(var(--nav-w) + var(--sp) * 2); height: 321px;
  isolation: isolate;
  background: transparent;
  border: none;
  clip-path: polygon(
    var(--chamfer) 0, 100% 0,
    100% 100%, 0 100%, 0 var(--chamfer)
  );
  box-shadow: 0 -4px 30px rgba(0, 0, 0, 0.5);
}
#faction-banner::before, #faction-banner::after {
  content: ''; position: absolute; clip-path: inherit; pointer-events: none;
}
#faction-banner::before { inset: 0; background: var(--panel-accent); z-index: -2; }
#faction-banner::after { inset: 1.5px 0 1.5px 1.5px; background: var(--panel-fill); z-index: -1; }
#faction-banner-img {
  position: absolute; inset: 1.5px 0 1.5px 1.5px;
  width: calc(100% - 1.5px); height: calc(100% - 3px);
  object-fit: cover; object-position: center top;
}

/* galaxy-map-concept.css's `.chat-portrait-col`/`.chat-main`: independent
   boxes positioned by their own exact offset within `.chat-dock` (25,73
   and 199,10 there, converted from that file's page-absolute coordinates
   to dock-relative ones — page-absolute numbers only mean anything at the
   ONE edit-session window size they were captured at, dock-relative
   offsets render identically at any size, same fix already applied to
   `#chat-dock` itself vs. the page). `.chat-main` uses `right`/`bottom`
   instead of a literal width/height so it fills whatever the (possibly
   viewport-clamped) dock width actually is instead of a fixed 537px that
   would either overflow or leave a gap. */
/* v0.60.29 (owner: "the commander card should fill out the whole empty
   area near the chat section") — this box only ever used 197px of
   #chat-dock's real 321px height, leaving ~124px of dead space above and
   below it. Width can't grow much (.chat-main starts at left:199px, and
   the owner didn't ask to crowd the message log to make room), so the
   real growth is vertical: 291px tall now, using nearly the dock's full
   height minus small top/bottom margins. */
.chat-portrait-col {
  position: absolute; left: 20px; top: 15px; width: 170px; height: 291px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  overflow: hidden;
  isolation: isolate;
}
/* Owner: "generate a player card image frame with higgsfield to fill out
   the dead space on the chat portrait panel, make sure the portrait part
   is transparent" — decorative border art (Higgsfield recraft_v4_1,
   generated on a flat chroma-key green background, alpha-cut via the
   project's own floodalpha.mjs keyed on #00FF00 with extra seeds for the
   portrait hole + both mini-action button holes, all disconnected regions)
   sitting BEHIND the real portrait/buttons — `position:absolute` pulls it
   out of the column's own flex flow (so it doesn't add a 3rd stacked flex
   item), `z-index:-1` + the parent's new `isolation:isolate` above keeps
   it behind its siblings without falling behind the whole page. */
.chat-portrait-frame-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* v0.60.29: was object-fit:fill, safe only because the column's own
     aspect ratio (166:197) was already close to the frame art's native
     one (400:514). Now that the column is much taller (170:291), `fill`
     would stretch the art's circle into a visible ellipse — `contain`
     preserves the frame's real proportions and letterboxes (centers)
     within the taller box instead. The real portrait `<img>` below is
     centered by the SAME box via this column's own flex centering, so it
     still lines up with the frame's circle regardless of the letterbox
     gap either fit mode would produce. */
  object-fit: contain; z-index: -1; pointer-events: none;
}
.chat-dock-portrait {
  width: 110px; height: 110px; border-radius: 50%; object-fit: cover;
  border: 2.5px solid var(--accent);
  box-shadow: 0 0 18px rgba(79, 216, 255, 0.45), 0 0 0 5px rgba(79, 216, 255, 0.08);
}
.chat-mini-actions { display: flex; gap: 8px; }
.chat-mini-actions button {
  width: 29px; height: 29px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.04); border: 1px solid var(--panel-border-hi);
  color: var(--accent); font-size: 14px; line-height: 1; cursor: pointer;
  transition: background 0.15s, box-shadow 0.15s;
}
.chat-mini-actions button:hover { background: rgba(79, 216, 255, 0.15); box-shadow: 0 0 8px rgba(79, 216, 255, 0.35); }
.chat-main {
  position: absolute; left: 199px; top: 10px; right: 14px; bottom: 11px;
  min-width: 0; display: flex; flex-direction: column; gap: 8px; overflow: hidden;
}
#chat-msgs {
  flex: 1; overflow-y: auto; min-height: 0;
  padding: 4px 0;
  display: flex; flex-direction: column; gap: 5px;
}
.chat-msg { font-size: 14.5px; line-height: 1.35; word-break: break-word; }
.chat-time { font-size: 11.5px; margin-right: 6px; }
.chat-user { font-weight: 700; margin-right: 6px; }
.chat-text { color: var(--txt); }

/* Real client-side search (owner: "a nem létező funckiókat pedig
   lefejleszted és összekötöd" — the reference's tab-row search icon had
   no behavior at all in the mockup; wired to an actual filter over the
   currently-loaded channel history since no server-side search endpoint
   exists to build a deeper one against). Hidden by default, toggled by
   `#chat-search-btn` in main.js. */
#chat-search-input {
  margin-bottom: 2px; padding: 5px 9px; font-size: 14px;
}
#chat-search-input.hidden { display: none; }

.chat-input-row {
  display: flex; gap: 6px; padding-top: 6px;
  border-top: 1px solid var(--panel-border); flex: none;
}
#chat-input { flex: 1; }

/* ============================================================== PHASE 1 */

/* ---- alliance tag chip ---- */
.atag {
  display: inline-block; padding: 0 5px;
  font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
  color: var(--accent2);
  background: rgba(122, 159, 255, 0.10);
  border: 1px solid rgba(122, 159, 255, 0.30);
  border-radius: 3px;
  vertical-align: baseline;
}
.atag-lg { font-size: 17px; padding: 2px 9px; }
.atag-input { text-transform: uppercase; }

/* ---- emergency shield / loyalty (colony page) ---- */
.tag-shield { background: rgba(61, 220, 151, 0.14); color: var(--ok); margin-left: 0; }
.tag-cooldown { background: rgba(255, 176, 46, 0.12); color: var(--warn); margin-left: 0; }
.shield-row { display: flex; align-items: center; gap: calc(var(--sp) * 1.5); flex-wrap: wrap; padding: 4px 0; }
.shield-meta { font-size: 13.5px; }
.shield-note { font-size: 13.5px; padding: 4px 0; }
.loyalty-row { display: flex; align-items: center; gap: var(--sp); margin-top: var(--sp); }
.loyalty-label { font-size: 13px; font-weight: 700; letter-spacing: 0.1em; white-space: nowrap; }
.loyalty-bar { flex: 1; height: 6px; background: rgba(255, 176, 46, 0.12); margin-top: 0; }
.loyalty-fill { background: linear-gradient(90deg, #b97b16, var(--warn)); }

/* ---- alliance page ---- */
.alliance-head { display: flex; flex-direction: column; gap: var(--sp); }
.alliance-title { display: flex; align-items: center; gap: var(--sp); flex-wrap: wrap; }
.alliance-name { font-size: 19px; font-weight: 700; letter-spacing: 0.06em; }
.alliance-stats { margin-bottom: 0; }
.alliance-desc { white-space: pre-wrap; padding: 4px 0; word-break: break-word; }
.desc-edit { width: 100%; resize: vertical; background: rgba(5, 7, 15, 0.7);
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  color: var(--txt); padding: 8px; font-family: inherit; font-size: 15px; }
.desc-edit:focus { border-color: var(--panel-border-hi); outline: none; }
.desc-edit-actions { display: flex; gap: var(--sp); margin-top: 6px; }
/* Owner: "promote to join our discord to find an alliance" — un-allied
   view only (alliance.js). Reuses .panel-framed's own accent/fill, tinted
   toward Discord's blurple via a local --panel-accent override, same
   pattern .hud-res-cell.res-danger/.res-warn already use for a one-off
   accent color on an otherwise-shared chamfer component. */
.discord-promo {
  --panel-accent: rgba(88, 101, 242, 0.55);
  display: flex; align-items: center; gap: calc(var(--sp) * 2);
  flex-wrap: wrap; margin-bottom: calc(var(--sp) * 2);
}
.discord-promo-ic {
  flex: none; font-size: 28px; line-height: 1; color: #5865F2;
  text-shadow: 0 0 12px rgba(88, 101, 242, 0.65);
}
.discord-promo-body { flex: 1; min-width: 200px; }
.discord-promo-title { font-weight: 700; letter-spacing: 0.04em; margin-bottom: 2px; }
.invite-row { display: flex; align-items: center; gap: var(--sp); flex-wrap: wrap; margin-bottom: var(--sp); }
.danger-zone-row { display: flex; align-items: center; gap: calc(var(--sp) * 2); flex-wrap: wrap; }
.alliance-create-grid { grid-template-columns: 1fr 2fr 2fr; }
/* Owner: "a resource kijelző majdnem rálóg a TAG inputra" — .send-grid has
   no bottom margin of its own (other panels follow it with a hint/button,
   not another dense row), so the founding-cost chips landed flush against
   the field row with 0px gap. Scoped to the sibling that actually follows
   this specific grid rather than touching .b-cost-row or .send-grid
   globally. */
.alliance-create-grid + .b-cost-row { margin-top: 10px; }

.role-badge {
  display: inline-block; padding: 1px 7px;
  font-size: 12px; font-weight: 700; letter-spacing: 0.12em;
  border-radius: 3px;
}
.role-leader { background: rgba(255, 209, 102, 0.14); color: var(--fc-traders); }
.role-officer { background: rgba(79, 216, 255, 0.12); color: var(--accent); }
.role-member { background: rgba(125, 147, 173, 0.10); color: var(--txt-dim); }

/* armed (two-step confirm) buttons */
.btn.armed {
  color: var(--danger) !important;
  border-color: rgba(255, 77, 93, 0.6) !important;
  background: var(--danger-soft) !important;
  animation: banner-throb 1.2s ease-in-out infinite;
}

/* ---- market page ---- */
.market-cols { display: grid; grid-template-columns: 1fr 1.2fr; gap: calc(var(--sp) * 2); align-items: start; }
.market-cols > div > .panel { margin-bottom: 0; }
.filter-row { display: flex; align-items: center; gap: var(--sp); flex-wrap: wrap; margin-bottom: var(--sp); }
.market-table .cost-chip { white-space: nowrap; }
.offer-expand td { background: rgba(79, 216, 255, 0.05); border-left: 2px solid var(--accent); }
.accept-row { display: flex; align-items: center; gap: var(--sp); flex-wrap: wrap; }
.accept-note { font-size: 13.5px; }
.accept-warn { font-size: 14px; padding-top: 4px; }
.offer-grid { display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--sp); align-items: end; margin-top: var(--sp); }
.offer-arrow { font-size: 20px; padding-bottom: 6px; }
.amount-input { width: 100%; margin-top: 4px; }
.ratio-line { margin-top: var(--sp); font-size: 14.5px; display: flex; align-items: center; gap: 6px; }
.ratio-badge { padding: 1px 8px; border-radius: 3px; font-weight: 700; }
.ratio-ok { color: var(--ok); background: rgba(61, 220, 151, 0.10); border: 1px solid rgba(61, 220, 151, 0.35); }
.ratio-bad { color: var(--danger); background: var(--danger-soft); border: 1px solid rgba(255, 77, 93, 0.45); }
.escrow-note { font-size: 13.5px; margin-top: var(--sp); }

/* ---- season page ---- */
.victory-banner {
  --panel-accent: rgba(255, 209, 102, 0.45);
  --panel-fill: color-mix(in srgb, rgb(38, 31, 10) 60%, var(--bg0));
}
.victory-line { font-size: 17px; font-weight: 700; letter-spacing: 0.08em; margin-bottom: 4px; }
.tier-callout { display: flex; align-items: center; gap: calc(var(--sp) * 2); --panel-accent: var(--panel-border-hi); }
.tier-big { font-size: 24px; font-weight: 700; color: var(--accent); text-shadow: 0 0 14px rgba(79, 216, 255, 0.5); }
.tier-callout.tier-1 .tier-big { color: #ffd166; text-shadow: 0 0 14px rgba(255, 209, 102, 0.6); }
.tier-chip { display: inline-block; padding: 1px 7px; border-radius: 3px; font-weight: 700; font-size: 13px;
  background: rgba(122, 159, 255, 0.12); color: var(--accent2); }
.tier-chip.tier-1 { background: rgba(255, 209, 102, 0.16); color: var(--fc-traders); }
.tier-chip.tier-2 { background: rgba(205, 215, 228, 0.14); color: #cdd7e4; }
.tier-chip.tier-3 { background: rgba(209, 145, 90, 0.16); color: #d1915a; }

.race-row {
  display: flex; align-items: center; gap: var(--sp);
  padding: 8px; border-radius: var(--radius);
  background: rgba(5, 7, 15, 0.4);
  border: 1px solid rgba(79, 216, 255, 0.08);
  margin-bottom: 6px;
}
.race-row.race-mine { border-color: var(--panel-border-hi); box-shadow: inset 2px 0 0 var(--accent); }
.race-rank { flex: none; width: 34px; text-align: right; }
.race-main { flex: 1; min-width: 0; }
.race-title { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.race-name { font-weight: 600; }
.race-bar { height: 8px; margin-top: 5px; background: rgba(122, 159, 255, 0.10); }
.race-fill { background: linear-gradient(90deg, var(--accent2), #c58bff); transition: width 0.5s; }
.race-level { flex: none; text-align: right; font-size: 14.5px; }
.race-eta { font-size: 13.5px; }
.results-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: calc(var(--sp) * 2); }
.results-cols .panel { margin-bottom: 0; }
.mega-req { margin: 0 3px; }

/* ---- overview banners ---- */
a.season-banner {
  display: flex; align-items: center; gap: var(--sp);
  color: var(--fc-traders);
  border-color: rgba(255, 209, 102, 0.45);
  font-size: 14.5px; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700;
}
a.season-banner:hover { text-shadow: 0 0 10px rgba(255, 209, 102, 0.5); }
.season-banner-ic { font-size: 20px; }
a.defense-alert {
  display: flex; align-items: center; gap: var(--sp);
  color: var(--warn); font-size: 14.5px; font-weight: 600;
}
a.defense-alert:hover { text-shadow: 0 0 10px rgba(255, 176, 46, 0.5); }

/* ---- reports ---- */
.verdict-shield { background: rgba(61, 220, 151, 0.08); color: var(--ok); border: 1px solid rgba(61, 220, 151, 0.3); }
.verdict-season { background: rgba(255, 209, 102, 0.10); color: var(--fc-traders); border: 1px solid rgba(255, 209, 102, 0.35); }
.report-cta { align-self: flex-start; }

/* ---- comms channel tabs ----
   galaxy-map-concept.css's `.chat-tab`: compact rounded PILLS in a row
   (content-width, `border-radius: pill`), not a full-width flex:1
   underline-tab strip — the one other structural piece of the reference
   chat panel this pass hadn't matched yet. */
/* Precise viewport-normalized remeasurement (getBoundingClientRect, both
   pages at the SAME 1920×1250 window — the concept mockup's own
   page-absolute child coordinates only render correctly at roughly that
   size, see the chat-portrait-col/chat-main comment above) found this
   was the one real gap left in the internal chat layout: reference's
   `.chat-tabs` is 43.7% of the dock's width (an explicit 334px, narrower
   than the log/input below it); this was stretching to the same 72% as
   `.chat-main` itself (the log/input's actual width). `max-width` caps it
   back to the narrower reference proportion without needing a literal px
   value that would only be exactly right at the reference's own width. */
.chat-tabs { display: flex; align-items: center; gap: 5px; max-width: 61%; padding-bottom: 8px; border-bottom: 1px solid var(--panel-border); }
.chat-tab {
  flex: 0 0 auto; padding: 5px 12px; position: relative;
  background: transparent; border: 1px solid transparent; border-radius: 999px;
  cursor: pointer; text-decoration: none;
  color: var(--txt-dim); font-family: inherit;
  font-size: 12px; font-weight: 700; letter-spacing: 0.14em;
  transition: all 0.15s;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
.chat-tab:hover { color: var(--txt); }
.chat-tab.active {
  color: var(--accent); background: var(--accent-soft);
  border-color: var(--panel-border-hi); box-shadow: 0 0 10px rgba(79, 216, 255, 0.2);
}
/* galaxy-map-concept.css's `.sq-btn` — borderless small icon-only button
   (search icon in the chat-tabs row here; the map toolbar's expand/
   settings icons already use plain unstyled buttons of their own, this is
   scoped to just `.chat-tabs` so it can't collide with anything else). */
.chat-tabs .sq-btn {
  margin-left: auto; width: 24px; height: 24px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--txt-dim); cursor: pointer;
  font-size: 15px; transition: color 0.15s;
}
.chat-tabs .sq-btn:hover { color: var(--accent); }
.chat-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--danger); box-shadow: 0 0 6px var(--danger);
}
.chat-empty { padding: var(--sp); text-align: center; font-size: 14px; }

/* ---- leaderboard tabs ---- */
.lb-tabs { margin-bottom: calc(var(--sp) * 2); border-bottom: 1px solid var(--panel-border); }
.lb-tabs .tab { flex: 0 0 auto; padding: 9px 20px; }

/* Owner: "make everything 1 page size and remove the possibility to scroll
   down... super compact" — zero-scroll pass (docs/PHASE6-UI-CHROME.md §30).
   #page's own bottom chrome reserve (--chat-dock-h, 332px) plus its top
   padding eats roughly half of a typical viewport's height before any real
   content starts, so a full PAGE of rows (leaderboard.js's own PAGE const,
   already reduced) still needs every other bit of chrome on this page
   trimmed too. Scoped to `.lb-page` (this page's own wrapper) rather than
   the shared `.page-head`/`.panel`/`.sec-h`/`.table` rules, so this doesn't
   shrink headers/tables on every other page that hasn't been through this
   same pass yet. */
.lb-page .page-head { margin-bottom: 4px; }
.lb-page .lb-tabs { margin-bottom: 4px; }
.lb-page .lb-tabs .tab { padding: 4px 14px; }
.lb-page .panel { padding: 4px; margin-bottom: 0; }
/* Real bug (owner screenshot: "the panels slides into each other") — the
   section-header strip's `-24px` negative margin (`.panel.panel-framed >
   .sec-h:first-child`, tuned to exactly cancel `.panel-framed`'s own 24px
   padding and sit flush against the panel's top edge) was hardcoded against
   THAT 24px, not `.lb-page .panel`'s reduced 8px above — so the header kept
   pulling itself up by the OLD amount and slid out of the now-smaller box,
   overlapping the tab bar above it. Same bug class + same fix shape
   Directives' own zero-scroll pass already hit (`.dir-tier` + its own
   scoped `.panel.dir-tier.panel-framed > .sec-h.dir-tier-head:first-child`
   override) — match the negative margin to the new padding, don't just
   shrink the padding and leave the offset stale. Extra `.lb-page` prefix
   is required to out-specificity the base (unscoped) rule. */
.lb-page .panel.panel-framed > .sec-h:first-child { margin: -4px -4px 4px; padding: 3px 10px 3px calc(10px + var(--chamfer)); }
.lb-page .sec-h { margin-bottom: 3px; }
.lb-page .lb-table { font-size: 13px; }
.lb-page .lb-table th { padding: 2px 8px; }
.lb-page .lb-table td { padding: 2px 8px; }
.lb-page .pager { padding-top: 0; }
/* Owner: "the ranking list per page is too short, should be 25/page at
   least" — restored PAGE to 25 (see leaderboard.js's own comment) and,
   instead of shrinking row count to fit, split the table into 2 side-by-
   side mini-tables via `lbColumns()` so a full page of rows fits within
   the same scarce vertical budget by using the viewport's WIDTH instead —
   same idea as Colony's `.colony-list-row`. */
.lb-cols { display: flex; gap: var(--sp); align-items: flex-start; }
.lb-cols .lb-col { flex: 1 1 0; min-width: 0; }
.lb-cols .lb-table { min-width: 0; }

/* ============================================================ GO-LIVE v0.3 */

/* ---- username profile links: visually subtle, dim underline on hover ---- */
.ulink { color: inherit; }
.ulink:hover {
  color: var(--accent);
  text-shadow: none;
  text-decoration: underline dotted rgba(79, 216, 255, 0.55);
  text-underline-offset: 2px;
}

/* ---- SYSTEM chat line (admin announce) — distinct amber, no profile link */
.chat-msg.chat-system {
  background: rgba(255, 176, 46, 0.08);
  border-left: 2px solid var(--warn);
  padding: 3px 6px;
  border-radius: 3px;
}
.chat-system-user { color: var(--warn); font-weight: 700; letter-spacing: 0.08em; margin-right: 6px; }
.chat-msg.chat-system .chat-text { color: var(--warn); }

/* ---- directives page ---- */
.dir-note { font-size: 14px; margin: calc(var(--sp) * -1) 0 0; }
.dir-blurb { font-size: 12px; letter-spacing: 0.04em; text-transform: none; font-weight: 400; }
.dir-tier-done { opacity: 0.82; }

/* v0.60.17: .dir-tier reuses .panel-framed's 24px padding (tuned for boxy
   stat panels game-wide) — scoped down here since a tier's own tab is the
   ONLY thing sharing the viewport with the tab bar/stat-strip above it;
   shrinking the shared class globally would touch every other panel in
   the game for a Directives-only fit problem. The chamfered title-strip
   header (`.panel.panel-framed > .sec-h:first-child`, style.css:918)
   bleeds itself back out to the panel's OUTER edge via a negative margin
   sized to match that 24px — shrinking the panel's own padding without
   correspondingly shrinking that margin makes the header bleed PAST the
   panel's edge (caught live: header rect measured outside the panel
   rect on all 3 sides). `.dir-tier-head` needs `.sec-h` on the same
   element (quests.js) to out-specificity the base rule (5 classes +
   :first-child vs. its 3 classes) rather than losing silently to it. */
.dir-tier { padding: 12px; margin-bottom: 4px; }
.panel.dir-tier.panel-framed > .sec-h.dir-tier-head:first-child { margin: -12px -12px 2px; }
.dir-tier-body { padding-top: 1px; }

/* v0.60.17 (zero-scroll pass): TODAY'S ORDERS + one tab per tier, instead
   of every tier stacking open at once (see quests.js paint()'s comment
   for the ~3000px-overflow root cause) — mirrors .colony-ops-tabs, which
   proved this same nowrap+horizontal-scroll treatment for a tab bar with
   more entries than comfortably fit one row. */
.dir-tabs { overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; margin-bottom: 4px; }
.dir-tabs .tab { flex: 0 0 auto; padding: 9px 16px; white-space: nowrap; }

.dir-row {
  display: flex; align-items: flex-start; gap: var(--sp);
  padding: 6px 8px;
  border-radius: var(--radius);
  border: 1px solid rgba(79, 216, 255, 0.07);
  background: rgba(5, 7, 15, 0.4);
  margin-bottom: 2px;
}
.dir-glyph {
  flex: none; width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-size: 15px;
  border: 1px solid var(--panel-border);
  color: var(--txt-faint);
  background: rgba(5, 7, 15, 0.6);
}
.dir-main { flex: 1; min-width: 0; }
.dir-title { font-weight: 700; font-size: 15.5px; }
/* v0.60.17: single-line clamp (was 2-3 wrapped lines) — full text still
   reaches the player via the native title tooltip (quests.js questRow())
   — part of compacting each row enough that a tier's worst case (6
   objectives) fits one tab without scrolling. */
.dir-text {
  font-size: 14px; margin-top: 2px; max-width: 640px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dir-reward { display: flex; align-items: center; gap: var(--sp); margin-top: 2px; flex-wrap: wrap; }
.dir-reward-label {
  font-size: 11px; font-weight: 700; letter-spacing: 0.18em;
  color: var(--txt-faint);
}
.dir-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.dir-claim { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.dir-colony-sel { max-width: 200px; font-size: 14px; }
/* Same dot+pill badge language as `.fleet-state` (hud_components.png's
   "STATUS BADGES") — CLAIMED/in-progress were plain colored text before,
   same gap as fleet state had. */
.dir-claimed-line, .dir-progress-line {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px 2px 6px; border-radius: 999px; border: 1px solid transparent;
  font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
}
/* Only `.dir-progress-line` gets the dot — `.dir-claimed-line`'s own text
   already bakes in a ✓ glyph (quests.js), a second dot next to it would
   just be redundant clutter. */
.dir-progress-line::before {
  content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; box-shadow: 0 0 6px currentColor;
}
.dir-claimed-line { color: var(--ok); background: rgba(61, 220, 151, 0.12); border-color: rgba(61, 220, 151, 0.35); }
.dir-claimed-time { font-size: 12px; }
.dir-progress-line { color: var(--txt-dim); background: rgba(138, 162, 184, 0.1); border-color: var(--panel-border); }
.dir-next-tag { margin-left: 8px; }

/* locked-look for incomplete directives */
.dir-locked { opacity: 0.62; }
.dir-locked .dir-title { color: var(--txt-dim); }
/* v0.60.17: only the single globally-highlighted "next" objective keeps
   its flavor text — every other row (locked OR claimable-but-not-next)
   hides it, same idea as .dir-done's text already being hidden. Caps a
   tier's worst case at exactly ONE text-bearing row (there's only ever
   one "next" quest across the whole catalog) instead of "however many
   happen to be claimable at once" — needed for a 6-objective tier to
   provably fit one tab with zero scroll in every state, not just the
   common one. */
.dir-row:not(.dir-next) .dir-text { display: none; }

/* completed-unclaimed: the dopamine row */
.dir-claimable {
  border-color: rgba(79, 216, 255, 0.35);
  background: rgba(79, 216, 255, 0.06);
}
.dir-claimable .dir-glyph {
  color: var(--accent); border-color: var(--panel-border-hi);
  box-shadow: 0 0 10px rgba(79, 216, 255, 0.35);
}

/* claimed: done styling */
.dir-done .dir-glyph { color: var(--ok); border-color: rgba(61, 220, 151, 0.4); }
.dir-done .dir-title { color: var(--txt-dim); }
.dir-done .dir-text { display: none; }

/* the next unclaimed directive, highlighted */
.dir-next { box-shadow: inset 2px 0 0 var(--accent); }

/* overview hint card. Owner: "hiányos a frame/overlay formázása" (its
   frame looks incomplete) — it isn't structurally (same --panel-fill/
   --panel-accent/clip-path/box-shadow as every other .panel-framed, byte-
   for-byte verified against .stat), but at this shape (very wide, only
   ~70px tall — padding eats most of the height, so the chamfer cut reads
   as a barely-there diagonal) and now that --panel-fill is a deep navy
   close in luminosity to the page background behind it (v0.57.4, a
   deliberate "blend into the universe" choice), the border genuinely IS
   hard to see except where it happens to sit against something brighter.
   It's also the one call-to-action link on the page — extra presence is
   warranted anyway, not just a contrast patch. */
a.directive-hint {
  display: flex; align-items: center; gap: var(--sp);
  color: var(--accent); font-size: 14.5px; font-weight: 600;
  letter-spacing: 0.06em;
  box-shadow: 0 0 28px rgba(79, 216, 255, 0.4), var(--shadow-panel);
}
a.directive-hint:hover { text-shadow: 0 0 10px rgba(79, 216, 255, 0.5); }
a.directive-hint .dh-ic { font-size: 18px; }

/* Grid children default to min-width:auto, so nowrap/ellipsis rows inside
   them force the column wider than the viewport on narrow screens. Let the
   split/grid panels actually shrink (fixes reports + comms at ≤ 400px). */
.reports-split > *, .comms-split > *, .market-cols > *,
.results-cols > *, .battle-sides > * { min-width: 0; }

/* ---- comms (direct messages) page ---- */
.comms-split {
  display: grid; grid-template-columns: minmax(300px, 400px) 1fr;
  gap: calc(var(--sp) * 2);
  align-items: start;
}
.comms-list-box, .comms-detail-box { margin-bottom: 0; }
.comms-tabs { margin: calc(var(--sp) * -2) calc(var(--sp) * -2) var(--sp); }
.comms-tabs .tab { padding: 10px; }

.dm-list { display: flex; flex-direction: column; gap: 2px; max-height: 60vh; overflow-y: auto; }
.dm-item {
  display: flex; align-items: flex-start; gap: var(--sp);
  padding: 8px 10px; border-radius: var(--radius);
  color: var(--txt-dim);
  border: 1px solid transparent;
  cursor: pointer;
  transition: all 0.12s;
}
.dm-item:hover { background: var(--accent-soft); color: var(--txt); }
.dm-item.selected { border-color: var(--panel-border-hi); background: var(--accent-soft); }
.dm-item.unread { color: var(--txt); }
.dm-item.unread .dm-subject { font-weight: 700; }
.dm-item.unread .dm-ic { color: var(--accent); text-shadow: 0 0 6px var(--accent); }
.dm-ic { flex: none; width: 18px; text-align: center; padding-top: 1px; }
.dm-main { flex: 1; min-width: 0; }
.dm-line1 { display: flex; justify-content: space-between; gap: var(--sp); align-items: baseline; }
.dm-user { font-weight: 600; font-size: 14.5px; }
.dm-time { font-size: 12px; flex: none; }
.dm-subject {
  font-size: 15px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dm-preview {
  font-size: 13.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dm-del { flex: none; }

.dm-detail-head { display: flex; align-items: flex-start; gap: var(--sp); margin-bottom: var(--sp); }
.dm-detail-subject { font-size: 17px; word-break: break-word; }
.dm-detail-meta { font-size: 14px; }
.dm-detail-actions { display: flex; gap: 6px; flex: none; }
.dm-text {
  white-space: pre-wrap; word-break: break-word;
  background: rgba(5, 7, 15, 0.45);
  border: 1px solid rgba(79, 216, 255, 0.08);
  border-radius: var(--radius);
  padding: calc(var(--sp) * 1.5);
  font-size: 15.5px;
  line-height: 1.55;
}

.dm-compose { display: flex; flex-direction: column; gap: var(--sp); }
.dm-compose-text {
  width: 100%; resize: vertical;
  background: rgba(5, 7, 15, 0.7);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  color: var(--txt);
  padding: 8px; font-family: inherit; font-size: 15px;
}
.dm-compose-text:focus { border-color: var(--panel-border-hi); outline: none; }
.dm-compose-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp); margin-top: 6px; }
.dm-counter { font-size: 13px; }

/* ---- player profile page ---- */
.player-head { display: flex; align-items: flex-start; gap: calc(var(--sp) * 2); flex-wrap: wrap; }
.player-glyph {
  font-size: 32px; width: 58px; height: 58px; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5, 7, 15, 0.6);
  border: 1px solid var(--panel-border-hi);
  border-radius: var(--radius);
}
.player-id { flex: 1; min-width: 200px; display: flex; flex-direction: column; gap: 5px; }
.player-name { font-size: 21px; font-weight: 700; letter-spacing: 0.06em; word-break: break-word; }
.player-sub { display: flex; align-items: center; gap: var(--sp); flex-wrap: wrap; }
.player-alliance { font-size: 15px; }
.player-since { font-size: 13.5px; }
.player-actions { flex: none; }

/* -------------------------------------------------------------- responsive */

@media (max-width: 1280px) {
  :root { --nav-w: 56px; }
  .nav-txt { display: none; }
  .nav-link { justify-content: center; padding: 10px 0; }
  .nav-ic { width: auto; font-size: 19px; }
  .nav-badge { position: absolute; top: 2px; right: 4px; margin: 0; }
  .hud-res-cell { min-width: 100px; }
  .hud-title { display: none; }
  /* C10/C12: in the icon-only rail the colony chips center (they're already
     icon-sized — a 44px content column fits one 26px chip per row) and the
     shortcut hint's text goes the way of .nav-txt. */
  .nav-colonies { justify-content: center; }
  .nav-hint-txt { display: none; }
}

/* owner: "a command dock pedig tökéletesen a képernyő közepén helyezkedjen
   el" — `#chat-dock`'s own width now shrinks to whatever clears the
   command-dock's TRUE-centered, fixed 700px box (see both rules' own
   comments) — below roughly 1805px viewport that computed width drops
   under ~250px, too narrow for the portrait column (191px alone) to fit
   next to any usable chat-main width. Same portrait-drop fallback the
   ≤1120px tier below already uses, just at a wider threshold — the two
   overlap harmlessly (portrait already hidden well before 1120px too). */
@media (max-width: 1805px) {
  .chat-portrait-col { display: none; }
  .chat-main { left: 14px; }
}

@media (max-width: 1120px) {
  .reports-split { grid-template-columns: 1fr; }
  .comms-split { grid-template-columns: 1fr; }
  .battle-sides { grid-template-columns: 1fr; }
  .send-grid { grid-template-columns: 1fr; }
  .hud-stat { display: none; }
  .hud-res-cell { min-width: 92px; }
  .hud-res-rate { display: none; }
  #chat-dock { width: 300px; }
  .market-cols { grid-template-columns: 1fr; }
  .alliance-create-grid { grid-template-columns: 1fr; }
  .offer-grid { grid-template-columns: 1fr; }
  .offer-arrow { display: none; }
}

/* ---- mobile pass to ~768px (GOLIVE §4): nav is already an icon rail from
   1280px down; here the HUD wraps to two rows (row 1 brand/colony/user,
   row 2 resources), grids collapse to one column, and the chat dock spans
   the viewport. Wide tables already live in .table-scroll wrappers, so the
   body never scrolls horizontally. */
@media (max-width: 768px) {
  :root { --hud-h: 96px; --nav-w: 52px; }
  body.has-activity { --activity-h: 28px; }
  .activity-chip { font-size: 12px; padding: 2px 8px; }

  #hud {
    flex-wrap: wrap;
    align-content: center;
    row-gap: 4px;
    column-gap: var(--sp);
    padding: 4px var(--sp);
  }
  .hud-logo { font-size: 19px; }
  .hud-colony { display: flex; align-items: center; gap: 6px; }
  .hud-colony .hud-label { display: none; }
  .hud-colony select { max-width: 46vw; font-size: 14px; }
  .hud-right { gap: var(--sp); margin-left: auto; }
  #hud-username { display: none; }
  /* row 2: the four resource cells share the full width — back into normal
     flex flow here (order/flex-basis are meaningless on the desktop rule's
     `position:absolute`), matching this breakpoint's own 2-row HUD wrap. */
  .hud-res {
    position: static; transform: none; width: auto; max-width: none;
    order: 10; flex-basis: 100%; gap: 0; justify-content: stretch;
  }
  .hud-res-cell { min-width: 0; flex: 1; padding: 2px 6px; gap: 4px; }
  .hud-res-val { font-size: 14px; }

  #page { padding: var(--sp); padding-bottom: calc(var(--sp) + var(--chat-dock-h)); }
  .page-head h2 { font-size: 17px; }

  .stat-strip { grid-template-columns: repeat(2, 1fr); }
  .colony-grid { grid-template-columns: 1fr; }
  .building-grid, .unit-grid { grid-template-columns: 1fr; }
  .unit-picker, .cargo-grid { grid-template-columns: 1fr; }
  .results-cols { grid-template-columns: 1fr; }
  .faction-grid { grid-template-columns: 1fr; }
  .login-wrap { padding-top: 2vh; }
  .kv-k { width: 104px; }

  .galaxy-nav { justify-content: center; }
  .gal-jump { flex-wrap: wrap; justify-content: center; }

  /* forms stack */
  .send-footer { flex-direction: column; align-items: stretch; text-align: center; }
  .send-footer .btn { width: 100%; }
  .invite-row input { flex: 1; min-width: 0; }
  .dir-row { flex-wrap: wrap; }
  .dir-side { width: 100%; align-items: stretch; }
  .dir-claim { justify-content: space-between; }
  .dir-colony-sel { flex: 1; max-width: none; }
  .dm-detail-head { flex-wrap: wrap; }
  .player-head { gap: var(--sp); }

  /* chat dock spans the viewport above the icon rail */
  #chat-dock {
    left: calc(var(--nav-w) + 6px); right: 6px; width: auto;
    max-height: 55vh;
  }
  #chat-msgs { min-height: 120px; max-height: 34vh; }
  /* #faction-banner is sized/positioned to butt flush against #chat-dock's
     own (fixed) left edge at desktop widths — at this breakpoint the dock
     itself grows to `left: nav-w + 6px` (spans nearly the full viewport),
     which the banner's own fixed width would now overlap. Same "not enough
     room" call already made for `.chat-portrait-col` at ≤1805px above. */
  #faction-banner { display: none; }

  #toasts { left: var(--sp); right: var(--sp); max-width: none; }
  #incoming-banner {
    left: calc(var(--nav-w) + var(--sp) * 2); right: var(--sp); width: auto; max-width: none;
    padding: 12px 14px 10px 58px; min-height: 78px;
  }
  .ib-title { font-size: 15px; }
}

/* ============================================================================
   PHASE 2 — Living Galaxy interactive map (galaxy → system → planet globe)
   Canvas stage + glass overlay panels. Reuses theme tokens; 8px grid.
   ========================================================================== */

.map-stage {
  position: relative;
  /* Full-bleed left/right past #page's own padding (owner: galaxy-lab visual
     pass, v0.60.12 — "the background should stretch to fill the whole
     background, not sit in a framed/separated box") — same technique
     .village-scene already uses. This box's own geometry (width/height
     below) still matters even though it no longer paints its own
     background (v0.60.17) — `.map-canvas`/`.map-models-layer`/
     `.map-overlay` are all `position:absolute; inset:0` children of THIS
     box, so it still has to land in the right place for them, plus the
     interactive/hit-testing area. */
  width: calc(100% + var(--sp) * 6);
  margin: 0 calc(var(--sp) * -3);
  /* v0.60.13 (owner: "still not extended to fill the whole background") —
     replaced the old blind `- 64px` fudge factor with the actual sum of
     everything eating into #page's vertical box: its own top+bottom padding
     (var(--sp)*3 each = --sp*6, --chat-dock-h folded into the bottom one)
     plus this tier's page-head (~26px) + its margin-bottom (--sp*2). The old
     constant *undershot* this real total, leaving a dead gray strip below
     the nebula; going the other way and overshooting it instead reopens
     #page's own overflow-y:auto (the map page would gain a scrollbar) —
     verified against a real measured layout across 3 viewport sizes. */
  height: calc(100vh - var(--hud-h) - var(--sp) * 8 - var(--chat-dock-h) - 26px);
  height: calc(100dvh - var(--hud-h) - var(--sp) * 8 - var(--chat-dock-h) - 26px); /* MOBILE-PLAN.md §3.2: dvh overrides vh above where supported — mobile URL-bar collapse no longer clips/overflows the stage */
  min-height: 420px;
  /* No border/radius/overflow-clip — a bordered, rounded box reads as "a
     panel with a picture in it"; removing the frame is what makes the
     nebula read as an actual BACKDROP instead (same reasoning as
     .village-scene's own "shouldn't read as just another panel"). */
  overflow: visible;
  /* v0.60.17 (owner screenshot, red-boxed hairline seams top+bottom: "are
     you using the same background twice in multiple layers instead of just
     extending it and stretching to the full area") — yes, that was exactly
     the v0.60.13–16 design (this box + a `::before` + a `::after`, each
     independently repainting "the same" image/tint/day-night wash) and it
     really was leaving a visible boundary at every join, matching-fixed-
     positioning notwithstanding. Fixed properly this time: this box paints
     NOTHING itself; the single `::before` further down is now the ONLY
     thing that paints the nebula, spanning the header strip + this box's
     own area + the #chat-dock clearance strip in one unbroken rectangle.
     One element can't seam with itself. */
  background: none;
  touch-action: none;
}
/* v0.60.17 — the map page's entire nebula backdrop, painted exactly once.
   Spans from right below the HUD (`top: var(--hud-h)`) all the way to the
   real viewport bottom (`bottom: 0`, not a computed height — nothing to get
   subtly wrong), and the same left/right bleed as `.map-stage` used to
   carry itself. `position: fixed` keeps it out of #page's overflow-y:auto
   box entirely (can't reopen the old scrollbar bug), and `z-index: -1`
   paints it behind every piece of static/positioned content sharing this
   stacking context — page-head's title/toolbar, `.map-stage`'s own
   absolutely-positioned canvas/star-field/legend children, all of it —
   without needing to individually match any of their z-indexes: a negative
   z-index always paints before z-index:auto positioned descendants,
   regardless of DOM nesting depth. Applies to the planet tier too
   (v0.60.20) — `position:fixed` means its own extent is viewport-relative,
   not sized by whichever grid track `.map-stage` itself happens to be a
   member of, so this reaches the true page edges there as well. */
/* owner: "a stage panel a teljes képernyőt kitölti, mint legalsó réteg,
   ezt is definiáltuk az editorban" — galaxy-map-concept.css's
   `.stage-bg-fill` is `position:fixed; inset:0` — the TRUE full viewport,
   sitting behind the sidenav/topbar too (both now translucent — see
   `#sidenav`'s own recent opacity fix — so this actually shows through
   them, not just a no-op). This layer previously stopped at
   `left:var(--nav-w)`/`top:var(--hud-h)`, leaving a visible seam right at
   the sidenav/topbar edge instead of one continuous backdrop with the
   HUD floating semi-transparently over it. `z-index:-1` (unchanged) is
   already well below `#sidenav`(10)/`#hud`(40), so extending the bounds
   doesn't risk painting over anything real. */
.map-stage::before {
  content: '';
  position: fixed;
  inset: 0;
  /* owner: "a világosság megmaradt, de a lényeget kitörölted" — a first
     pass at "too bright" swapped this photo out for a flat gradient,
     which killed the actual nebula artwork instead of just dimming it.
     Corrected approach: keep the real image (still the visible texture/
     detail underneath), add a flat dark wash PAINTED OVER it instead —
     first-listed background layer draws on top of the ones after it, so
     this scrim sits between the photo and the WebGL shader/content above,
     darkening without deleting. */
  background:
    linear-gradient(rgba(4, 7, 14, 0.55), rgba(4, 7, 14, 0.55)),
    radial-gradient(1200px 700px at 60% 30%, rgba(30, 44, 86, 0.35), transparent 60%),
    url('/img/space/nebula_backdrop.jpg') center / cover no-repeat,
    var(--bg0);
  /* owner: "a nap éjszaka ciklust töröld, tuti az okozza a hibát" — removed.
     Was a 30-real-minute box-shadow cycle (v0.60.16) between a light warm
     tint and a dark tint; had a real history of reading as "always stuck
     on the light phase" (v0.60.26) depending on whether the WebGL nebula
     shader was occluding the lower layer it lived on. Simplest fix for the
     recurring brightness complaints: the cycle is gone rather than
     re-diagnosed again. */
  pointer-events: none;
  z-index: -1;
}
.map-canvas {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  cursor: grab;
  outline: none;
  /* MOBILE-PLAN.md §3.2: without this, a touch drag/pinch on the canvas gets
     hijacked by the page's own scroll/pinch-to-zoom before our own pointer
     handlers (and touch.js's attachPinch) ever see it. */
  touch-action: none;
}
/* Gated on .map-canvas-kbd-focus (map.js's suppressPointerFocusRing), not a
   raw :focus-visible — the browser also flags a modifier-held pointerdown
   (Shift/Ctrl/Alt/Meta-drag) as keyboard-equivalent focus, which has no
   real meaning here (this canvas has no modifier-click feature) and was
   popping the ring on an ordinary Shift-held drag. */
.map-canvas.map-canvas-kbd-focus:focus-visible { box-shadow: inset 0 0 0 2px var(--panel-border-hi); }
.map-canvas.grabbing { cursor: grabbing; }
/* Magnetic cursor prototype (galaxyRenderer3D only) — native pointer hidden
   while the custom reticle below stands in for it. `!important` is load-
   bearing, not sloppiness: the drag path (`.grabbing` class) and stray
   inline `style.cursor` writes from before the toggle flipped on (the
   canvas element survives projection switches) both otherwise outrank
   this rule — an inline style can ONLY be beaten by an !important
   stylesheet declaration. */
.map-canvas.map-cursor-none { cursor: none !important; }

/* Magnetic cursor prototype (map.js galaxyRenderer3D, toggle-gated, default
   OFF): a 4-corner "target-lock" bracket (owner reference: docs/art/ref-GUI/
   generated-frames/corner_brackets.svg), each corner eased independently
   toward its own slot of the hovered star's bounding SQUARE, or a small
   fixed reticle around the raw pointer otherwise — see the renderer's own
   updateMagneticCursor() for the per-frame math. Pure CSS L-shapes (2 border
   sides meeting at a rounded elbow), not a raster import or a single
   scaling ring — matching `.tabs`'s own corner-tick rationale above ("stays
   crisp at any zoom/DPI"), and the only way 4 corners can resize/move
   independently every frame. Positioned via `transform: translate()` only
   (JS updates it every frame — no CSS transition on transform, which would
   fight the per-frame lerp and look janky on fast hover changes).
   pointer-events:none throughout — this must never steal a click/drag meant
   for the canvas underneath. */
.map-magnetic-cursor {
  position: absolute; top: 0; left: 0;
  width: 0; height: 0;
  pointer-events: none;
  z-index: 4;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.map-magnetic-cursor.show { opacity: 1; }
.map-magnet-corner {
  position: absolute; left: 0; top: 0;
  width: 14px; height: 14px; /* matches CORNER_ARM in map.js */
  border: 0 solid var(--accent);
  filter: drop-shadow(0 0 4px rgba(79, 216, 255, 0.6)) drop-shadow(0 0 8px rgba(79, 216, 255, 0.35));
}
/* Each corner only borders its own two outward-facing edges, rounded at the
   elbow where they meet — an L-shape, never a full box outline. */
.map-magnet-corner.tl { border-top-width: 2px; border-left-width: 2px; border-top-left-radius: 4px; }
.map-magnet-corner.tr { border-top-width: 2px; border-right-width: 2px; border-top-right-radius: 4px; }
.map-magnet-corner.bl { border-bottom-width: 2px; border-left-width: 2px; border-bottom-left-radius: 4px; }
.map-magnet-corner.br { border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: 4px; }

.map-overlay { position: absolute; inset: 0; pointer-events: none; }
.map-overlay > * { pointer-events: auto; }

/* System-view real 3D bodies (map.js bodyModelLayer) — a decorative overlay
   of spinning WebGL planet/star models positioned over the canvas's own
   computed coordinates. pointer-events:none throughout — the canvas beneath
   keeps sole ownership of hover/click hit-testing; these boxes are purely a
   visual replacement for the flat glow-blob read. Circular clip (no card
   chrome) so a model reads as a sphere, not a UI tile. */
.map-models-layer { position: absolute; inset: 0; pointer-events: none; }
/* Holo war-table layer (galaxy tier): dust cloud + projector dais + dome,
   rendered by model-viewer.js's GalaxyHoloTable. Inserted BEFORE .map-canvas
   in the DOM so it paints under every interactive 2D element (fog, halos,
   rings, labels) and under the WebGL star bodies — pure decoration, never
   hit-tested. */
.map-holo-layer { position: absolute; inset: 0; pointer-events: none; }
/* v0.60.24 — the live procedural nebula shader's container (map.js's
   `mountNebulaBackdrop`, restored after owner asked twice for it back).
   Deliberately NOT `position:absolute;inset:0` like `.map-models-layer`
   above (that would size it to `.map-stage`'s own real box again, the
   exact mismatch that caused the seam this got removed for at v0.60.19) —
   instead it's `position:fixed` with the IDENTICAL top/left/right/bottom as
   `.map-stage::before`, so its canvas covers the exact same full-bled
   rectangle the static image does. Same `z-index: -1`: DOM order (this div
   is a real child of `.map-stage`, inserted after where `::before`
   conceptually sits) places it painting on top of the static image but
   still behind every static/positioned piece of real content, same
   relative order the original (smaller-box) version had. */
/* Same `inset:0` fix as `.map-stage::before` just above (its own comment
   has the full "stage fills the whole screen as the lowest layer"
   rationale) — kept identical bounds to that layer per this rule's own
   original comment, so both had to move together. */
.map-nebula-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: -1;
}
/* Compound selector (not just .map-body-box) — .model-hero's own position/
   border/background/border-radius rules are declared LATER in this file and
   would otherwise win the cascade at equal specificity (single-class vs
   single-class; source order breaks the tie). .model-hero.map-body-box has
   strictly higher specificity, so it wins regardless of declaration order. */
.model-hero.map-body-box {
  position: absolute; top: 0; left: 0;
  border: 0; background: transparent; border-radius: 50%;
  pointer-events: none;
}
/* Corona sprite (map.js bodyModelLayer) — a real photographic glow image
   (star_corona.png, real per-pixel alpha) IS the sun itself as of v0.46.0
   (the old GLB star model was removed — owner: "remove the old sun").
   Plain positioned `<img>`, not a .model-hero — no WebGL viewer, no click
   target, `mix-blend-mode: screen` so its own dark-orange background tone
   lightens/adds onto the starfield instead of reading as a flat sprite
   edge (the source alpha already fades to 0 well before its own bounding
   box, so `screen` mostly matters at the innermost, brightest pixels).
   A slow continuous spin (owner: "make the new sun to spin slowly") lives
   on the INNER `.map-body-corona-spin` <img>, not this wrapper: map.js's
   place() drives this wrapper's `transform: translate(...)` every frame,
   and a running CSS animation on `transform` fully overrides any inline
   `transform` JS sets on the same element — so translate (wrapper) and
   rotate (inner img) must be on separate elements or the spin would
   silently cancel the positioning. */
.map-body-corona {
  position: absolute; top: 0; left: 0;
  pointer-events: none;
}
.map-body-corona-spin {
  display: block;
  width: 100%; height: 100%;
  mix-blend-mode: screen;
  animation: map-corona-spin 90s linear infinite;
}
@keyframes map-corona-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .map-body-corona-spin { animation: none; }
}
/* NPC site badge (map.js bodyModelLayer) — a small hazard-tinted glow so a
   Custodian remnant reads as "a marker ON this planet" at a glance, distinct
   from the plain planet body it's offset against. */
.map-body-box.map-npc-badge {
  filter: drop-shadow(0 0 5px rgba(255, 93, 108, 0.65)) drop-shadow(0 0 2px rgba(255, 93, 108, 0.9));
}
.map-overlay .empty-state { position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; background: rgba(5, 7, 15, 0.35); }

/* breadcrumb (in the page head) */
.map-crumbs { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
.map-crumb {
  padding: 3px 10px; border: 1px solid var(--panel-border); border-radius: 20px;
  background: rgba(20, 28, 46, 0.6); color: var(--txt-dim); white-space: nowrap;
  letter-spacing: 0.04em; transition: color 0.15s, border-color 0.15s;
}
a.map-crumb:hover { color: var(--txt); border-color: var(--panel-border-hi); }
.map-crumb.here { color: var(--accent); border-color: var(--panel-border-hi); }
.map-crumb-sep { color: var(--txt-faint); }

/* floating info panel (galaxy/system hover) */
.map-panel {
  position: absolute; top: var(--sp); right: var(--sp); width: 250px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px;
  padding: calc(var(--sp) * 1.5); backdrop-filter: blur(9px);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
}
.map-panel.hidden { display: none; }
/* Sci-fi HUD "callout" variant (galaxy AND system tiers — planet tier keeps
   the plain pinned panel above, untouched): positioned every frame via
   `transform: translate()` from map.js's mountHoverCallout(), so `top`/
   `right` must get out of the way and start from the overlay's own (0,0).
   `pointer-events: none` is NOT cosmetic — floating the panel near the
   hovered star means the cursor can end up sitting right on top of it; if
   the panel could still catch the mousemove, that event never reaches the
   canvas underneath, the star hover clears, the panel snaps back/away,
   hover restores, and the whole thing repeats — an infinite flicker loop. */
/* pointer-events was `none` here — which made the callout's own ATTACK/SPY
   quick-action buttons unclickable for a human (a programmatic .click()
   ignores pointer-events, which is why headless verification never caught
   it). Clickable now; the sticky hover-intent wrapper in map.js keeps the
   panel open long enough to actually reach it. */
.map-panel.callout { top: 0; left: 0; right: auto; width: 230px; pointer-events: auto; }
.map-panel-title { font-size: 17px; font-weight: 600; letter-spacing: 0.02em; }
.map-panel-sub {
  color: var(--txt-dim); font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  margin-bottom: var(--sp);
}
.map-panel-cta { font-size: 13px; margin-top: var(--sp); font-style: italic; }

.map-kv { display: flex; justify-content: space-between; gap: var(--sp); font-size: 15px;
  padding: 4px 0; border-top: 1px solid rgba(120, 160, 220, 0.09); }
.map-k { color: var(--txt-dim); }
.map-v { color: var(--txt); }

.map-flag { font-size: 14px; margin-top: 6px; padding: 4px 8px; border-radius: 6px;
  background: rgba(79, 216, 255, 0.08); border: 1px solid var(--panel-border); }
.map-flag.mine { color: var(--accent); border-color: rgba(79, 216, 255, 0.3); }
.map-flag.rare { color: var(--fc-traders); border-color: rgba(255, 209, 102, 0.35);
  background: rgba(255, 209, 102, 0.06); }
.map-flag.danger { color: var(--danger); border-color: rgba(255, 77, 93, 0.4);
  background: var(--danger-soft); }
.map-flag.nebula { color: #cfa9ff; border-color: rgba(176, 127, 232, 0.35);
  background: rgba(176, 127, 232, 0.07); }
/* Phase 10A (PHASE10-DEBRIS.md §6): scrap-bronze — distinct from contact
   amber/gate steel/rare gold, non-hostile (not `.danger`) since a debris
   field is scavenging texture, not a threat flag. */
.map-flag.debris { color: #c9a86b; border-color: rgba(201, 168, 107, 0.35);
  background: rgba(201, 168, 107, 0.07); }
/* Stellar Nexus map overlay: legendary-gold, distinct from rare's amber and
   debris's bronze — the host location is deliberately public (mega.js
   megaMapMarkers), so this reads as a landmark flag, not a threat. */
.map-flag.mega { color: #ffb02e; border-color: rgba(255, 176, 46, 0.4);
  background: rgba(255, 176, 46, 0.08); }

/* legend + hint (bottom overlay) */
.map-legend {
  /* Bumped up from `bottom: var(--sp)` — `#chat-dock` moved from the right
     edge to bottom-left (matching the owner's own reference mockup, see
     its comment), landing directly under this legend box. Clearing the
     COLLAPSED chat-dock's height (`--chat-dock-h`) is enough; an
     deliberately-EXPANDED chat-dock is already established elsewhere
     (`.map-side-dash`'s own comment) as allowed to cover other overlay
     content when the player opens it themselves. */
  position: absolute; bottom: calc(var(--chat-dock-h) + var(--sp) * 2); left: var(--sp);
  /* owner: "ugyanúgy pozícionálj minden egyes elemet... ahogy a bemásolt
     JSON fájlban kaptad" — galaxy-map-concept.css's `.legend` is an exact
     122×194px box (the owner's own edit-mode export); this used to be
     unset/auto-sized here. `overflow-y:auto` (already present) is the
     safety net if a future content change doesn't fit. */
  width: 122px; height: 194px; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
  /* Reference's `.panel` (the base every side-panel/legend/chat-body box
     shares there) uses `blur(14px) saturate(1.2)` — a noticeably stronger
     glass effect than this had (`blur(8px)`, no saturate boost). */
  padding: 9px 11px; backdrop-filter: blur(14px) saturate(1.2); font-size: 13px; color: var(--txt-dim);
}
.map-lg { display: flex; align-items: center; gap: 7px; margin: 3px 0; }
.map-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
.map-hint {
  /* Owner: "drag to tumble dialog is annoying at the middle of the screen,
     move just above the center of the command dock" — real bug: this used
     plain `left:50%` (true viewport center), but `.map-action-dock` centers
     itself EXCLUDING the dashboard sidebar's reserved width (see its own
     `calc((100vw + var(--nav-w) - 345px) / 2)` formula) — the two haven't
     shared a center since the sidebar was promoted to global chrome, so
     the hint read as floating at a disconnected "middle of the screen"
     instead of sitting over the dock. Now uses the EXACT same centering
     formula. Vertical clearance also switched from a flat 150px (tuned
     for the dock's max height only) to the dock's own actual computed
     height formula + a small fixed gap, so it tracks "just above the
     dock" at any viewport width instead of leaving a growing/shrinking
     gap as the dock's own aspect-ratio height scales with its width. */
  position: fixed;
  bottom: calc(var(--sp) + min(700px, calc(100vw - var(--nav-w) - 380px)) * 0.2 + 14px);
  left: calc((100vw + var(--nav-w) - 345px) / 2); transform: translateX(-50%);
  font-size: 14px; color: var(--txt-dim); letter-spacing: 0.03em; text-align: center;
  background: rgba(8, 12, 22, 0.6); padding: 6px 14px; border-radius: 20px;
  border: 1px solid var(--panel-border); white-space: nowrap;
}

/* ---- action dock (galaxy-map-concept GUI, wired to real routes) ----
   Owner: "kösd be az éles játékba az új GUI-t, kezdjük a Galaxy view-al".
   Reuses the same chamfer double-layer technique as `.panel-framed`
   (--panel-accent/--panel-fill/--chamfer, see its own big comment above)
   rather than a parallel system, but cut into a full OCTAGON (all 4
   corners, not just 2) to match the concept art's card shape — a
   `.panel-framed`-style modifier, not a copy of its polygon. */
/* owner (pixel-exact rebuild): centering on a flat `left:50%` centers the
   dock over `.map-stage`'s own box, which spans nav-w→100vw (the full
   page width `#page` itself gets) — but `.map-side-dash` (313px + its own
   `--sp*2` gap) floats on top of the RIGHT portion of that as a separate
   fixed overlay, same as `galaxy-map-concept.css`'s `.galaxy-stage` which
   explicitly insets `right:326px` to exclude `.sidebar`'s space from the
   stage entirely. Shifting the center point left by half the sidebar's
   reserved width approximates that same "centered in the visible stage,
   not under the dashboard" placement without needing to actually resize
   `.map-stage` itself (which real click/drag/pan hit-testing depends on
   staying full-bleed). */
/* galaxy-map-concept.css's `.search-box` — a real, working system-name
   search (see map.js's `renderGalaxy()` for the actual filter/navigate
   logic), not just the reference's inert placeholder input. */
.map-search-box {
  display: flex; align-items: center; gap: 7px; padding: 7px 12px;
  border-radius: 999px; border: 1px solid var(--panel-border);
  background: rgba(255, 255, 255, 0.02); color: var(--txt-dim); width: 178px;
}
.map-search-box:focus-within { border-color: var(--panel-border-hi); }
.map-search-ic { flex: none; font-size: 15px; }
.map-search-input {
  background: none; border: none; outline: none; color: var(--txt); font: inherit;
  font-size: 14.5px; width: 100%; padding: 0;
}
.map-search-input::placeholder { color: var(--txt-faint); }

/* owner: "E:/Stellar/docs/art/ref-GUI/generated-frames/console_dock.svg
   haszáld ezt a command dockhoz és a kör közepére helyezd az ikonokat" —
   full rebuild replacing the old per-button CSS chamfer/corner-bracket
   frame with a single real generated frame image (public/img/panels/
   console-dock.svg — copied in from that ref-GUI asset; its own
   placeholder slot labels were edited in-place to the 7 real button
   names, see map.js's buildActionDock()). The whole dock is now ONE
   `<img>` background (viewBox 900×180, aspect-ratio 5:1) with the 7
   icon buttons absolutely positioned on top at each slot's exact
   icon-circle center (`xPct`/`yPct` in map.js, taken straight from the
   SVG's own `cx`/`cy`) — sized as a % of the container so it stays
   pixel-exact to the art at any width instead of drifting off the
   circles the art actually drew. */
/* owner: "make the GUI consistent on each tab" — was `position:absolute`
   (relative to `.map-stage`, Galaxy-only); now `position:fixed`
   (viewport-relative, like `#chat-dock`/`#sidenav`) since command-shell.js
   mounts this once globally, on every page.
   v2 (owner: "a chat rálóg a command dock-ra, vidd balrább") — briefly
   shrank + repositioned this sequentially after `#chat-dock` instead.
   v3 (owner: "most miért kicsinyítetted le a command dockot? nem
   kértem ilyet, az előző méret tökéletes volt... a command dock pedig
   tökéletesen a képernyő közepén helyezkedjen el") — v2 was the wrong
   fix. Reverted to TRUE centering at the original 700px, excluding only
   `.map-side-dash`'s reserved width on the right (same formula as the
   very first version of this rule) — `#chat-dock` is the one that now
   adapts its own width to clear THIS box's actual (centered) left edge
   instead, see its own comment. */
.map-action-dock {
  /* v0.60.30 (owner: "hozd annyival feljebb, hogy az alja egy síkban
     legyen a chat ablak aljával" — bring it up enough that its bottom is
     level with the chat window's bottom) — was `var(--sp)` (8px), 3px
     short of `#chat-dock`'s own `bottom: 11px`. Matching that value
     exactly puts both fixed docks' bottom edges on the same line instead
     of the 3px mismatch. */
  position: fixed; bottom: 11px;
  left: calc((100vw + var(--nav-w) - 345px) / 2); transform: translateX(-50%);
  z-index: 15;
  width: min(700px, calc(100vw - var(--nav-w) - 380px));
  aspect-ratio: 900 / 180;
}
.map-dock-frame {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 0;
  filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.4));
}
/* Some of these render as <a href>, some as <button onclick> (Officers —
   see map.js's buildActionDock() comment) — reset button chrome so both
   tags look identical. Sized to the art's own r=22 icon-circle diameter
   (44px of the 900px-wide viewBox = 4.89%); `.primary` (Command, r=24)
   gets the slightly larger 5.33%. `transform: translate(-50%,-50%)`
   centers the glyph ON the `left`/`top` point map.js sets (the circle's
   own center), not anchored by its own top-left corner. */
.map-dock-slot {
  position: absolute; z-index: 1;
  width: 4.89%; aspect-ratio: 1;
  transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  text-decoration: none; color: var(--txt-dim); cursor: pointer;
  background: none; border: none; padding: 0;
  transition: filter 0.15s;
}
.map-dock-slot:hover { filter: drop-shadow(0 0 8px rgba(79, 216, 255, 0.65)); }
.map-dock-slot.primary { width: 5.33%; color: var(--ok); }
.map-dock-slot.primary:hover { filter: drop-shadow(0 0 10px rgba(61, 220, 151, 0.7)); }
/* Now global (was Galaxy-only, where "Command" was always the obvious
   active slot) — `updateActiveDockSlot()` (command-shell.js, called from
   main.js's route() on every navigation) toggles this so whichever
   section you're actually viewing lights up, mirroring `.nav-link.active`
   in the sidenav. */
.map-dock-slot.active { color: var(--accent); filter: drop-shadow(0 0 10px rgba(79, 216, 255, 0.75)); }
.map-dock-slot.primary.active { color: var(--ok); filter: drop-shadow(0 0 12px rgba(61, 220, 151, 0.8)); }
/* Real generated icon art (public/img/icons/dock/*.png) with a hard
   fallback to the original unicode glyph on load error — same pattern
   `iconImg()` in components.js already uses everywhere else. */
.map-dock-glyph-wrap { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
/* Probe-only — never shown, exists purely so its `error` event (map.js
   dockGlyph()) can trigger the unicode-glyph fallback; a `mask-image` 404
   fails silently with no comparable event to hook. */
.map-dock-glyph-probe { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
/* The actual visible icon: a plain color fill, shaped by the PNG's alpha
   via `mask-image` — see dockGlyph()'s big comment for why color is
   CSS-driven instead of baked into the PNG. Relative sizing (was fixed
   px) so it scales with the slot's own %-of-container size. */
.map-dock-glyph-mask {
  display: inline-block; width: 62%; height: 62%;
  background-color: var(--accent);
  -webkit-mask-image: var(--icon-url); mask-image: var(--icon-url);
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
}
.map-dock-slot.primary .map-dock-glyph-mask { background-color: var(--ok); }
.map-dock-glyph { position: relative; display: flex; font-size: 20px; }

/* ---- galaxy dashboard sidebar (galaxy-map-concept GUI, real data) ----
   Owner: "addig csináld amíg be nem kötötted teljesen az új GUI-t" — see
   map.js's `buildDashboardSidebar()` for the data-sourcing rationale.
   `position:fixed` overlay on the right edge, same language as
   `.map-legend`/`#chat-dock`/`.map-action-dock` on this page — deliberately
   NOT a flex sibling of `.map-stage` (see that function's own comment).
   `bottom: 54px` clears the collapsed `#chat-dock` strip docked to the same
   right edge below it; an EXPANDED chat-dock (z-index 50) still stacks
   above this (z-index 2) if the player opens it over this sidebar — an
   acceptable transient overlap, not a permanent layout conflict. */
/* Owner: "ügyelj az általam megadott elrendezésre és méretekre/arányokra" —
   313px matches the owner's own hand-set `.sidebar` width from the mockup
   edit session (was 280px, an arbitrary guess). */
/* owner: "make the GUI consistent on each tab" — was Galaxy-only
   (z-index:2, only ever stacked against that page's own canvas/overlay);
   now global (command-shell.js), bumped to z-index:15 to consistently sit
   above every page's normal content, same tier as `.map-action-dock`. */
.map-side-dash {
  position: fixed; top: calc(var(--hud-h) + var(--sp) * 2); right: calc(var(--sp) * 2); bottom: 54px;
  width: 313px; z-index: 15; overflow-y: auto;
  display: flex; flex-direction: column;
  /* Unified-shell prototype (owner: "darabos a HUD/GUI") — the 5 sidebar
     cards used to each carry their own border + corner-tick decal
     (v0.60.32, see the history below), reading as 5 separate stacked
     boxes. That frame moved HERE, to the container, so the whole column
     is now ONE continuous panel — same #2D3B55/#101a26 family as
     #page's own bezel and #chat-dock, so this reads as part of the same
     console housing. Individual cards (.map-side-card below) are now
     borderless sections inside it, divided by a thin rule instead of a
     full box each. */
  border: 1px solid #2D3B55;
  background: linear-gradient(180deg, #101a26 0%, #0a0f16 100%);
  background-image:
    url(/img/ui/corner-tick-tl.svg), url(/img/ui/corner-tick-tr.svg),
    linear-gradient(180deg, #101a26 0%, #0a0f16 100%);
  background-repeat: no-repeat;
  background-position: top left, top right, 0 0;
  background-size: 20px 20px, 20px 20px, 100% 100%;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
}
/* v0.60.32's per-card frame (border + corner-tick decal) is gone — see
   .map-side-dash above, which now carries ONE frame for the whole column.
   Each card is a plain padded section, separated from the next by a thin
   top divider (skipped on the first card, which butts against the
   container's own top edge/corner-ticks instead). */
.map-side-card {
  padding: var(--sp) calc(var(--sp) * 1.5);
  border-top: 1px solid rgba(45, 59, 85, 0.4);
}
.map-side-card:first-child { border-top: none; }
/* Unified-shell prototype: the per-card width rules from the old
   "5 separate boxes centered in a wider column" layout are gone — each
   card now stretches to the container's own full width (flex-column's
   default cross-axis stretch), since they're borderless sections of ONE
   panel rather than independently-sized boxes floating inside it. */
.map-side-card .sp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.map-side-card .sp-title { font-family: var(--font-display); font-weight: 700; font-size: 14px; letter-spacing: 0.05em; color: var(--txt); text-transform: uppercase; }
.map-side-card .sp-sub { font-size: 12px; color: var(--txt-dim); letter-spacing: 0.06em; margin-top: 1px; }
.map-side-card .sp-badge {
  font-size: 12px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
  background: rgba(79, 216, 255, 0.14); color: var(--accent); border: 1px solid var(--panel-border-hi);
}
.map-side-stats { display: flex; flex-direction: column; gap: 4px; margin-bottom: 4px; }
.map-side-stat { display: flex; align-items: center; justify-content: space-between; font-size: 13.5px; }
.map-side-fleets { display: flex; flex-direction: column; gap: 5px; margin-bottom: 4px; }
.map-side-fleet-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.map-side-fleet-row .fleet-ic { color: var(--accent); flex: none; }
.map-side-fleet-row .fleet-info { min-width: 0; flex: 1; }
.map-side-fleet-row .fleet-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.map-side-fleet-row .fleet-eta { flex: none; font-size: 12.5px; color: var(--txt-dim); }
.map-side-card .obj-task-row { font-size: 14px; margin-bottom: 6px; }
.map-side-card .obj-rewards-label { font-size: 11.5px; letter-spacing: 0.08em; color: var(--txt-dim); text-transform: uppercase; margin-bottom: 4px; }
/* v0.60.31 history (paint-order bug, flat-line-vs-glow, corner-inset
   fixes) — originally built and iterated on the alerts card alone via a
   `.map-side-card-alerts` modifier class; v0.60.32 (owner: "apply the
   same frame for all the panels above the system alert") folded all of
   it into the shared `.map-side-card` base rule above instead, so the
   modifier class and this whole block became redundant — removed rather
   than left as dead CSS. The `.sp-head`/`.btn-block` divider rules below
   are now unscoped to `.map-side-card` generally (every card has both —
   confirmed via command-shell.js, all 5 `paint*()` functions build the
   same `.sp-head` title row + a single `.btn-block` footer link). */
.map-side-card .sp-head {
  padding-bottom: 6px; margin-bottom: 8px;
  border-bottom: 1px solid rgba(31, 42, 58, 0.35);
}
.map-side-card .btn-block {
  margin-top: 8px; padding-top: 10px;
  border-top: 1px solid rgba(31, 42, 58, 0.35);
}
/* Owner: "system alerts got smaller, could also be colorful like reference
   image" — bigger rows + a real circular icon-chip badge per row (was a
   bare inline glyph), and a 3rd amber `warn` tier alongside the existing
   danger/info (command-shell.js's ALERT_KIND_SEVERITY). Rows with a real
   navigation target (see dashAlertRow's `onNav`) get a trailing chevron
   and a hover state — matches the reference's own clickable-row affordance. */
.map-side-alerts { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; }
.map-side-alert {
  display: flex; align-items: center; gap: 10px; font-size: 14px;
  padding: 8px 10px; border-radius: 8px; line-height: 1.35;
  border: 1px solid transparent;
}
.map-side-alert.clickable { cursor: pointer; transition: background 0.15s, border-color 0.15s; }
.map-side-alert .a-ic {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px;
}
.map-side-alert .a-txt { flex: 1; min-width: 0; }
.map-side-alert .a-chev { flex: none; font-size: 16px; opacity: 0.6; }
.map-side-alert.alert-danger {
  background: var(--danger-soft); color: #ffb9c0;
  animation: fx-danger-pulse 1.8s ease-in-out infinite;
}
.map-side-alert.alert-danger .a-ic { background: rgba(255, 77, 93, 0.22); color: var(--danger); }
.map-side-alert.alert-danger.clickable:hover { background: rgba(255, 77, 93, 0.22); border-color: rgba(255, 77, 93, 0.5); }
@keyframes fx-danger-pulse {
  0%, 100% { box-shadow: 0 0 0 rgba(255, 77, 93, 0); }
  50% { box-shadow: 0 0 10px rgba(255, 77, 93, 0.35); }
}
@media (prefers-reduced-motion: reduce) {
  .map-side-alert.alert-danger { animation: none; }
}
.map-side-alert.alert-warn { background: rgba(255, 176, 46, 0.1); color: var(--warn); }
.map-side-alert.alert-warn .a-ic { background: rgba(255, 176, 46, 0.22); color: var(--warn); }
.map-side-alert.alert-warn.clickable:hover { background: rgba(255, 176, 46, 0.2); border-color: rgba(255, 176, 46, 0.5); }
.map-side-alert.alert-info { background: rgba(79, 216, 255, 0.08); color: var(--txt); }
.map-side-alert.alert-info .a-ic { background: rgba(79, 216, 255, 0.18); color: var(--accent); }
.map-side-alert.alert-info.clickable:hover { background: rgba(79, 216, 255, 0.18); border-color: var(--panel-border-hi); }
/* Narrower Galaxy stage clearance at the same 1120px breakpoint the
   .map-action-dock's own siblings (#chat-dock) start shrinking at — the
   sidebar would otherwise overlap the canvas's own right-side content at
   that width. */
@media (max-width: 1120px) {
  .map-side-dash { display: none; }
}

/* 3D / Flat projection toggle (map header, galaxy + system tiers) */
.map-viewseg {
  display: inline-flex; flex: none;
  border: 1px solid var(--panel-border); border-radius: 20px; overflow: hidden;
}
.map-viewseg button {
  font: inherit; font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--txt-dim); background: rgba(20, 28, 46, 0.6); border: 0;
  padding: 4px 12px; cursor: pointer; transition: color 0.15s, background 0.15s;
}
.map-viewseg button + button { border-left: 1px solid var(--panel-border); }
.map-viewseg button:hover { color: var(--txt); }
.map-viewseg button.on { color: var(--accent); background: rgba(79, 216, 255, 0.1); }
.map-viewseg button:focus-visible { outline: 2px solid var(--panel-border-hi); outline-offset: -2px; }

/* Nebulae (alliance territory fog) toggle — a one-button segment beside the
   3D/Flat control; carries .map-viewseg so it inherits that exact skin
   (Dominion Nebulae, docs/specs/alliance-nebulae.md §2). */
.map-fogseg button { white-space: nowrap; }

/* planet tier: globe stage + side inspector, two columns */
.planet-layout { display: grid; grid-template-columns: 1fr 320px; gap: calc(var(--sp) * 2);
  align-items: start; }
.planet-layout .map-stage {
  /* Same precise budget as the base `.map-stage` height fix (v0.60.13) —
     this tier had the identical old `- 64px` blind fudge factor, which
     undershoots the real total (page padding + page-head + margin) enough
     to leave #page's own overflow-y:auto scrolling by a few px. Caught
     while verifying the v0.60.20 background-bleed change above, not
     something that change itself introduced. */
  height: calc(100vh - var(--hud-h) - var(--sp) * 8 - var(--chat-dock-h) - 26px);
  height: calc(100dvh - var(--hud-h) - var(--sp) * 8 - var(--chat-dock-h) - 26px); /* MOBILE-PLAN.md §3.2: dvh overrides vh above where supported — mobile URL-bar collapse no longer clips/overflows the stage */
  min-height: 460px;
  /* Cancel the galaxy-tier full-bleed WIDTH (base .map-stage rule, above) —
     here .map-stage is a GRID ITEM (the 1fr column next to .planet-side's
     320px sidebar); a negative-margin bleed would blow past the grid track
     into the sidebar column instead of the page edge. The BACKGROUND still
     reaches the true page edges though (v0.60.20, owner: "extend the
     background on this page too just like on galaxy map") — the shared
     `::before` (base rule) is `position:fixed`, so its own left/right/top/
     bottom span the real viewport regardless of what grid track THIS box
     sits in; the .planet-side info panels just paint their own opaque
     backgrounds on top of it as normal, same as the sidenav does on the
     galaxy tier. v0.60.21 (owner screenshot, red arrow at the box's own
     left/top edge: "the blue panel frame border is still there") — dropped
     the border/radius/overflow-clip here too, same reasoning as the base
     rule's own "reads as an actual BACKDROP, not a panel with a picture in
     it": now that the background is seamless with everything around it,
     a visible box outline around the globe viewport just re-draws the seam
     the background fix was for. */
  width: 100%;
  margin: 0;
  overflow: visible;
  background: none;
}
.planet-side { min-width: 0; }
.territory-inspector { min-height: 120px; }

/* yield bars */
.yield-list { margin-top: var(--sp); display: flex; flex-direction: column; gap: 8px; }
.yield-head { display: flex; align-items: center; gap: 6px; font-size: 14px; }
.yield-head .yield-x { margin-left: auto; color: var(--txt-dim); }
.yield-bar { height: 6px; border-radius: 4px; background: rgba(255, 255, 255, 0.08);
  overflow: hidden; margin-top: 4px; }
.yield-fill { display: block; height: 100%; border-radius: 4px; background: var(--accent); }
.yield-fill.res-alloys { background: #c9d2e0; }
.yield-fill.res-crystal { background: #a56cff; }
.yield-fill.res-isotopes { background: #4fd8ff; }
.yield-fill.res-rations { background: #57e08a; }

/* territory actions + tech gate */
.territory-actions { margin-top: calc(var(--sp) * 1.5); }
.action-btn-row { display: flex; gap: 6px; flex-wrap: wrap; }
.action-btn-row .btn { flex: 1; min-width: 84px; }
/* Inside the map's hover-info panel the row sits in a plain flow stack with
   no gap of its own — measured 0px to whatever is above it, which reads fine
   after a text row but visibly "slides into" a bordered .map-flag badge
   (owner screenshot: the wrapped Custodian-remnant badge touching ATTACK/
   SPY). One breathing-room rule beats per-branch margins in map.js. */
.map-panel .action-btn-row { margin-top: 8px; }
.gate-notice {
  display: flex; align-items: flex-start; gap: 6px; font-size: 14px; line-height: 1.4;
  color: var(--warn); background: rgba(255, 176, 46, 0.08);
  border: 1px solid rgba(255, 176, 46, 0.3); border-radius: 6px;
  padding: 7px 9px; margin-bottom: var(--sp);
}
.gate-lock { flex: none; }
.gate-link { color: var(--accent); white-space: nowrap; }

/* aurium HUD/colony accent (5th resource) */
.res-aurium { color: var(--fc-traders); }
.hud-res-cell.res-aurium .res-ic { color: var(--fc-traders); text-shadow: 0 0 8px rgba(255, 209, 102, 0.6); }

@media (max-width: 860px) {
  .planet-layout { grid-template-columns: 1fr; }
  .planet-layout .map-stage { height: 60vh; min-height: 360px; }
  .map-panel { width: 210px; }
  .map-hint { display: none; }
}

/* Phase 2 — research panel (interstellar reach tech) + aurium row */
.research-active {
  display: flex; align-items: center; gap: var(--sp);
  padding: 8px 10px; margin-bottom: var(--sp);
  background: rgba(79, 216, 255, 0.06); border: 1px solid var(--panel-border-hi);
  border-radius: var(--radius); font-size: 15px;
}
.research-active .q-name { flex: 1; min-width: 0; }
.research-card {
  padding: calc(var(--sp) * 1.25); border: 1px solid var(--panel-border);
  border-radius: var(--radius); margin-top: var(--sp);
  background: rgba(13, 21, 38, 0.5);
}
.research-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp); }
.research-name { font-weight: 600; letter-spacing: 0.02em; }
.research-card .b-desc { margin: 4px 0 8px; }
.research-card .b-cost-row { margin-bottom: 6px; }
.research-card .req-list { margin: 4px 0; }
.aurium-row td { color: var(--fc-traders); }
.aurium-row .dim { color: var(--txt-dim); }

/* ============================================================================
   Phase 1.5 QoL (PHASE15.md §§1-5) — standing orders, co-pilot, daily orders,
   farm lists, map range/fleet overlays. Additive only.
   ========================================================================== */

/* ---- Standing Orders (colony Defense panel, §1) ---- */
.so-block { margin-top: calc(var(--sp) * 1.5); border-top: 1px solid var(--panel-border); padding-top: var(--sp); }
.so-head {
  font-size: 12px; letter-spacing: 0.18em; color: var(--txt-dim);
  text-transform: uppercase; margin-bottom: 6px;
}
.so-row {
  display: flex; align-items: flex-start; gap: 10px; padding: 8px 6px;
  border-radius: var(--radius); cursor: pointer;
}
.so-row:hover { background: rgba(79, 216, 255, 0.05); }
.so-check { margin-top: 3px; accent-color: var(--accent); flex: none; width: 15px; height: 15px; }
.so-check:disabled { opacity: 0.5; }
.so-main { flex: 1; min-width: 0; }
.so-label { font-weight: 600; font-size: 15px; }
.so-desc { font-size: 14px; margin-top: 2px; max-width: 640px; }
.so-warn { font-size: 13.5px; margin-top: 3px; max-width: 640px; }
.so-tip { margin-left: 6px; vertical-align: middle; }

/* ---- Co-pilot settings block (Overview, §2) ---- */
/* Now a grid-cell sibling of .colony-card (owner request: move it beside the
   colony card, shrink it) — the long explanatory paragraph became an
   on-demand ⓘ tooltip (info-tip below) instead of always-on body text, so
   the card reads at a glance instead of as a wall of text. */
.copilot-blurb { font-size: 14px; padding: 2px 6px 8px; max-width: 720px; }
.copilot-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 4px 6px; }
.copilot-k { font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase; min-width: 92px; }
.copilot-current { font-weight: 600; }
.copilot-input { max-width: 200px; }
.copilot-hint { font-size: 14px; }
.copilot-shield-btn { color: var(--ok); border-color: rgba(61, 220, 151, 0.4); }
/* Owner: "put the appoint button to the right after the commander name
   input field" — back to a plain row (input + Appoint side by side),
   reverting v0.57.8/v0.57.15's stacked layout. `.copilot-row`'s own
   default `align-items: center` is enough now that this is one row again. */
.copilot-appoint { display: flex; flex-direction: row; align-items: center; gap: 8px; }

.copilot-compact .copilot-row { padding: 4px 2px; gap: 8px; }
.copilot-compact .copilot-k { min-width: 0; font-size: 13px; }
.copilot-compact .copilot-input { max-width: 130px; }
.copilot-compact .copilot-for { flex-direction: column; align-items: flex-start; gap: 2px; }
.copilot-compact .copilot-hint { display: block; margin-top: 2px; }

/* A small "ⓘ" that reveals the full explanation via the native title
   tooltip on hover/focus — no JS popup needed for something this short. */
.info-tip {
  display: inline-flex; align-items: center; justify-content: center;
  margin-left: auto; width: 16px; height: 16px; border-radius: 50%;
  font-size: 13px; color: var(--txt-faint); cursor: help;
  border: 1px solid var(--panel-border);
}
.info-tip:hover, .info-tip:focus-visible { color: var(--accent); border-color: var(--panel-border-hi); outline: none; }

/* v0.57.5: owner — the native `title` tooltip (.info-tip above) "very ugly,
   make it act like a button and pop up the information in [a] new window
   in the middle." Same round "ⓘ" footprint, but a real <button> (cursor:
   pointer, no `title`) that opens openOverlay's centered 'panel' overlay —
   the exact "new window in the middle" every other info/detail popup in
   the game already uses, instead of the browser's own tooltip chrome. */
.info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  margin-left: auto; width: 18px; height: 18px; border-radius: 50%;
  font-size: 14px; color: var(--txt-faint); cursor: pointer;
  background: transparent; border: 1px solid var(--panel-border);
  padding: 0; line-height: 1; transition: color 0.15s, border-color 0.15s;
}
.info-btn:hover, .info-btn:focus-visible {
  color: var(--accent); border-color: var(--panel-border-hi); outline: none;
  box-shadow: 0 0 8px rgba(79, 216, 255, 0.35);
}
.info-btn-body { font-size: 15px; line-height: 1.55; padding: 4px 2px; }
.copilot-shield-btn:hover:not(:disabled) { background: rgba(61, 220, 151, 0.12); }

/* ---- Daily Orders (Directives page, §3) ---- */
.daily-panel { --panel-accent: var(--panel-border-hi); }
.daily-reset { font-size: 13px; letter-spacing: 0.05em; text-transform: none; }
.daily-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--sp); padding: 2px;
}
.daily-card {
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  background: rgba(13, 21, 38, 0.55); padding: calc(var(--sp) * 1.25);
  display: flex; flex-direction: column; gap: 7px;
}
.daily-card.daily-claimable {
  border-color: rgba(255, 176, 46, 0.5);
  box-shadow: inset 0 0 24px rgba(255, 176, 46, 0.05);
}
.daily-card.daily-done { opacity: 0.68; }
.daily-title { font-weight: 700; font-size: 15.5px; display: flex; align-items: center; gap: 7px; }
.daily-glyph { color: var(--warn); }
.daily-done .daily-glyph { color: var(--ok); }
.daily-text { font-size: 14px; }
.daily-prog { display: flex; align-items: center; gap: 8px; }
.daily-bar { flex: 1; height: 7px; }
.daily-fill-done { background: var(--ok); }
.daily-count { font-size: 13px; color: var(--txt-dim); flex: none; }
.daily-foot { margin-top: auto; display: flex; justify-content: flex-end; }
.daily-note { font-size: 13.5px; padding: 8px 6px 2px; }

/* ---- Farm lists (Fleet page, §4) ---- */
.farm-panel { margin-top: calc(var(--sp) * 2); }
.farm-blurb { font-size: 14px; padding: 2px 6px 8px; max-width: 760px; }
.farm-create-row { display: flex; gap: 8px; align-items: center; padding: 0 6px 8px; flex-wrap: wrap; }
.farm-create-row input { max-width: 260px; }
.farm-list {
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  background: rgba(13, 21, 38, 0.5); padding: calc(var(--sp) * 1.25);
  margin-top: var(--sp);
}
.farm-list-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.farm-list-name { font-weight: 700; letter-spacing: 0.02em; }
.farm-list-spacer { flex: 1; }
.farm-launch-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.farm-launch-row select { max-width: 240px; }
.farm-cool { font-size: 14px; }
.farm-table { margin-top: 6px; }
.farm-table td { vertical-align: middle; }
.farm-scan { display: inline-flex; align-items: center; gap: 6px; }
.farm-chip {
  font-size: 13px; font-family: var(--mono); padding: 2px 8px; border-radius: 10px;
  white-space: nowrap;
}
.farm-chip-ok { color: var(--ok); background: rgba(61, 220, 151, 0.1); border: 1px solid rgba(61, 220, 151, 0.35); }
.farm-chip-skip { color: var(--warn); background: rgba(255, 176, 46, 0.08); border: 1px solid rgba(255, 176, 46, 0.35); }
.farm-add { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px;
  border-top: 1px dashed var(--panel-border); padding-top: 8px; }
.farm-add-coords { display: flex; align-items: center; gap: 4px; }
.farm-picker { display: flex; gap: 6px; flex-wrap: wrap; }
.farm-pick { display: inline-flex; align-items: center; gap: 3px; cursor: pointer; }
.farm-pick .q-ic { font-size: 15px; }
.farm-qty { width: 52px; padding: 3px 4px; font-size: 14px; }

/* anchored "add to which list?" chooser (galaxy / map / reports) */
.farm-menu {
  position: fixed; z-index: 90; min-width: 220px; max-width: 320px;
  background: var(--panel-solid); border: 1px solid var(--panel-border-hi);
  border-radius: 10px; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  padding: 6px; display: flex; flex-direction: column; gap: 2px;
}
.farm-menu-head { font-size: 13px; padding: 4px 8px; }
.farm-menu-item {
  font: inherit; text-align: left; display: flex; justify-content: space-between;
  align-items: center; gap: 10px; padding: 7px 9px; border: 0; border-radius: 6px;
  background: transparent; color: var(--txt); cursor: pointer;
}
.farm-menu-item:hover { background: rgba(79, 216, 255, 0.1); }
.farm-menu-item:disabled { opacity: 0.5; cursor: default; }
.farm-menu-name { font-weight: 600; }
.farm-menu-units { font-size: 13px; }

/* ---- Map overlays (§5): range control bar + legend title ---- */
.map-rangebar {
  position: absolute; top: var(--sp); left: var(--sp);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; max-width: 380px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
  padding: 7px 10px; backdrop-filter: blur(8px); font-size: 14px;
}
.map-rangebar select { font-size: 14px; max-width: 190px; }
.map-rangebar-k { color: var(--txt-dim); letter-spacing: 0.08em; text-transform: uppercase; font-size: 12px; }
.map-lg-title { color: var(--txt); font-weight: 600; letter-spacing: 0.04em; margin-bottom: 4px; }

@media (max-width: 860px) {
  .daily-grid { grid-template-columns: 1fr; }
  .farm-add { flex-direction: column; align-items: stretch; }
  .map-rangebar { max-width: calc(100% - 16px); }
}

/* ============================================================================
   Phase 2c (PHASE2C.md §6) — territory claim/contest, controlled territories,
   the Galactic Ascendancy panel + controller board. Additive only.
   ========================================================================== */

/* shared danger button (contest) + rare tag */
.btn-danger {
  --btn-accent: rgba(255, 77, 93, 0.42);
  --btn-fill: color-mix(in srgb, var(--danger) 14%, var(--bg0));
  color: var(--danger);
}
.btn-danger:hover:not(:disabled) {
  --btn-accent: rgba(255, 77, 93, 0.6);
  --btn-fill: color-mix(in srgb, var(--danger) 22%, var(--bg0));
}
.tag-rare { background: rgba(255, 209, 102, 0.16); color: var(--fc-traders); margin-left: 4px; }

/* ---- map territory inspector: CLAIM / CONTEST blocks ---- */
.claim-block, .contest-block {
  margin-top: var(--sp); padding: 10px; border-radius: var(--radius);
  border: 1px solid var(--panel-border);
}
.claim-block { background: rgba(255, 209, 102, 0.05); border-color: rgba(255, 209, 102, 0.26); }
.contest-block { background: rgba(255, 77, 93, 0.05); border-color: rgba(255, 77, 93, 0.26); }
.claim-head, .contest-head { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
.claim-head { color: var(--fc-traders); }
.contest-head { color: var(--danger); }
.claim-sub, .contest-sub { font-size: 13.5px; line-height: 1.4; margin-bottom: 8px; }
.claim-cost { margin-bottom: 8px; }
.claim-forcol, .contest-forcol { margin-bottom: 8px; }
.claim-forcol-note { font-size: 13px; margin-bottom: 8px; }
.claim-forcol select, .contest-forcol select { width: 100%; }
.claimed-note .dim { font-weight: 400; }

/* ---- colony: Controlled Territories list ---- */
.claims-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.claim-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--radius);
  border: 1px solid var(--panel-border); background: rgba(13, 21, 38, 0.5);
}
.claim-ic { font-size: 18px; color: var(--fc-traders); flex: none; }
.claim-main { flex: 1; min-width: 0; }
.claim-title { font-weight: 600; font-size: 15px; }
.claim-row .claim-sub { font-size: 13.5px; margin: 2px 0 0; }

/* ---- season: Galactic Ascendancy panel ---- */
.ascendancy-panel { --panel-accent: rgba(255, 209, 102, 0.28); }
.victory-ascendancy { --panel-accent: rgba(255, 209, 102, 0.5); box-shadow: 0 0 22px rgba(255, 209, 102, 0.18); }
.asc-conditions { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 8px; }
.asc-cond { display: flex; align-items: baseline; gap: 8px; font-size: 14.5px; }
.asc-cond-num { color: var(--fc-traders); font-weight: 700; flex: none; }
.asc-cond-label { font-weight: 600; min-width: 68px; flex: none; }
.asc-cond-text { line-height: 1.4; }
.asc-note { font-size: 13.5px; line-height: 1.4; margin-bottom: 8px; }
.ascendancy-table th, .ascendancy-table td { white-space: nowrap; }
.asc-check .asc-mark { font-weight: 700; }
.asc-ok .asc-mark { color: var(--ok); }
.asc-pending { color: var(--txt-dim); }
.asc-pending .asc-mark { color: var(--txt-dim); }

@media (max-width: 860px) {
  .claim-row { flex-wrap: wrap; }
}

/* ============================================================================
   PHASE 3 — WebGL "model hero" (PHASE3-WEBGL.md §5/§6/§8)
   A lazy 3D viewer that occupies the icon slot a 2D glyph used to fill, with a
   HARD fallback to that glyph. The hero box reserves the SAME footprint as the
   glyph it replaces (JS sets width/height per kind: building/unit 38, commander
   58, mega 96) so there is NO layout shift on fallback. The dark "glyph box"
   chrome that .b-glyph/.u-glyph/.player-glyph provided is reproduced here so a
   WebGL-off client looks byte-identical to today. Cosmetic only.
   ========================================================================== */

/* The reserved square box. width/height come inline from modelHero(); this only
   supplies the chrome (dark inset panel), clips the canvas to the radius, and
   centres the fallback glyph. */
.model-hero {
  flex: none;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: rgba(5, 7, 15, 0.6);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  color: var(--accent);
  line-height: 1;
}

/* The per-card 2D <canvas> the shared renderer blits into (mv-canvas), plus the
   fallback glyph span (mv-glyph). Both must exactly fill the box. */
.model-hero .mv-canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
}
.mv-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 24px;          /* matches the old .b-glyph/.u-glyph default */
  user-select: none;
}

/* Interactive heroes (click/Enter → orbit modal): affordance + focus ring. */
.model-hero.mv-interactive { cursor: pointer; }
.model-hero.mv-interactive:hover { border-color: var(--panel-border-hi); }
.model-hero.mv-interactive:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* -- Per-surface sizing so each hero reads exactly like the glyph it replaced -- */

/* Shipyard units: 64px box, 34px glyph, accent colour (enlarged from the
   original 38px/22px — too small to read hull detail on a full-fleet grid).
   The box's width/height are actually set INLINE by modelHero()'s `size`
   param (shipyard.js now passes size:64 explicitly) — this rule only sizes
   the fallback glyph font, since that's genuinely CSS-driven (no inline
   override exists for font-size). Colony building heroes size themselves
   the same explicit way (44px tiles, 72px detail overlay — see
   structure-tile / b-detail-head in colony.js). */
.u-card-head .model-hero .mv-glyph { font-size: 36px; }

/* Commander profile: 58px box, 30px glyph, brighter border (was .player-glyph). */
.player-head .model-hero {
  width: 58px; height: 58px;
  border-color: var(--panel-border-hi);
}
.player-head .model-hero .mv-glyph { font-size: 32px; }

/* Faction-tinted heroes (commander + faction ships) read as that faction. The
   glyph colour follows the fc-* token; the model itself is already faction art. */
.model-hero.fc-combine { color: var(--fc-combine); }
.model-hero.fc-traders { color: var(--fc-traders); }
.model-hero.fc-hegemony { color: var(--fc-hegemony); }
.model-hero.fc-vayu { color: var(--fc-vayu); }

/* -- Season Nexus showpiece: a centred panel hero above the race standings. -- */
.mega-hero-row {
  display: flex;
  align-items: center;
  gap: calc(var(--sp) * 1.5);
  padding: 4px 0 10px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--panel-border);
}
.mega-hero-row .model-hero {
  width: 96px; height: 96px;
  border-color: var(--panel-border-hi);
  background:
    radial-gradient(60% 60% at 50% 40%, rgba(79, 216, 255, 0.10), transparent 70%),
    rgba(5, 7, 15, 0.7);
}
.mega-hero-row .model-hero .mv-glyph { font-size: 50px; color: var(--accent); }
.mega-hero-cap { font-size: 14px; line-height: 1.4; flex: 1; min-width: 0; }

/* ============================================================================
   PHASE 3 — orbit modal (PHASE3-WEBGL.md §6/§8)
   A focus-trapped, Esc-closable, labelled dialog that hosts an INTERACTIVE
   (drag-to-orbit) viewer. Matches the dark console palette; returns focus to the
   opener on close (handled in model-viewer.js).
   ========================================================================== */

.mv-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--sp) * 2);
  background: rgba(3, 5, 12, 0.72);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: mv-fade-in 0.14s ease-out;
}
@keyframes mv-fade-in { from { opacity: 0; } to { opacity: 1; } }

.mv-modal-dialog {
  display: flex;
  flex-direction: column;
  width: min(560px, 92vw);
  max-height: 88vh;
  background: var(--panel-solid);
  border: 1px solid var(--panel-border-hi);
  border-radius: calc(var(--radius) + 2px);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(79, 216, 255, 0.06);
  overflow: hidden;
}

.mv-modal-head {
  display: flex;
  align-items: center;
  gap: var(--sp);
  padding: calc(var(--sp) * 1.25) calc(var(--sp) * 1.5);
  border-bottom: 1px solid var(--panel-border);
}
.mv-modal-title {
  flex: 1;
  min-width: 0;
  font-weight: 700;
  font-size: 16px;
  letter-spacing: 0.04em;
  color: var(--txt);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Faction-tint the modal title bar accent when opened from a faction hero. */
.mv-modal-dialog.fc-combine .mv-modal-title { color: var(--fc-combine); }
.mv-modal-dialog.fc-traders .mv-modal-title { color: var(--fc-traders); }
.mv-modal-dialog.fc-hegemony .mv-modal-title { color: var(--fc-hegemony); }
.mv-modal-dialog.fc-vayu .mv-modal-title { color: var(--fc-vayu); }

.mv-modal-close {
  flex: none;
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5, 7, 15, 0.6);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  color: var(--txt-dim);
  font-size: 16px;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s;
}
.mv-modal-close:hover { color: var(--danger); border-color: var(--danger); }
.mv-modal-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The 3D stage: a large canvas host. The viewer's mv-canvas fills it; the
   fallback glyph (mv-glyph-lg) is shown big and centred if the model can't load. */
.mv-modal-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  max-height: 62vh;
  background:
    radial-gradient(70% 70% at 50% 45%, rgba(18, 29, 51, 0.9), var(--bg0) 78%);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.mv-modal-stage .mv-canvas { width: 100%; height: 100%; display: block; }
.mv-glyph-lg {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
  font-size: 98px;
  color: var(--accent);
  opacity: 0.8;
  user-select: none;
}

.mv-modal-hint {
  padding: calc(var(--sp)) calc(var(--sp) * 1.5);
  font-size: 13.5px;
  text-align: center;
  border-top: 1px solid var(--panel-border);
}
/* v0.57.15: the ship-class flavor text, moved here off the compact card. */
.mv-modal-desc {
  padding: 0 calc(var(--sp) * 1.5) calc(var(--sp) * 1.5);
  font-size: 15px; line-height: 1.5;
  color: var(--txt-dim);
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
}

@media (prefers-reduced-motion: reduce) {
  .mv-modal-overlay { animation: none; }
}

@media (max-width: 860px) {
  .mega-hero-row { flex-wrap: wrap; }
  .mv-modal-stage { max-height: 54vh; }
}

/* ===================================== Phase 4A — Prestige & Spectacle */
/* The Crowning (steeper RECOGNITION), the Final-Push ribbon, the Highlight Reel.
   Gold = champion (#ffd166 / --fc-traders); danger = the final push. */

.champion-crown {
  display: flex; align-items: center; gap: calc(var(--sp) * 2);
  --panel-accent: rgba(255, 209, 102, 0.55);
  /* Radial highlight layered over an opaque base, not the old translucent
     rgba() base — .panel-framed's --panel-fill now paints on top of its
     own border-color layer rather than the page, so the base stop must
     be fully opaque; the translucent radial highlight on top of it still
     composites exactly the same way it always did. */
  --panel-fill:
    radial-gradient(130% 150% at 0% 0%, rgba(255, 209, 102, 0.15), transparent 60%),
    color-mix(in srgb, rgb(38, 31, 10) 55%, var(--bg0));
  box-shadow: 0 0 26px rgba(255, 209, 102, 0.14), inset 0 0 0 1px rgba(255, 209, 102, 0.10);
}
.champion-crown.champion-ascendancy { box-shadow: 0 0 32px rgba(255, 209, 102, 0.26); }
.crown-glyph {
  font-size: 48px; line-height: 1; color: #ffd166; flex: none;
  text-shadow: 0 0 18px rgba(255, 209, 102, 0.6);
}
.crown-body { min-width: 0; }
.crown-eyebrow { font-size: 12px; letter-spacing: 0.22em; color: #ffd166; opacity: 0.85; }
.crown-title { font-size: 24px; font-weight: 700; letter-spacing: 0.04em; margin: 3px 0; }
.crown-name { color: var(--txt); }
.crown-sub { font-size: 14px; }

.sovereign-callout { display: flex; align-items: center; gap: calc(var(--sp) * 2); --panel-accent: var(--panel-border-hi); }
.sov-glyph { font-size: 30px; color: #ffd166; text-shadow: 0 0 12px rgba(255, 209, 102, 0.5); flex: none; }
.sov-body { min-width: 0; }
.sov-name { font-size: 18px; font-weight: 700; }
.sov-pts { font-weight: 400; font-size: 15px; }

.final-push-ribbon {
  display: flex; align-items: center; gap: var(--sp);
  --panel-accent: rgba(255, 77, 93, 0.5);
  /* The ribbon glow (unchanged) used to fade to true page transparency;
     now it fades into the panel's own opaque base fill instead (appended
     as a second, bottom background layer) since --panel-fill paints over
     --panel-accent, not the page — visually near-identical, since the
     page behind panels is dark navy too. */
  /* v0.57.4: same --bg2/--bg1 recolor as .panel-framed's base, same reason. */
  --panel-fill:
    linear-gradient(90deg, rgba(255, 77, 93, 0.14), rgba(255, 176, 46, 0.06) 70%, transparent),
    linear-gradient(180deg, color-mix(in srgb, var(--bg2) 90%, var(--bg0)), color-mix(in srgb, var(--bg1) 94%, var(--bg0)));
}
.fp-pulse { font-size: 22px; color: var(--danger); animation: fp-throb 1.1s ease-in-out infinite; }
.fp-text { font-weight: 700; letter-spacing: 0.18em; color: var(--danger); font-size: 15px; }
@keyframes fp-throb { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.55; transform: scale(1.18); } }

.spectacle-feed .feed-line { font-size: 14.5px; padding: 2px 0; }
.spectacle-feed .feed-line + .feed-line { border-top: 1px solid rgba(79, 216, 255, 0.06); }

.highlights-panel .reel { display: flex; flex-direction: column; gap: 2px; }
.reel-item {
  display: flex; align-items: baseline; gap: var(--sp);
  padding: 6px 8px; border-radius: var(--radius); border: 1px solid transparent;
}
.reel-item:nth-child(odd) { background: rgba(79, 216, 255, 0.035); }
.reel-glyph { color: var(--accent2); flex: none; width: 18px; text-align: center; }
.reel-summary { flex: 1; min-width: 0; font-size: 15px; }
.reel-time { flex: none; font-size: 13px; }
.reel-item.reel-win { border-color: rgba(255, 209, 102, 0.45); background: rgba(255, 209, 102, 0.10); }
.reel-item.reel-win .reel-glyph { color: #ffd166; text-shadow: 0 0 10px rgba(255, 209, 102, 0.5); }
.reel-item.reel-win .reel-summary { font-weight: 700; }

@media (prefers-reduced-motion: reduce) {
  .fp-pulse { animation: none; }
}

/* Phase 4B — Hall of Legends (cross-reset roll of past champions). Gold, like The Crowning. */
.hall-panel { --panel-accent: rgba(255, 209, 102, 0.28); }
.hall-list { display: flex; flex-direction: column; gap: 4px; }
.hall-row {
  display: flex; align-items: center; gap: var(--sp);
  padding: 8px; border-radius: var(--radius);
  border: 1px solid transparent; background: rgba(255, 209, 102, 0.05);
}
.hall-row:hover { border-color: rgba(255, 209, 102, 0.28); }
.hall-crest {
  font-size: 24px; color: #ffd166; flex: none; width: 26px; text-align: center;
  text-shadow: 0 0 10px rgba(255, 209, 102, 0.4);
}
.hall-main { min-width: 0; flex: 1; }
.hall-title { font-size: 17px; font-weight: 700; }
.hall-name { color: var(--txt); }
.hall-asc { background: rgba(255, 209, 102, 0.16); color: #ffd166; margin-left: 6px; font-size: 12px; }
.hall-sub { font-size: 14px; margin-top: 1px; }

/* Phase 4C — durable accounts + the Legend badge. */
.account-panel { --panel-accent: var(--panel-border-hi); }
.account-form { display: flex; flex-direction: column; gap: 8px; }
.account-form-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.account-form input {
  flex: 1 1 140px; min-width: 120px;
  background: var(--bg2); color: var(--txt);
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  padding: 6px 8px; font: inherit;
}
.account-form input:focus { outline: none; border-color: var(--accent); }
.account-or { font-size: 14px; }
.account-titles { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; align-items: center; }

.legend-badge-panel {
  --panel-accent: rgba(255, 209, 102, 0.4);
  --panel-fill: color-mix(in srgb, rgb(255, 209, 102) 4%, var(--bg0));
}
.legend-badge { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.legend-handle { font-weight: 700; color: #ffd166; }
.legend-title-chip {
  display: inline-block; padding: 2px 9px; border-radius: 999px; font-weight: 700; font-size: 14px;
  color: #ffd166; background: rgba(255, 209, 102, 0.14); border: 1px solid rgba(255, 209, 102, 0.3);
}

/* v0.32.6 — Captain Legacy: a trophy-only career record (no gameplay effect —
   a new season's Captain always starts at zero). Reuses .cap-tier-* colors. */
.captain-legacy { margin-top: 4px; border-top: 1px dashed var(--panel-border); padding-top: 8px; }
.captain-legacy-head { font-size: 13.5px; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }
.captain-legacy-row {
  display: flex; gap: 10px; align-items: baseline; font-size: 15px;
  padding: 3px 0; border-bottom: 1px solid var(--panel-border);
}
.captain-legacy-row:last-child { border-bottom: none; }

/* Phase 4D — cosmetic auras (rings the model-hero box) + the Wardrobe. */
.mv-aura { border-radius: 50%; }
.mv-aura.mv-aura-soft {
  box-shadow: 0 0 10px 1px var(--aura-color, var(--accent));
  outline: 1px solid var(--aura-color, var(--accent)); outline-offset: -1px;
}
.mv-aura.mv-aura-strong {
  outline: 2px solid var(--aura-color, #ffd166); outline-offset: -1px;
  animation: aura-pulse 2.4s ease-in-out infinite;
}
@keyframes aura-pulse {
  0%, 100% { box-shadow: 0 0 12px 2px var(--aura-color, #ffd166), inset 0 0 6px 0 var(--aura-color, #ffd166); }
  50%      { box-shadow: 0 0 22px 5px var(--aura-color, #ffd166), inset 0 0 10px 0 var(--aura-color, #ffd166); }
}

/* v0.32.7 — the Trophy Case: a gold-styled showcase of earned-only cosmetics,
   distinct from the plain free-claim Wardrobe grid below. */
.trophy-case {
  margin-top: 4px; border-top: 1px dashed var(--panel-border); padding-top: 8px;
}
.trophy-case-head {
  font-size: 14px; font-weight: 700; letter-spacing: 0.04em; margin-bottom: 8px;
  color: #ffd166;
}
.trophy-case-grid { display: flex; flex-direction: column; gap: 8px; }
.trophy-card {
  display: flex; align-items: center; gap: 12px; padding: 8px 10px;
  border: 1px solid rgba(255, 209, 102, 0.3); border-radius: var(--radius);
  background: rgba(255, 209, 102, 0.05);
}
.trophy-swatch {
  flex-shrink: 0; width: 26px; height: 26px; border-radius: 50%;
  background: radial-gradient(circle, var(--aura-color) 0%, transparent 72%);
  box-shadow: 0 0 12px 2px var(--aura-color); border: 1px solid var(--aura-color);
}
.trophy-name { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 6px; }
.trophy-desc { font-size: 13px; line-height: 1.35; margin-top: 2px; }

.wardrobe-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; padding: 8px; }
.wardrobe-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  padding: 10px; border: 1px solid var(--panel-border); border-radius: var(--radius); background: var(--bg2);
}
.wardrobe-card.wardrobe-equipped { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.wardrobe-swatch {
  width: 28px; height: 28px; border-radius: 50%;
  background: radial-gradient(circle, var(--aura-color) 0%, transparent 72%);
  box-shadow: 0 0 12px 2px var(--aura-color); border: 1px solid var(--aura-color);
}
.wardrobe-name { font-weight: 700; font-size: 15px; }
.wardrobe-desc { font-size: 13px; line-height: 1.35; }
.wardrobe-locked { background: var(--bg3); color: var(--txt-dim); }
/* Phase 4D-2 — Wardrobe kind sections (Auras / Hull Skins / Engine Trails). */
.wardrobe-section + .wardrobe-section { margin-top: 10px; }
.wardrobe-section-label { font-size: 12px; letter-spacing: 0.16em; padding: 0 8px 4px; }

@media (prefers-reduced-motion: reduce) { .mv-aura.mv-aura-strong { animation: none; } }

/* Phase 4C-2a — login: account-auth switch link + intro blurbs. */
.login-switch { margin-top: 12px; text-align: center; font-size: 15px; }
.login-switch a { font-weight: 600; }
.login-blurb { font-size: 14px; line-height: 1.45; margin-bottom: 10px; }
/* Phase 4C-2b: inline success note on the verify / reset / forgot panels. */
.login-ok { color: var(--ok); margin-top: 10px; }

/* ============================================================================
   UI SHELL — Popup/Overlay System (void-reach-ui-shell-and-art-brief.md
   §1). #overlay-root is appended to <body> once by overlay.js; every open
   overlay is a direct child so z-index stacking is simple sibling ordering.
   'screen' overlays get a dimmed backdrop; 'panel' overlays (tooltips, building
   info, confirmations) float without one, like a popover. z-index base 1000+
   sits below the existing 3D orbit modal (2000, model-viewer.js) so a hero
   inside an overlay can still open its own modal on top; toasts (2600) stay
   above everything.
   ========================================================================== */

.ov-backdrop {
  position: fixed; inset: 0;
  background: rgba(5, 7, 15, 0.62);
  backdrop-filter: blur(2px);
  animation: ov-fade-in 0.15s ease-out;
}

/* v0.55.21 — dropped the raster corner-bracket frame (panel-overlay-frame/
   shell.png) entirely and switched .ov-box to the SAME "HOLO GUI" clip-path
   chamfer language as .panel-framed (owner: "what's the point of this?",
   after the raster asset had already caused this exact stray-line bug class
   repeatedly across the v0.55.x arc). One fewer rendering system in the
   game, and the overlay chrome now matches every panel's own chrome by
   construction instead of by lookalike raster art. */
/* v0.58.0: same double-layer clip-path fill as .panel-framed (see its
   comments for the full rationale) instead of `border` + `clip-path` +
   a corner wedge — fixes the same cross-browser "missing edges" seam.
   `--ov-fill` doesn't need color-mix() baking: `--panel-solid` is
   already a fully opaque hex color, not a translucent one. */
.ov-box {
  --ov-accent: var(--accent);
  --ov-border-w: 2px;
  --ov-fill: var(--panel-solid);
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: flex; flex-direction: column;
  isolation: isolate;
  background: transparent;
  border: none;
  border-radius: 0;
  /* Same two-opposite-corners cut as .panel-framed (top-right + bottom-left)
     — clip-path clips children to this shape too, the same mechanism as
     overflow:hidden but to a custom polygon, so the old manual
     overflow:hidden hard-clip is no longer needed. */
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer)) 0, 100% var(--chamfer),
    100% 100%, var(--chamfer) 100%, 0 calc(100% - var(--chamfer))
  );
  box-shadow: 0 0 22px color-mix(in srgb, var(--ov-accent) 28%, transparent), var(--shadow-panel), var(--shadow-glow);
  animation: ov-fade-in 0.15s ease-out;
  max-height: 88vh;
}
.ov-box::before, .ov-box::after {
  content: '';
  position: absolute;
  clip-path: inherit;
  pointer-events: none;
}
.ov-box::before { inset: 0; background: var(--ov-accent); z-index: -2; }
.ov-box::after { inset: var(--ov-border-w); background: var(--ov-fill); z-index: -1; }
.ov-panel { width: min(440px, 92vw); }
.ov-screen { width: min(1100px, 94vw); height: min(760px, 86vh); }

@keyframes ov-fade-in {
  from { opacity: 0; transform: translate(-50%, -50%) translateY(6px); }
  to { opacity: 1; transform: translate(-50%, -50%); }
}

/* Same title-strip treatment as .panel.panel-framed > .sec-h:first-child —
   owner asked for the overlay's own title to read as the same headline
   style as an ordinary panel's section header, not a separate look.
   position:relative + z-index:1 is now redundant (.ov-box's border/fill
   pseudo-elements sit at negative z-index — see .ov-box::before — so any
   real child stacks above them automatically) but harmless, and left in
   place rather than risk missing some other reason it was here. */
.ov-head {
  position: relative; z-index: 1;
  flex: none;
  display: flex; align-items: center; gap: var(--sp);
  background: linear-gradient(180deg, rgba(79, 216, 255, 0.16), rgba(79, 216, 255, 0.05));
  border-bottom: 1px solid rgba(79, 216, 255, 0.45);
  padding: 10px calc(var(--sp) * 1.5) 10px calc(var(--sp) * 1.5 + var(--chamfer));
}
.ov-title {
  flex: 1; min-width: 0;
  font-family: var(--font-display);
  font-size: 13px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--txt-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ov-close {
  flex: none; width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--panel-border);
  border-radius: var(--radius); color: var(--txt-dim);
  cursor: pointer; font-size: 16px; line-height: 1;
  transition: color 0.12s, border-color 0.12s;
  /* Nudged clear of .ov-box's own top-right chamfer cut — without this the
     button's own corner would sit outside the parent's clip-path polygon
     and get clipped off, not just visually collide with the wedge. */
  transform: translate(calc(var(--chamfer) * -0.35), calc(var(--chamfer) * 0.15));
}
.ov-close:hover { color: var(--danger); border-color: var(--danger); }
.ov-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ov-body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: calc(var(--sp) * 1.5);
}
/* Owner: scrolled content was getting hard-clipped at the top/bottom edge of
   this scroll container with zero transition — a section's own framed
   border would just vanish mid-scroll, reading as a rendering glitch. A
   `mask-image` fade on .ov-body itself (tried first) made it WORSE — fading
   a crisp 1-2px border line to transparent over a short band doesn't read
   as a soft vignette, it reads as a rigid rectangle getting wiped away,
   which is exactly the "like there's a layer sliding underneath" look the
   owner called out a second time. The standard fix for this (macOS/Discord-
   style scroll fade) is a SCRIM, not a content fade: an opaque-to-transparent
   gradient painted OVER the scrolled content near each edge, so a border
   appears to slide UNDER a solid panel rather than dissolve pixel-by-pixel.
   `::before`/`::after` as sticky flow children (negative margin collapses
   the space they'd otherwise reserve, so they overlay instead of pushing
   content down) pin them to the visible top/bottom of the scroll regardless
   of scroll position. */
.ov-body::before, .ov-body::after {
  content: ''; position: sticky; left: 0; right: 0; height: 24px;
  display: block; pointer-events: none; z-index: 5;
}
.ov-body::before {
  top: 0; margin-bottom: -24px;
  background: linear-gradient(to bottom, var(--panel-solid), transparent);
}
.ov-body::after {
  bottom: 0; margin-top: -24px;
  background: linear-gradient(to top, var(--panel-solid), transparent);
}

.ov-footer {
  flex: none;
  display: flex; align-items: center; justify-content: flex-end; gap: var(--sp);
  padding: var(--sp) calc(var(--sp) * 1.5);
}

/* Owner: "confirm_modal_frame.svg ... use this as a confirmation window if
   needed somewhere" — see confirmModal() in overlay.js. Reuses the SAME
   .ov-head/.ov-body/.ov-footer structure every overlay already has (kicker
   title, close ✕, body, footer), just reskinned: the shared clip-path
   chamfer is swapped for the generated frame image (its own double frame +
   glowing corner brackets + two divider lines), and head/body/footer lose
   their default backgrounds/borders since the frame already draws those
   dividers. `100% 100%` stretch, not aspect-ratio-locked — same reasoning
   as the alert-panel/resource-bar frames: pure chrome, and confirmation
   body text length varies per call site so the box needs to grow, not clip. */
.ov-confirm {
  clip-path: none; box-shadow: none;
  background: url('/img/panels/confirm-modal.svg?v=2') no-repeat center / 100% 100%;
  width: min(420px, 92vw); min-height: 250px;
}
.ov-confirm::before, .ov-confirm::after { content: none; }
.ov-confirm .ov-head {
  background: none; border-bottom: none;
  justify-content: center; padding: 24px calc(var(--sp) * 1.5) 2px;
}
.ov-confirm .ov-title { flex: none; text-align: center; font-size: 11px; letter-spacing: 0.3em; }
.ov-confirm .ov-close { transform: translate(2px, 2px); }
.ov-confirm .ov-body {
  flex: none; overflow: visible; text-align: center;
  padding: 14px calc(var(--sp) * 2.5) 18px;
}
.ov-confirm-headline { font-size: 20px; font-weight: 700; color: var(--txt); margin-bottom: 12px; }
.ov-confirm-desc { color: #8AA2B8; font-size: 13px; line-height: 1.6; }
.ov-confirm .ov-footer {
  justify-content: center; gap: calc(var(--sp) * 2);
  padding: 18px calc(var(--sp) * 2.5) 22px;
}
.ov-confirm-btn {
  flex: 1; max-width: 130px; padding: 9px 0;
  font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  border-radius: 2px; cursor: pointer; transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.ov-confirm-cancel { background: none; border: 1px solid #2D3B55; color: #8AA2B8; }
.ov-confirm-cancel:hover { border-color: var(--panel-border-hi); color: var(--txt); }
.ov-confirm-danger { background: #2a1214; border: 1px solid #FF4D4D; color: #FF4D4D; }
.ov-confirm-danger:hover { background: rgba(255, 77, 93, 0.25); color: #fff; box-shadow: 0 0 10px rgba(255, 77, 93, 0.35); }
.ov-confirm-primary { background: rgba(79, 216, 255, 0.14); border: 1px solid var(--accent); color: var(--accent); }
.ov-confirm-primary:hover { background: rgba(79, 216, 255, 0.28); color: #fff; box-shadow: 0 0 10px rgba(79, 216, 255, 0.35); }

/* v0.57.9: bell/settings/logout used to share `.btn`'s chamfered frame,
   whose `clip-path` cuts a diagonal off the top-right corner — exactly
   where the notification badge sits, so the count rendered partially
   clipped. A plain circular frame (no clip-path, so nothing outside the
   button's own box gets cut) fixed that.
   v2 (owner, pixel-exact rebuild): galaxy-map-concept.css's `.icon-btn`
   goes further — no frame AT ALL, just the bare glyph on the open
   background (its own comment: "don't draw panel frames that aren't in
   the image"). Dropping the circle doesn't reintroduce the original
   clipping bug — that bug came specifically from `.btn`'s clip-path
   fighting the corner-positioned badge, and a bare glyph has no
   clip-path to fight it with in the first place. */
.hud-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none;
  background: none;
  border: none;
  color: var(--txt-dim);
  font-size: 18px;
  cursor: pointer;
  padding: 0;
  position: relative;
  transition: color 0.15s;
}
.hud-icon-btn:hover { color: var(--accent); }
.hud-icon-btn:active { transform: translateY(1px); }

/* Phase 5 (PHASE5-NOTIFICATIONS.md §5-6): HUD bell + notification center. */
.hud-bell { position: relative; }
.hud-bell .nav-badge {
  position: absolute; top: -4px; right: -4px; margin: 0;
}
.notif-list { display: flex; flex-direction: column; gap: 2px; }
.notif-row {
  display: flex; align-items: flex-start; gap: var(--sp);
  padding: calc(var(--sp) * 0.75) calc(var(--sp) * 0.5);
  border-radius: var(--radius);
  border-left: 2px solid transparent;
}
.notif-row.unread {
  background: var(--accent-soft);
  border-left-color: var(--accent);
}
.notif-row.clickable { cursor: pointer; }
.notif-row.clickable:hover { background: var(--bg3); }
.notif-icon { flex: none; width: 18px; text-align: center; font-size: 16px; color: var(--accent); }
.notif-body { flex: 1; min-width: 0; }
.notif-text { font-size: 14.5px; color: var(--txt); line-height: 1.35; }
.notif-time { font-size: 12.5px; margin-top: 2px; }

/* C11 (UI audit): right-edge slide-in notification drawer — replaces the
   small centered .notif-center popup. Own sibling mechanism to overlay.js
   (see notifications.js's comment on openNotificationCenter for why); the
   two z-indices below are picked to sit above EVERYTHING in the existing
   scale except toasts — above #chat-dock (z-index 50), above the HUD (40)
   and any open overlay.js popup (1000+) or the 3D orbit modal (2000), but
   still under #toasts (2600) so a toast can never be hidden behind it. */
.notif-drawer-backdrop {
  position: fixed; inset: 0;
  background: rgba(5, 7, 15, 0.55);
  z-index: 2499;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.notif-drawer-backdrop.open { opacity: 1; }

.notif-drawer {
  position: fixed;
  top: calc(var(--hud-h) + var(--activity-h)); right: 0; bottom: 0;
  width: min(400px, 100vw);
  z-index: 2500;
  display: flex; flex-direction: column;
  background: var(--panel-solid);
  border-left: 1px solid var(--panel-border-hi);
  box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5), 0 0 22px color-mix(in srgb, var(--accent) 22%, transparent);
  /* Off-screen to the right until .open flips the transform — short
     (~180ms) to match the game's other transitions (.btn/.stat hovers,
     .ov-box's own 0.15s fade-in) rather than a slower drawer-app feel. */
  transform: translateX(100%);
  transition: transform 0.18s ease;
}
.notif-drawer.open { transform: translateX(0); }

.notif-drawer-head {
  flex: none;
  display: flex; align-items: center; gap: var(--sp);
  padding: calc(var(--sp) * 1.25) calc(var(--sp) * 1.5);
  background: linear-gradient(180deg, rgba(79, 216, 255, 0.16), rgba(79, 216, 255, 0.05));
  border-bottom: 1px solid rgba(79, 216, 255, 0.45);
}
.notif-drawer-title {
  flex: 1; min-width: 0;
  font-family: var(--font-display);
  font-size: 13px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--txt-dim);
}
.notif-drawer-close {
  flex: none; width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--panel-border);
  border-radius: var(--radius); color: var(--txt-dim);
  cursor: pointer; font-size: 16px; line-height: 1;
  transition: color 0.12s, border-color 0.12s;
}
.notif-drawer-close:hover { color: var(--danger); border-color: var(--danger); }
.notif-drawer-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.notif-drawer-filters {
  flex: none;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: calc(var(--sp) * 1) calc(var(--sp) * 1.5);
  border-bottom: 1px solid var(--panel-border);
}
.notif-filter-chip {
  font-family: var(--font-body); font-size: 13px; font-weight: 600; letter-spacing: 0.04em;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--panel-border);
  background: transparent; color: var(--txt-dim);
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.notif-filter-chip:hover { color: var(--txt); border-color: var(--panel-border-hi); }
.notif-filter-chip.active {
  color: var(--accent); border-color: var(--accent); background: var(--accent-soft);
}

.notif-drawer-body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: calc(var(--sp) * 1.25);
}
/* More breathing room than the old cramped popup now that there's a full
   column of height to spend it in. */
.notif-drawer-body .notif-text { font-size: 15px; }
.notif-drawer-body .notif-row { padding: calc(var(--sp) * 0.9) calc(var(--sp) * 0.6); }

.notif-drawer-footer {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp) calc(var(--sp) * 1.5);
  border-top: 1px solid var(--panel-border);
}

@media (prefers-reduced-motion: reduce) {
  .notif-drawer, .notif-drawer-backdrop { transition: none; }
}

/* Same --hud-h:96px breakpoint everything else in the HUD/activity-strip
   scale switches on — the drawer goes full-width there rather than pinning
   an arbitrary ~400px column on a narrow viewport. */
@media (max-width: 768px) {
  .notif-drawer { width: 100vw; }
}

@media (prefers-reduced-motion: reduce) {
  .ov-backdrop, .ov-box { animation: none; }
}

@media (max-width: 640px) {
  .ov-panel { width: 94vw; }
  .ov-screen { width: 96vw; height: 92vh; }
}

/* ============================================================================
   Village Scene — PAINTED TOWN SCREEN (PHASE4-VILLAGE.md §11, HoMM3-style).
   A single rich painted background per biome — the perimeter fence/gate is
   baked directly into the painting (no separate transparent overlay), with
   epic terrain pushed to the horizon and a flat buildable clearing in the
   middle. Buildings sit on the clearing at fixed organic positions; clicking
   one opens the same detail content in a panel overlay (overlay.js §1) —
   buildingTile()'s tile itself and its states (st-empty/st-locked/
   st-building/st-maxed) are UNCHANGED; only the container/positioning and
   the .village-tile rules below are new.
   ========================================================================== */

/* `.colony-page-scene` (colony.js: applied instead of plain `.colony-page`
   in SCENE view only — LIST mode keeps the plain class, unaffected) —
   gives `.village-scene` below a real bounded height to flex-grow inside,
   instead of the page just growing to whatever its content naturally
   needs. Height mirrors #page's own available content area: 100vh minus
   the HUD, minus #page's own top+bottom padding (--sp*3 each side) and
   its chat-dock reserve (folded into the bottom padding). If either of
   those #page rules changes, this must move with it. */
.colony-page-scene {
  display: flex; flex-direction: column;
  height: calc(100vh - var(--hud-h) - var(--sp) * 6 - var(--chat-dock-h));
  height: calc(100dvh - var(--hud-h) - var(--sp) * 6 - var(--chat-dock-h)); /* MOBILE-PLAN.md §3.2: dvh overrides vh above where supported — mobile URL-bar collapse no longer clips/overflows the stage */
}
.colony-page-scene > .page-head,
.colony-page-scene > .bld-icon-row-panel { flex: none; }

/* `.colony-page-table` (◈ Table mode — colony.js) — same flex-column shape as
   `.colony-page-scene` above but a TALLER budget: measured live, the scene
   formula's `--chat-dock-h` (332px, the dock's fully-EXPANDED height) doubles
   up with hud-2026's own #page padding-bottom (100px, "dock 84 + breathing
   room" — see hud-2026.css), which already reserves for the dock. That
   worked fine for the flat scene's own historical tuning, but for a 3D
   command table there's real, measured value in reclaiming that ~250-380px
   of dead space (owner: "van bőven hely" — plenty of room to use) — the 3D
   view benefits from a taller stage far more than the flat 16:9 painting
   ever needed. Trusts #page's own padding for the dock reserve instead of
   re-subtracting it. */
.colony-page-table {
  display: flex; flex-direction: column;
  /* 140px (not a --sp multiple): measured live against a real #page — a
     smaller reserve here left #page scrolling by ~120px. This is a
     from-measurement constant, not derived from the var chain (which mixes
     an old chat-dock literal with hud-2026's own #page padding). */
  height: calc(100vh - var(--hud-h) - 140px);
  height: calc(100dvh - var(--hud-h) - 140px);
}
.colony-page-table > .page-head,
.colony-page-table > .bld-icon-row-panel { flex: none; }

/* ------ Colony 3D command table (◈ Table mode) — minimal chrome (skin A).
   The WebGL canvas (mounted by model-viewer.js's mountColonyTable) fills the
   stage; the "command table" framing is pure CSS around it (rim ring + inner
   vignette + faint scanlines), the same WebGL-stage + CSS-dressing split the
   galaxy holo table uses. flex:1 so it fills the same space .village-scene
   does in scene mode. The rich "war-room" chrome is a later skin (Phase 3). */
.colony-table-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 150px;
  margin: 0 calc(var(--sp) * -3);
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 118%, rgba(24, 52, 74, 0.55), transparent 60%),
    radial-gradient(90% 70% at 50% -10%, rgba(30, 46, 78, 0.35), transparent 60%),
    var(--bg0);
}
.colony-table-stage > .colony-table-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
}
.colony-table-stage > .colony-table-minimap {
  position: absolute; right: 10px; bottom: 10px; z-index: 3;
  width: 84px; height: 84px;
  background: rgba(6, 14, 22, 0.55);
  border: 1px solid var(--panel-border-hi, rgba(89, 213, 242, 0.4));
  border-radius: 50%;
  pointer-events: none;
}
.colony-table-stage > .colony-table-tune {
  position: absolute; left: 10px; bottom: 10px; z-index: 3;
  padding: 6px 9px;
  font: 11px/1.5 ui-monospace, monospace;
  white-space: pre;
  color: #cdeeffe6;
  background: rgba(6, 14, 22, 0.72);
  border: 1px solid var(--panel-border-hi, rgba(89, 213, 242, 0.4));
  border-radius: 4px;
  pointer-events: none;
}
.colony-table-stage::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  box-shadow:
    inset 0 0 0 1px var(--panel-border-hi),
    inset 0 0 70px rgba(0, 0, 0, 0.7),
    inset 0 0 30px rgba(79, 216, 255, 0.1);
}
.colony-table-stage::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: repeating-linear-gradient(0deg, rgba(79, 216, 255, 0.035) 0 1px, transparent 1px 3px);
  mix-blend-mode: screen; opacity: 0.5;
}

.village-scene {
  position: relative; aspect-ratio: 16 / 9;
  /* Bleeds past #page's own padding (24px, --sp*3) on the left/right so the
     painting reaches the true edges instead of sitting in a framed box —
     owner: "stretch it to fill out the background". Bottom stays put (real
     gap before the operations panel is still wanted).
     v0.60.29 (owner: "the head section can not be seen") — this used to
     ALSO bleed -24px off the top, correct back when this scene was the
     first element in `.colony-page` (the head floated as an absolute
     overlay INSIDE it, not a sibling above it). Now that the head is a
     normal in-flow `.page-head` sibling before this scene, that same
     -24px pulled the scene up underneath the head's own box instead of
     bleeding past #page padding that no longer sits above it — a real
     ~18-24px overlap, not just a visual illusion. margin-top: 0 lets the
     scene start exactly where the head ends. */
  width: calc(100% + var(--sp) * 6);
  margin: 0 calc(var(--sp) * -3) 0;
  /* Cap only on ultra-wide desktops (true full-bleed there would make it
     absurdly large). No border, no border-radius/overflow clip and no flat
     background box — this hero scene shouldn't read as "just another
     panel". Owner tried the edge-to-edge fade-to-transparent look (a
     .village-bg mask, since removed) and didn't like it — "fill it"
     instead: the painting now fills its full box, no vignette. */
  max-width: calc(2200px + var(--sp) * 6);
  /* v0.60.29 (owner: "push down the village plots and make it a bit
     bigger, it can go down to almost reach the chat section") — this used
     to be a `max-height: clamp(150px, calc(100vh - ...- Npx), 60vh)`, a
     FIXED-px-budget guess for "everything else sharing the viewport"
     (HUD, #page padding, the icon row below). That guess had to be
     re-tuned every time the icon row's own height changed (single-row →
     wrapped 2-3 rows earlier this same version), and even then a value
     tuned to avoid overflow on a short/narrow viewport left real unused
     space on a tall/wide one (288px scene height with 114px of slack
     above the chat-dock reserve, measured at 1920×1040) — the two
     viewports needed different constants, which a single guessed number
     structurally can't provide.
     Real fix: `.colony-page-scene` (see below, JS-applied only in SCENE
     view) is a flex column with a height bound to the actual available
     area (viewport minus HUD minus #page's own padding minus the
     chat-dock reserve); this scene is `flex: 1 1 auto` inside it, so it
     ALWAYS fills exactly whatever's left after the head and the icon-row
     panel take their own natural (possibly wrapped, possibly not) height
     — no guessing, no re-tuning when the icon row's row-count changes.
     min-height keeps the same 150px floor the old clamp() had. */
  flex: 1 1 auto;
  min-height: 150px;
  /* Owner request: building models should resize with the scene as the
     browser window is resized, not just reposition (left/top were already
     %-based). container-type establishes this as a query container so the
     tile/icon sizes below can use cqw (% of THIS element's own rendered
     width) instead of a fixed px. */
  container-type: inline-size;
}
.village-bg {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  /* v0.60.34 (owner plan: "Colony Scene: Making It Alive", option C — cheap
     ambient motion) — a deliberately SUBTLE slow breathing zoom, distinct
     from the day/night wash removed in the comment right below this one
     (that was a color/brightness overlay the owner never asked for and
     could crush a dark biome toward black; this is pure geometry, no
     color/contrast change at all, so it doesn't carry the same risk).
     1.015 max scale over 90s is intentionally almost imperceptible frame-
     to-frame — the goal is "the scene feels alive," not "the player
     notices zooming." transform-origin stays centered so it reads as a
     slow push-in/out, not a pan. */
  animation: village-bg-breathe 90s ease-in-out infinite;
  /* v0.60.18 briefly added the galaxy map's day/night wash here too (when
     this was, for one round, the same universal nebula image as the map) —
     reverted along with that swap (v0.60.19, back to the 9 real per-biome
     paintings) but the animation itself was accidentally left behind.
     v0.60.22 (owner: "eltűnt a 9 db biome háttér az épületek mögül") — the
     wash was tuned against ONE bright image; at its "night" extreme
     (rgba(6,10,26,0.4), a fairly strong dark overlay) it can crush an
     already-darker biome painting toward near-black. Never actually asked
     for here — removed outright rather than re-tuned. */
}
@keyframes village-bg-breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
  .village-bg { animation: none; }
}
/* Isometric ground plates (ISO_GROUND_BIOMES, colony.js) are a painted
   diamond platform inset within the rectangular canvas — the corners around
   it are flat dead space (measured ~20-21% of the canvas along each edge).
   Owner: mask it with "some mystical fog" rather than leaving it as an
   obvious asset gap. 4 corner radial-gradients read as a containment-field
   haze bleeding into deep space: a near-opaque core matching the canvas's
   own near-black background (so the seam is invisible), a thin --accent
   cyan haze band (echoing the ground art's own glowing crack-lines), fading
   to fully transparent well inside the platform's real edge — sized (30%
   /34%) with margin over the measured ~21% overhang so the jagged, organic
   rock silhouette never pokes through with a hard edge. Sits above
   .village-bg but below .village-plots so buildings/motes render on top,
   unaffected. */
/* Uniform darkening scrim over every biome painting — see colony.js's own
   comment on the append site. 0.38 chosen to bring the measured rocky
   biome's 163.6/255 mean down near ~100/255, still comfortably lighter
   than the near-black chrome so the art reads as a lit clearing, not a
   silhouette. */
.village-dim {
  position: absolute; inset: 0; pointer-events: none;
  background: rgba(4, 8, 16, 0.38);
}
.village-fog {
  position: absolute; inset: 0; pointer-events: none;
  background-image:
    radial-gradient(ellipse 30% 34% at 0% 0%, rgba(9, 14, 28, 0.92) 0%, rgba(9, 14, 28, 0.58) 40%, rgba(79, 216, 255, 0.10) 62%, rgba(79, 216, 255, 0) 78%),
    radial-gradient(ellipse 30% 34% at 100% 0%, rgba(9, 14, 28, 0.92) 0%, rgba(9, 14, 28, 0.58) 40%, rgba(79, 216, 255, 0.10) 62%, rgba(79, 216, 255, 0) 78%),
    radial-gradient(ellipse 30% 34% at 0% 100%, rgba(9, 14, 28, 0.92) 0%, rgba(9, 14, 28, 0.58) 40%, rgba(79, 216, 255, 0.10) 62%, rgba(79, 216, 255, 0) 78%),
    radial-gradient(ellipse 30% 34% at 100% 100%, rgba(9, 14, 28, 0.92) 0%, rgba(9, 14, 28, 0.58) 40%, rgba(79, 216, 255, 0.10) 62%, rgba(79, 216, 255, 0) 78%);
  animation: village-fog-drift 16s ease-in-out infinite;
}
@keyframes village-fog-drift {
  0%, 100% { opacity: 0.82; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .village-fog { animation: none; opacity: 0.9; }
}
.village-plots { position: absolute; inset: 0; }

/* v0.60.34 (owner plan: "Colony Scene: Making It Alive", option C) — a
   dozen small drifting dust/spark motes floating up through the scene,
   purely atmospheric. Sits ABOVE .village-plots (later in DOM = higher
   paint order at the same z-index-less stacking level) so motes drift in
   front of buildings too, like real airborne dust would — `pointer-events:
   none` throughout so it never blocks a tile click. Each mote's own
   position/timing comes from inline custom properties set in colony.js
   (ambientMotes()); the CSS only defines the shared drift animation. */
.village-ambient { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.village-mote {
  position: absolute;
  bottom: -4%;
  border-radius: 50%;
  background: rgba(200, 235, 255, 0.55);
  box-shadow: 0 0 3px 1px rgba(120, 210, 255, 0.4);
  opacity: 0;
  animation: village-mote-drift var(--mote-duration, 14s) ease-in var(--mote-delay, 0s) infinite;
}
@keyframes village-mote-drift {
  0% { transform: translate(0, 0); opacity: 0; }
  10% { opacity: 0.7; }
  90% { opacity: 0.5; }
  100% { transform: translate(var(--mote-drift, 8%), -112%); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .village-ambient { display: none; }
}

/* v0.60.28 (owner: "remove the admin layout and colony ground title and
   button, panel. Give a real page-head to colony name, just like other
   sections has") — colony.js no longer renders the floating overlay bar
   (`.village-scene-head`) or the "Colony Grounds" badge + Admin Layout
   button (`.village-hero-badge`/`.village-hero-label`); the colony name
   header is a normal in-flow `.page-head` again, same as every other page.
   Both rules removed as dead CSS. This also made the in-place village
   layout editor (villageEdit/villageEditControls/makeTileDraggable/
   makeSelectionHandles) unreachable — confirmed genuinely obsolete and
   deleted from colony.js (along with the .village-editing/.village-edit-bar/
   .village-tile-draggable/.village-tile-dragging/.village-tile-selected/
   .village-handle-* rules that styled it, removed here too); the real admin
   path is the standalone `public/dev/village-layout-editor.html` tool (see
   [[void-reach-galaxy-map-concept-gui]] memory). */

/* Quest marker (v0.30): the plot that satisfies the next unclaimed directive's
   building check — a soft pulsing ring, distinct from the steady selection
   outline above (this is "go here", not "currently selected"). */
.village-tile-objective .model-hero, .village-tile-objective .st-plus {
  outline: 2px solid var(--accent); outline-offset: 6px; border-radius: 50%;
  animation: objective-pulse 1.8s ease-in-out infinite;
}
@keyframes objective-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(79, 216, 255, 0.55); }
  50% { box-shadow: 0 0 14px 4px rgba(79, 216, 255, 0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .village-tile-objective .model-hero, .village-tile-objective .st-plus { animation: none; }
}

/* Frameless "attached to the ground" tile: no card box AND no model-hero box —
   the building model/icon sits directly on its hex, larger and readable. Only
   the level badge + name get a small dark backing for legibility over terrain.
   (Overrides the .structure-tile card styling that the grid variant still uses
   inside the panel overlay.) */
.village-tile.structure-tile {
  position: absolute; transform: translate(-50%, -50%);
  /* Scales with the scene's own rendered width (cqw), clamped so a very
     narrow window doesn't shrink it past legibility and an ultra-wide one
     doesn't grow it past today's tuned 104px look. 8.4cqw ≈ 104px at a
     ~1200px scene width (a typical non-ultra-wide desktop). Bumped up from
     the original 44-74px range — owner reported buildings were too small
     and hard to see on the colony view. */
  width: clamp(60px, 8.4cqw, 104px); padding: 0; gap: 2px;
  background: none; border: none; box-shadow: none; backdrop-filter: none;
}
.village-tile .model-hero {
  background: none; border: none; overflow: visible;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.6));
  transition: transform 0.12s;
  /* Overrides modelHero()'s inline px sizing (JS default 70px) with the same
     cqw-based scaling as the tile above, so the icon itself resizes in step
     with the scene on a window resize, not just its left/top position. */
  width: clamp(56px, 8cqw, 100px) !important;
  height: clamp(56px, 8cqw, 100px) !important;
}
/* Flat authored icon (public/img/icons/bld/) — fills the same reserved box
   the WebGL hero used, since iconImg() renders an <span class="ic"> that's
   otherwise sized for small inline text contexts (see style.css's own .ic
   rules near the top of the file). */
.village-tile .model-hero .ic { width: 100%; height: 100%; }
.village-tile .model-hero .ic-img { width: 100%; height: 100%; max-width: 100%; object-fit: contain; }
.village-tile .model-hero .ic-fb { font-size: 36px; line-height: 1; }
/* Owner-noticed clash: the crisp, PBR-lit 3D models read as "pasted onto"
   the soft, painterly Higgsfield backdrop rather than sitting IN it (no
   contact shadow, no color harmony with the atmospheric painting). A cheap
   render-side fix — scoped to the Village Scene ONLY, not the shared
   engine/lighting every other 3D surface (Shipyard cards, building detail,
   commander portraits) also uses — before considering an actual model
   re-generation pass. */
.village-tile .model-hero .mv-canvas {
  /* Slightly desaturated + lower-contrast + a touch warmer: nudges the
     crisp real-time render toward the backdrop's own muted, atmospheric
     palette without touching the shared PBR environment/lighting itself
     (that's global — this filter is local to canvases inside village
     tiles only). */
  filter: saturate(0.82) contrast(0.9) brightness(1.05) sepia(0.06);
}
/* A soft ground-contact shadow, ONLY for tiles that actually have a model
   rendered (empty "+" plots stay bare) — ::before so it paints BEHIND the
   model (DOM paint order) without needing z-index. Grounds the building
   in the scene instead of it reading as a floating sticker. */
.village-tile.structure-tile:not(.st-empty)::before {
  content: '';
  position: absolute; left: 50%; bottom: 2%;
  width: 60%; height: 20%;
  transform: translateX(-50%);
  background: radial-gradient(ellipse, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.22) 55%, transparent 78%);
  filter: blur(2px);
  pointer-events: none;
}
/* Isometric prototype icons (public/img/icons/bld-iso/, rocky biome only —
   see colony.js ISO_PROTOTYPE_BIOME/ISO_PROTOTYPE_KEYS) already bake their
   own ground-contact shadow + color grade at render time, tuned to the
   isometric ground plate's own perspective — suppress the generic CSS
   shadow ellipse above so these tiles don't get two mismatched shadows
   stacked on top of each other. Also drop the generic drop-shadow filter
   (redundant with the baked shadow) and scale up slightly to compensate
   for the render's own internal margin (the orthographic frustum leaves
   headroom around the model so nothing clips at odd camera angles). */
.village-tile.structure-tile:not(.st-empty):has(.mv-iso-icon)::before {
  content: none;
}
.village-tile .model-hero.mv-iso-icon {
  filter: none;
  transform: scale(1.18);
}
.village-tile.structure-tile:hover .model-hero.mv-iso-icon,
.village-tile.structure-tile:focus-visible .model-hero.mv-iso-icon {
  transform: scale(1.18) scale(1.05);
}
/* v0.60.34 (owner plan: "Colony Scene: Making It Alive", option C) — a
   small pulsing "beacon" light on every BUILT tile (excludes empty/locked
   plots — nothing to light up yet — and active `st-building` tiles, which
   already have their own scan-beam/pulse construction effect and don't
   need a second competing animation). Staggered via nth-child into 3
   delay groups so a cluster of buildings doesn't blink in visible unison —
   cheap, no new assets, reuses the same accent color already used for the
   scan-beam so it reads as part of one consistent "this is alive" language
   rather than a new unrelated effect. */
.village-tile.structure-tile:not(.st-empty):not(.st-locked):not(.st-building)::after {
  content: '';
  position: absolute; top: 14%; left: 50%;
  width: 5px; height: 5px; border-radius: 50%;
  background: rgba(180, 235, 255, 0.9);
  box-shadow: 0 0 4px 1px rgba(79, 216, 255, 0.7);
  animation: village-tile-beacon 3.4s ease-in-out infinite;
  pointer-events: none;
}
.village-tile.structure-tile:nth-child(3n+1)::after { animation-delay: 0s; }
.village-tile.structure-tile:nth-child(3n+2)::after { animation-delay: 1.1s; }
.village-tile.structure-tile:nth-child(3n)::after { animation-delay: 2.2s; }
@keyframes village-tile-beacon {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .village-tile.structure-tile::after { animation: none; opacity: 0.6; }
}
.village-tile.structure-tile:hover, .village-tile.structure-tile:focus-visible {
  transform: translate(-50%, -50%); background: none; border: none; box-shadow: none;
  outline: none;
}
.village-tile.structure-tile:hover .model-hero,
.village-tile.structure-tile:focus-visible .model-hero {
  transform: scale(1.1);
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, 0.7)) drop-shadow(0 0 6px rgba(79, 216, 255, 0.35));
}
.village-tile.structure-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius); }
/* Full building name — never truncated. It's allowed to be wider than the
   tile (the pill centers and overflows the narrow tile column) and wraps to a
   second line rather than ellipsing, so "Isotope Refinery" etc. read in full.
   A3 (owner UI audit): bumped 10px→12px — too small against the busy painted
   backdrop — and made BRIGHT by default (var(--txt), not the dim utility) so
   built structures' names read as "what I HAVE" at a glance; the st-empty
   override right below dims it back down for buildable/locked plots so
   "what I could build" recedes instead of competing for attention. */
.village-tile .st-name {
  padding: 1px 6px; border-radius: 6px;
  background: rgba(5, 7, 15, 0.72);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
  white-space: normal; overflow: visible; text-overflow: clip;
  max-width: 104px; width: max-content; line-height: 1.2;
  font-size: 14px; font-weight: 600; color: var(--txt);
}
/* Empty/locked plots stay frameless too — a glowing "+" (and a lock when
   gated) marks a buildable spot directly on the painted clearing, no card. */
.village-tile.structure-tile.st-empty { background: none; border: none; }
.village-tile.structure-tile.st-empty .st-name {
  color: var(--txt-dim); font-weight: 500;
  background: rgba(5, 7, 15, 0.45);
}
.village-tile .st-plus {
  color: var(--accent); text-shadow: 0 0 10px rgba(79, 216, 255, 0.8), 0 2px 3px rgba(0, 0, 0, 0.9);
}
.village-tile .st-level { bottom: auto; top: clamp(38px, 5.5cqw, 68px); right: 4px; font-size: 14px; }
.village-tile .st-ring-wrap {
  top: 0; left: 50%; transform: translateX(-50%);
  width: clamp(56px, 8cqw, 100px); height: clamp(56px, 8cqw, 100px);
}
.village-tile .st-plus { width: clamp(56px, 8cqw, 100px); height: clamp(56px, 8cqw, 100px); font-size: 46px; }

@media (max-width: 640px) {
  .village-tile.structure-tile { width: 88px; transform: translate(-50%, -50%) scale(0.86); }
  .village-tile.structure-tile:hover, .village-tile.structure-tile:focus-visible {
    transform: translate(-50%, -50%) scale(0.86);
  }
}

.structure-tile {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 10px 6px 8px;
  background: var(--panel); border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  cursor: pointer; text-align: center;
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s;
}
.structure-tile:hover, .structure-tile:focus-visible {
  border-color: var(--panel-border-hi);
  box-shadow: 0 0 14px rgba(79, 216, 255, 0.12);
  transform: translateY(-1px);
  outline: none;
}
.structure-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.structure-tile.st-empty { border-style: dashed; background: rgba(13, 21, 38, 0.4); }
.structure-tile.st-locked { opacity: 0.55; }
.structure-tile.st-building { border-color: var(--panel-border-hi); }
.structure-tile.st-maxed { border-color: rgba(255, 209, 102, 0.4); }

/* v0.60.9 (owner: "a spatial loading effect when the building is being
   built or upgraded") — the progress ring already reports the % complete;
   this is pure ambient flavor on top, so it's built as CSS-only (the
   `.st-building` class is already applied — see buildingTile()) rather
   than new markup. A holographic scan-beam sweeps the icon top-to-bottom
   (the structure "materializing" under construction), and the tile itself
   breathes with a soft cyan pulse so an in-progress build reads at a
   glance even before you notice the ring/countdown. Reduced-motion turns
   both off — same convention every other animation in this file follows. */
.structure-tile.st-building {
  animation: st-building-pulse 2.6s ease-in-out infinite;
}
@keyframes st-building-pulse {
  0%, 100% { box-shadow: none; }
  50% { box-shadow: 0 0 16px rgba(79, 216, 255, 0.35); }
}
.structure-tile.st-building .model-hero {
  position: relative;
  overflow: hidden;
}
/* v0.60.11 (owner: "last frame should be the same as the first, so it acts
   as a seamless loop") — a plain sweep from -110% to 210% snaps back to its
   start position every cycle, since those two background-position values
   don't render the same pixels. Fixed the standard way: a REPEATING gradient
   (the beam pattern tiles every 200% of the element's own height) animated
   by exactly ONE tile's worth of background-position, linear timing. At
   100% the visible pixels are identical to 0% (just one full repeat further
   into an infinite tiled pattern), so the restart is invisible — the beam
   reads as one continuous, endless sweep instead of a repeating pulse. */
.structure-tile.st-building .model-hero::after {
  content: '';
  position: absolute; inset: 0;
  background: repeating-linear-gradient(180deg,
    transparent 0%,
    transparent 40%,
    rgba(79, 216, 255, 0.5) 48%,
    rgba(180, 235, 255, 0.9) 50%,
    rgba(79, 216, 255, 0.5) 52%,
    transparent 60%,
    transparent 100%);
  background-size: 100% 200%;
  animation: st-scan-sweep 2.2s linear infinite;
  mix-blend-mode: screen;
  pointer-events: none;
}
@keyframes st-scan-sweep {
  0% { background-position: 0 0%; }
  100% { background-position: 0 200%; }
}
@media (prefers-reduced-motion: reduce) {
  .structure-tile.st-building { animation: none; }
  .structure-tile.st-building .model-hero::after { animation: none; opacity: 0; }
}

.st-plus {
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  font-size: 24px; color: var(--txt-faint);
}
.st-lock {
  position: absolute; top: 4px; right: 6px; font-size: 13px;
}
.st-level {
  font-size: 13px; color: var(--accent);
  position: absolute; bottom: 22px; right: 6px;
  background: rgba(5, 7, 15, 0.75); border-radius: 8px; padding: 0 4px;
}
.st-maxed .st-level { color: var(--warn); }
.st-name {
  font-size: 12px; line-height: 1.25; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.st-queued { font-size: 15px; color: var(--txt-dim); }
.st-eta {
  font-size: 12px; font-weight: 700; color: var(--accent2);
  background: rgba(5, 7, 15, 0.75); border-radius: 8px; padding: 0 5px;
}

.st-ring-wrap {
  position: absolute; top: 2px; left: 2px; width: 44px; height: 44px; pointer-events: none;
}
/* A dark radial backing plate behind the ring — without it, contrast depends
   entirely on whatever's painted in the village scene right behind the tile
   (sometimes bright terrain/structure art), which is why the ring could
   nearly vanish. This guarantees a dark base under the stroke every time. */
.st-ring-wrap::before {
  content: '';
  position: absolute; inset: -3px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(5, 7, 15, 0.6) 58%, rgba(5, 7, 15, 0) 80%);
}
.st-ring { width: 100%; height: 100%; position: relative; }
.st-ring-bg { fill: none; stroke: rgba(79, 216, 255, 0.35); stroke-width: 4; }
.st-ring-fg {
  fill: none; stroke: var(--accent); stroke-width: 4; stroke-linecap: round;
  transition: stroke-dasharray 0.3s linear;
  filter: drop-shadow(0 0 4px rgba(79, 216, 255, 0.9)) drop-shadow(0 0 1px rgba(255, 255, 255, 0.7));
}

/* ---- B8 (UI audit): colony SCENE ⇄ LIST toggle + the LIST-mode table ----
   The toggle itself is just `.map-viewseg` (defined above, galaxy map's own
   ◎3D/▦Flat control) dropped into the colony page's `.page-head-right` — no
   new pill/segment chrome invented, same control language everywhere. */
.colony-viewseg { flex: none; }

/* LIST mode's table reuses the plain `.table`/`.table-scroll` skin the
   Empire Overview table (v0.58.2) and System Scan table (galaxy.js) already
   established — same "flat data table" family, not a bespoke component. */
.bld-list-panel .table { min-width: 720px; }
/* Owner: "make everything 1 page size and remove the possibility to scroll
   down... super compact" — zero-scroll pass. Now that the ~23 plots are
   split across 3 tabs (built/buildable/locked) instead of one long table,
   each tab's own row count is much smaller, but still needs compacting to
   reliably clear #page's own bottom chrome reserve (--chat-dock-h) on a
   real (not ultra-tall) viewport — same treatment leaderboard.js's `.lb-page`
   got. Scoped to `.bld-list-panel`/`.bld-list-tabs` only. */
.bld-list-tabs { margin-bottom: 4px; }
.bld-list-tabs .tab { padding: 6px 12px; font-size: 14px; }
.bld-list-panel .bld-list-table { font-size: 14px; }
.bld-list-panel .bld-list-table th { padding: 4px 8px; }
.bld-list-panel .bld-list-table td { padding: 3px 8px; }
/* A locked/buildable row's "Next Cost" cell can hold 3-4 resource chips —
   `.cost-chips`'s shared `flex-wrap:wrap` let that column push the WHOLE
   row to ~2 lines tall (~62px) at a typical viewport width, the single
   biggest remaining contributor to this panel's overflow. Force single-line
   here specifically (`.table-scroll`'s existing `overflow-x:auto` handles
   any resulting horizontal overflow — a HORIZONTAL scrollbar on a data
   table is an established, unproblematic pattern already used elsewhere in
   this game; it's PAGE-level vertical scroll the owner wants gone). */
.bld-list-table .cost-chips { flex-wrap: nowrap; }
.bld-list-table .req-list { flex-wrap: nowrap; white-space: nowrap; }
.bld-list-body { padding-top: 4px; }
.bld-list-body .pager { padding-top: 2px; gap: var(--sp); }
.bld-list-body .pager .mono { font-size: 13px; }
/* Owner (zero-scroll pass): scoped to `.colony-page` (colony.js's own
   wrapper, same pattern as leaderboard.js's `.lb-page`) rather than the
   shared `.page-head`/`.panel` rules, so this doesn't shrink spacing on
   every other page. */
.colony-page .page-head { margin-bottom: 6px; }
.colony-page .bld-list-panel,
.colony-page .colony-ops-panel { margin-bottom: 6px; padding: 8px; }
.colony-page .bld-list-table th { line-height: 1.2; }
/* LIST mode: building list + ops panel side-by-side instead of stacked —
   see colony.js's own comment on `.colony-list-row` for why (a wide-but-
   height-starved viewport once #page's bottom chrome reserve grew). */
.colony-list-row { display: flex; align-items: flex-start; gap: var(--sp); }
.colony-list-row .bld-list-panel { flex: 1 1 58%; min-width: 0; margin-bottom: 0; }
.colony-list-row .colony-ops-panel {
  /* Overrides colony-ops-panel's own edge-to-edge bleed (width/negative
     margins meant to match the Village Scene's full-bleed width right
     above it in SCENE mode) — irrelevant and actively harmful inside this
     flex row, which has its own real siblings on both sides. */
  flex: 1 1 42%; min-width: 0; margin: 0; width: auto;
}
.colony-list-row .bld-list-panel .table { min-width: 0; }
.bld-row-name {
  display: inline-flex; align-items: center; gap: 8px;
  background: none; border: 0; padding: 0; margin: 0;
  color: var(--txt); font: inherit; text-align: left; cursor: pointer;
}
.bld-row-name:hover, .bld-row-name:focus-visible { color: var(--accent); }
.bld-row-name .ic-img, .bld-row-name .ic-fb { height: 1.5em; max-width: 2em; }
.bld-row-status.building { color: var(--accent2); }
.bld-row-status.queued { color: var(--warn); }
tr.bld-row-locked { opacity: 0.6; }
tr.bld-row-active { background: rgba(79, 216, 255, 0.05); }
.bld-row-action { white-space: nowrap; }

/* ---- building detail body (rendered inside a panel overlay) ---- */
.b-detail-head { display: flex; align-items: center; gap: var(--sp); margin-bottom: 6px; }
.b-active-row {
  display: flex; align-items: center; gap: var(--sp);
  padding: 8px 10px; margin-bottom: var(--sp);
  background: rgba(79, 216, 255, 0.06); border: 1px solid var(--panel-border-hi);
  border-radius: var(--radius); font-size: 15px;
}

/* ============================================================================
   Captain (void-reach-ui-shell-and-art-brief.md §5, Path A). Top-bar
   mini-widget + the screen overlay it opens. Portrait/frame art from
   public/img/portraits/ + public/img/panels/frame-tier-*.png (brief Part 2);
   any 404 (art missing) degrades gracefully — the <img> just hides itself.
   ========================================================================== */

#hud-captain.hidden { display: none; }
.cap-widget-btn {
  display: flex; align-items: center; gap: 6px;
  background: transparent; border: 1px solid var(--panel-border);
  border-radius: 20px; padding: 2px 10px 2px 2px; cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.cap-widget-btn:hover, .cap-widget-btn:focus-visible {
  border-color: var(--panel-border-hi);
  box-shadow: 0 0 10px rgba(79, 216, 255, 0.15);
  outline: none;
}
.cap-widget-portrait {
  width: 30px; height: 30px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--panel-border-hi);
}
.cap-widget-level { font-size: 14px; color: var(--accent); }
.cap-widget-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--warn);
  box-shadow: 0 0 5px var(--warn);
}

.cap-header { display: flex; align-items: center; gap: calc(var(--sp) * 2); margin-bottom: calc(var(--sp) * 2); }
.cap-portrait-wrap { position: relative; width: 96px; height: 96px; flex: none; border-radius: 50%; }
.cap-portrait { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.cap-portrait-frame {
  position: absolute; inset: -10px; width: calc(100% + 20px); height: calc(100% + 20px);
  pointer-events: none;
}
/* Flagship polish (docs/PHASE6-UI-CHROME.md §4 Phase 3): the tier-frame art
   sat on a plain, glow-less circle — echoes the same tier colors already
   used on `.cap-level` text as a soft ambient ring behind the portrait,
   so a higher tier reads as more "premium" at a glance, not just via the
   frame art alone. `none` tier gets no glow (nothing earned yet). */
.cap-portrait-wrap-bronze { box-shadow: 0 0 18px 2px rgba(201, 138, 75, 0.45); }
.cap-portrait-wrap-silver { box-shadow: 0 0 18px 2px rgba(199, 208, 218, 0.4); }
.cap-portrait-wrap-gold { box-shadow: 0 0 22px 3px rgba(255, 176, 46, 0.5); }
.cap-portrait-wrap-platinum { box-shadow: 0 0 26px 4px rgba(79, 216, 255, 0.55); }
.cap-header-main { flex: 1; min-width: 0; }
.cap-name { font-size: 20px; font-weight: 700; }
.cap-level { font-size: 15px; margin: 2px 0 6px; }
.cap-tier-bronze { color: #c98a4b; }
.cap-tier-silver { color: #c7d0da; }
.cap-tier-gold { color: var(--warn); }
.cap-tier-platinum { color: var(--accent); }
.cap-xp-bar { height: 8px; max-width: 360px; }
.cap-xp-label { margin-top: 3px; font-size: 13px; }

.cap-section-title { font-size: 15px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 4px; }
.cap-copilot-title { margin-top: calc(var(--sp) * 2); }
.cap-skills-note { font-size: 13.5px; margin-bottom: var(--sp); }
.cap-skills { margin-bottom: calc(var(--sp) * 2); }
.cap-skill-row {
  display: flex; align-items: center; gap: var(--sp);
  padding: 6px 4px; border-top: 1px solid var(--panel-border);
}
.cap-skill-row:first-of-type { border-top: none; }
.cap-skill-ic { flex: none; width: 24px; text-align: center; color: var(--accent2); }
.cap-skill-ic .ic-img { height: 1.6em; max-width: 2.1em; }
.cap-skill-name {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* .cap-skill-val's text is the point value PLUS an appended live-bonus
   string (captain.js bonusPctLabel(), e.g. "0 (+5.7% plunder capacity)") —
   a fixed 24px width (sized for the bare number only) let that longer text
   overflow leftward as a flex item, visually covering the icon/name beside
   it. Give it its own natural width instead, so .cap-skill-name (flex:1,
   min-width:0, now ellipsis-truncating) is what yields space under
   pressure, not the value colliding with its neighbors. */
.cap-skill-val { flex: 0 0 auto; max-width: 60%; white-space: nowrap; text-align: right; color: var(--accent); }

/* Path C "Relics" (PHASE4-CAPTAIN.md §11), reworked in v0.64.0 into a
   Web3-fork-ported Sets economy (docs/RELIC-ECONOMY.md): the 5 free
   equip-slot bar (unchanged, still the real anti-whale cap) + one card per
   SET (replaces the old flat branch-keyed inventory grid). */
.cap-relics { margin-bottom: calc(var(--sp) * 2); }
.cap-relic-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; margin-bottom: var(--sp); }
.cap-relic-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; }
.cap-relic-slot, .cap-relic-card {
  display: flex; flex-direction: column; gap: 2px; padding: 8px;
  border: 1px solid var(--panel-border); border-radius: var(--radius); font-size: 14px;
  background: var(--bg2);
}
.cap-relic-slot-empty { align-items: center; justify-content: center; text-align: center; border-style: dashed; }
.cap-relic-ic { color: var(--accent2); font-size: 24px; }
.cap-relic-ic .ic-img { height: 2.1em; max-width: 2.6em; }
.cap-relic-name { font-weight: 600; }
.cap-relic-rarity { font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.03em; }
.cap-relic-bonus { font-size: 13px; }
.cap-relic-slot .btn, .cap-relic-card .btn { margin-top: auto; align-self: center; padding-top: 4px; }
.cap-relic-rarity-common .cap-relic-rarity { color: var(--txt-dim); }
.cap-relic-rarity-uncommon .cap-relic-rarity { color: var(--ok); }
.cap-relic-rarity-rare .cap-relic-rarity { color: var(--accent2); }
.cap-relic-rarity-epic .cap-relic-rarity { color: var(--warn); }
/* legendary/immortal (v0.64.0 — the 35-item catalog's top 2 tiers, a
   pre-existing gap in this map since Path C topped out at epic): legendary
   a richer gold than epic's amber; immortal a premium violet-white, echoing
   the nebula map-flag's #cfa9ff (map.js) as this game's "cosmic rarity" hue. */
.cap-relic-rarity-legendary .cap-relic-rarity { color: #ffd24d; }
.cap-relic-rarity-immortal .cap-relic-rarity { color: #e6d9ff; }
.cap-relic-rarity-common { border-color: rgba(143, 162, 189, 0.35); }
.cap-relic-rarity-uncommon { border-color: rgba(87, 224, 138, 0.4); }
.cap-relic-rarity-rare { border-color: rgba(79, 216, 255, 0.4); }
.cap-relic-rarity-epic { border-color: rgba(255, 176, 46, 0.4); }
.cap-relic-rarity-legendary { border-color: rgba(255, 210, 77, 0.5); }
.cap-relic-rarity-immortal { border-color: rgba(230, 217, 255, 0.55); }

/* Set cards (v0.64.0 Sets rewrite) */
.cap-relic-sets-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--sp); }
.cap-relic-set-card {
  display: flex; flex-direction: column; gap: 6px; padding: 10px;
  border: 1px solid var(--panel-border); border-radius: var(--radius); background: var(--bg2);
}
.cap-relic-set-tier-grail { border-color: rgba(79, 216, 255, 0.35); }
.cap-relic-set-tier-immortal { border-color: rgba(230, 217, 255, 0.45); }
.cap-relic-set-complete { box-shadow: inset 0 0 0 1px var(--ok); }
.cap-relic-set-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.cap-relic-set-ic { font-size: 16px; color: var(--accent2); }
.cap-relic-set-name { font-weight: 600; }
.cap-relic-set-progress { font-size: 12.5px; margin-left: auto; }
.cap-relic-set-badge {
  font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--ok);
  border: 1px solid var(--ok); border-radius: 3px; padding: 1px 5px;
}
.cap-relic-set-stat { font-size: 13px; }
.cap-relic-set-pieces { display: flex; flex-wrap: wrap; gap: 6px; }
.cap-relic-piece {
  display: flex; flex-direction: column; gap: 2px; padding: 6px 8px; min-width: 120px;
  border: 1px solid var(--panel-border); border-radius: var(--radius); font-size: 12.5px;
  background: var(--bg1);
}
.cap-relic-piece-name { font-weight: 600; }
.cap-relic-piece-rarity { text-transform: uppercase; letter-spacing: 0.03em; font-size: 11px; }
.cap-relic-piece-unowned { opacity: 0.55; }
.cap-relic-piece-locked { font-size: 11.5px; }
.cap-relic-piece .btn { align-self: flex-start; margin-top: 2px; }
/* v0.64.x: alliance-scope relic transfer (the ONE exception to "relics
   never trade" — an alliance member can hand an Immortal Concord piece to a
   fellow member to consolidate a full set onto one bearer). */
.cap-relic-transfer-row { display: flex; gap: 4px; margin-top: 4px; }
.cap-relic-transfer-input { flex: 1; min-width: 0; font-size: 12px; padding: 3px 6px; }

.cap-orders-table th.c, .cap-orders-table td.c { text-align: center; }
.cap-copilot-row { display: flex; gap: var(--sp); align-items: center; margin: 6px 0; }
.cap-copilot-row input[type="text"] { flex: 1; max-width: 240px; }
.cap-copilot-of { font-size: 14px; }

@media (max-width: 640px) {
  .cap-header { flex-direction: column; align-items: flex-start; gap: var(--sp); }
  .cap-xp-bar { max-width: 100%; }
}

/* ============================================================================
   Phase 7A — Combat & Doctrine (docs/PHASE7-DOCTRINE.md §8): shipyard wing
   badges/hints, the send-form + colony stance selectors, and the battle
   report's Doctrine panel. Reuses the tag/dim/mono vocabulary — these rules
   only cover spacing and the few purpose-built classes.
   ========================================================================== */
.u-wing-badge { margin-left: 6px; font-size: 12px; vertical-align: middle; }
.u-wing-hint { font-size: 12.5px; margin-top: 2px; }
.so-stance-hint { font-size: 13.5px; }
.stance-select { min-width: 150px; }
.doctrine-panel { margin-top: var(--sp); border-top: 1px solid var(--panel-border); padding-top: var(--sp); }
.doctrine-side {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 4px 0; font-size: 14.5px;
}
.doctrine-wings { font-size: 13.5px; }

/* ============================================================================
   Phase 7B — Dynamic Economy (docs/PHASE7-ECONOMY.md §7): the Exchange Index
   strip on the market page and the colony-charter picker grid.
   ========================================================================== */
.idx-row { display: flex; gap: 12px; flex-wrap: wrap; padding: 4px 8px 8px; }
.idx-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px; font-size: 14px;
  border: 1px solid var(--panel-border); border-radius: var(--radius);
  background: var(--bg2);
}
.idx-spark { width: 64px; height: 16px; color: var(--accent2); opacity: 0.8; }
.idx-spark-none { width: 64px; text-align: center; }
.idx-note { font-size: 13px; padding: 0 8px 8px; }

.charter-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: var(--sp); padding: 0 8px 8px;
}
.charter-card { display: flex; flex-direction: column; gap: 6px; padding: 10px; }
.charter-card.charter-active { border-color: var(--accent); box-shadow: 0 0 10px rgba(79, 216, 255, 0.15); }
.charter-name { font-weight: 700; font-size: 15px; }
.charter-desc { font-size: 13.5px; line-height: 1.35; flex: 1; }
.charter-effects { display: flex; gap: 4px; flex-wrap: wrap; }
.charter-card .btn { align-self: flex-start; }

/* Phase 7C — warp lanes & intel (docs/PHASE7C-LANES.md §6): the system-page
   gate card. Map overlays draw on canvas; only these DOM bits need rules. */
.gate-card .gate-actions { display: flex; gap: 8px; }
/* v0.60.5: the gate-card hero strip (docs/art/PHASE7C-ART-PROMPTS.md
   gate-jump_gate-keyart) — same background-image + scrim technique as
   body.login-screen above, letterboxed so the section header/actions below
   always stay legible regardless of what's happening in the art's lower
   third. */
.gate-hero {
  aspect-ratio: 16 / 5;
  background:
    linear-gradient(to bottom, rgba(5, 7, 15, 0) 45%, rgba(5, 7, 15, 0.92) 100%),
    url("/img/npc/jump_gate_keyart.webp") center 35% / cover no-repeat;
}

/* ===========================================================================
   Admin Panel — HUD/chrome layout editor (public/js/ui/hud-editor.js)
   Owner: a live drag/resize/layer editor for the global HUD (repositioning,
   scaling, z-index reordering, replacement frame-image upload). Overlay
   boxes track their real target elements via getBoundingClientRect() on a
   rAF loop (positionOutlines()), so this needs no layout math of its own —
   just visual chrome for the drag/resize/reorder handles.
   =========================================================================== */
.hud-edit-root { position: fixed; inset: 0; z-index: 9000; pointer-events: none; }
.hud-edit-toolbar {
  position: fixed; top: calc(var(--hud-h) + 8px); left: 50%; transform: translateX(-50%);
  z-index: 9002; pointer-events: auto;
  display: flex; align-items: center; gap: 12px;
  background: var(--bg1); border: 1px solid var(--accent); border-radius: 8px;
  padding: 8px 14px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
  font-size: 13px; white-space: nowrap;
}
.hud-edit-title { color: var(--accent); font-weight: 700; letter-spacing: 0.03em; }
.hud-edit-hint { color: var(--txt-dim); font-size: 12px; }
.hud-edit-spacer { flex: 1; min-width: 12px; }
.hud-edit-outline {
  position: fixed; z-index: 9001; pointer-events: auto;
  border: 2px dashed rgba(79, 216, 255, 0.55);
  background: rgba(79, 216, 255, 0.04);
  cursor: grab; box-sizing: border-box;
}
.hud-edit-outline:active { cursor: grabbing; }
.hud-edit-outline.selected { border-color: var(--warn); background: rgba(255, 176, 46, 0.06); }
.hud-edit-label {
  position: absolute; top: -22px; left: -2px;
  background: var(--accent); color: #04121a; font-weight: 700; font-size: 11px;
  padding: 2px 7px; border-radius: 4px; white-space: nowrap; pointer-events: none;
}
.hud-edit-outline.selected .hud-edit-label { background: var(--warn); }
.hud-edit-zbar {
  position: absolute; top: -22px; right: -2px; display: flex; gap: 2px;
}
.hud-edit-zbar button {
  width: 20px; height: 20px; padding: 0; font-size: 11px; line-height: 1;
  background: var(--bg1); color: var(--txt); border: 1px solid var(--border);
  border-radius: 3px; cursor: pointer;
}
.hud-edit-zbar button:hover { border-color: var(--accent); }
/* ===== Game-wide custom cursor (chamfered holo arrowhead) =====
   Assets: public/img/ui/cursor-default.png / cursor-pointer.png (32x32,
   SVG source alongside for edits). Hotspot 4 3 = the arrow tip's pixel
   coord in the art — keep in sync if the SVGs are ever redrawn.
   Safari has no SVG-cursor support, so the PNG (not the SVG) is the real
   asset; keyword fallback (auto/pointer) is mandatory in case the image
   404s or an old browser refuses the format.
   Deliberately NOT touched: text inputs/textarea (native `text` caret),
   .map-canvas.map-cursor-none (`cursor: none !important` for the map's
   own magnetic reticle — that !important wins over this block, which is
   correct), and .map-canvas.grabbing (native grab cursor, out of scope). */
html, body {
  cursor: url('/img/ui/cursor-default.png') 4 3, auto;
}
a, button, .btn, select, summary, [role="button"],
input[type="checkbox"], input[type="radio"], label[for],
.tab, .nav-link, .report-item, .hud-select-btn {
  cursor: url('/img/ui/cursor-pointer.png') 4 3, pointer;
}

.hud-edit-handle {
  position: absolute; right: -6px; bottom: -6px; width: 12px; height: 12px;
  background: var(--accent); border: 2px solid var(--bg0); border-radius: 50%;
  cursor: nwse-resize;
}
