/* Primary UI typeface — Rajdhani, a geometric/technical sans that reads as
   more "futuristic HUD" than Exo 2 while staying highly legible at small
   sizes. Share Tech Mono stays a SEPARATE, deliberate choice for fixed-width
   numeric readouts (combat damage numbers, countdown timers, admin
   migration filenames, etc.) — it is NOT part of this swap. */
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg:          #090c07;
  --bg-sidebar:  #0c0f08;
  --bg-card:     #0e1209;
  --bg-card2:    #121709;
  --border:      #1c2512;
  --border-hi:   #2e3e18;
  --accent:      #8da020;
  --accent-hi:   #aac424;
  --gold:        #c9a42c;
  --gold-hi:     #e8c030;
  --purple:      #6530b8;
  --purple-hi:   #9060e0;
  --text:        #c8d2be;
  --text-muted:  #8e9e7e;
  --text-bright: #edf3e6;
  --green-ok:    #40c020;
  --sidebar-w:   218px;
  --header-h:    52px;
  --bg-input:    #0b0e08;
  --red:         #c03020;
  --red-bg:      #c0302015;
}

html, body {
  min-height: 100%;
  font-family: 'Rajdhani', sans-serif;
  font-size: 15px;
  font-weight: 400;
  background: var(--bg);
  color: var(--text);
  /* Light text on this UI's near-black background renders heavier/blurrier
     under default subpixel anti-aliasing — pronounced with Rajdhani's tight,
     tall letterforms at small label sizes, where it reads as smudged-together
     characters. Force grayscale AA so strokes stay crisp instead of fuzzy. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Form controls don't inherit font-family from body by default in most
   browsers — set it explicitly so buttons/inputs/selects/textareas actually
   render in Rajdhani instead of the OS UI font. */
body, button, input, select, textarea {
  font-family: 'Rajdhani', sans-serif;
}

/* Base link styling — themed accent instead of the browser default blue/purple (and no
 * purple "visited" state). Wrapped in :where() so the ENTIRE rule contributes ZERO
 * specificity (0,0,0): it still colors every bare <a>, but ANY scoped style overrides it
 * automatically — base AND :hover. This is the genuinely "low specificity" default the
 * comments below assume. Previously the literal `a:link, a:visited` selectors were (0,1,1)
 * and silently beat single-class button rules (0,1,0) — e.g. a `.travel-btn-go` link got
 * accent-green text on its accent-green background and the label vanished. (Author origin
 * still beats the UA :visited purple regardless of specificity, so the visited state is
 * covered too.) Any button-styled <a> with a class that sets `color` now wins on its own. */
:where(a, a:link, a:visited) { color: var(--accent); text-decoration: none; }
:where(a:hover) { color: var(--accent-hi); }

/* Inline player-name links used across pages */
a.profile-link {
  color: inherit; text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color .13s, border-color .13s;
}
a.profile-link:hover { color: var(--accent-hi); border-bottom-color: var(--accent-hi); }

/* ─── Keyboard focus ───────────────────────────────────────────────────────
 * One global focus ring for keyboard users. The file had ZERO :focus-visible
 * rules while stripping outlines in two places, so tabbing through the app gave
 * either no visible focus at all or a ring at 12.5% alpha that is invisible on
 * the near-black --bg. That matters more now that the combat controls are real
 * <button>s and actually reachable by Tab.
 *
 * :focus-visible (not :focus) means mouse clicks never show the ring — only
 * keyboard/AT navigation does, so this costs nothing visually for mouse players.
 * --accent-hi is redefined per theme further down, so the ring follows the
 * player's chosen theme automatically. */
:focus-visible {
  outline: 2px solid var(--accent-hi);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ─── App shell ─── */
/* 100dvh tracks the *visible* viewport so the layout doesn't sit behind the
   mobile address bar (100vh is the larger, chrome-inclusive height). */
.app { display: flex; height: 100vh; height: 100dvh; overflow: hidden; }

/* ─────────────────────────────────────
   SIDEBAR
───────────────────────────────────── */
.sidebar {
  width: var(--sidebar-w); flex-shrink: 0;
  background: var(--bg-sidebar); border-right: 1px solid var(--border);
  display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden;
}
.sidebar::-webkit-scrollbar { width: 3px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 2px; }

.sidebar-brand { display: block; padding: 1.4rem 1.2rem 1.1rem; border-bottom: 1px solid var(--border); user-select: none; text-decoration: none; }

/* ── Desktop "hide menu" toggle ──────────────────────────────────────────────
   The logo row carries a collapse button on its right; clicking it hides the
   whole sidebar (main.js toggles .sidebar-hidden on .app, persisted). A small
   floating button brings it back. Desktop only — on tablet/mobile the sidebar
   is already an off-canvas overlay driven by the header hamburger. */
.sidebar-top { display: flex; align-items: stretch; justify-content: space-between; border-bottom: 1px solid var(--border); }
.sidebar-top .sidebar-brand { flex: 1 1 auto; border-bottom: none; }
.sidebar-hide-btn {
  flex: 0 0 auto; align-self: flex-start; margin: 0.9rem 0.7rem 0 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  background: transparent; border: 1px solid var(--border); border-radius: 4px;
  color: var(--text-muted); cursor: pointer; font-size: 0.78rem;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.sidebar-hide-btn:hover { color: var(--accent-hi); border-color: var(--accent); background: #ffffff05; }

.sidebar-show-btn {
  display: none; position: fixed; top: 9px; left: 9px; z-index: 300;
  width: 34px; height: 34px; align-items: center; justify-content: center;
  background: var(--bg-card); border: 1px solid var(--border-hi); border-radius: 6px;
  color: var(--text); cursor: pointer; font-size: 0.9rem;
  transition: color 0.12s, border-color 0.12s;
}
.sidebar-show-btn:hover { color: var(--accent-hi); border-color: var(--accent); }

@media (min-width: 992px) {
  .app.sidebar-hidden .sidebar { display: none; }         /* the main flex child expands to fill */
  .app.sidebar-hidden .sidebar-show-btn { display: flex; }
}
@media (max-width: 991px) {
  .sidebar-hide-btn { display: none; }                    /* mobile uses the hamburger overlay instead */
}

.brand-top { display: flex; align-items: center; gap: 0.5rem; justify-content: center;}
.brand-void { font-size: 2.1rem; font-weight: 700; letter-spacing: 0.18em; color: var(--text-bright); line-height: 1; text-shadow: 0 0 24px #8da02033; }
.brand-icon { display: flex; align-items: center; opacity: 0.6; }
.brand-divider { display: flex; align-items: center; gap: 0.4rem; margin: 0.35rem 0; }
.brand-line { flex: 1; height: 1px; background: linear-gradient(90deg, transparent, var(--accent), transparent); }
.brand-diamond { font-size: 0.7rem; color: var(--accent); }
.brand-frontier { font-size: 0.7rem; letter-spacing: 0.38em; color: var(--accent); text-transform: uppercase; font-weight: 600; text-align: center;}

/* ── Sidebar wallet ──────────────────────────────────────────────────────────
   Currencies relocated out of the top header (see includes/header.php). A compact
   icon + value + unit stack that fills otherwise-empty sidebar space. The value
   spans carry .coins/.shards/.premium so the Daily-Bonus claim handler updates
   them live (same classes the old header tiles used). */
.sidebar-wallet {
  display: flex; flex-direction: column; gap: 0.12rem;
  padding: 0.55rem 0.7rem; margin: 0; border-bottom: 1px solid var(--border);
}
.swallet-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.28rem 0.5rem; border-radius: 4px;
  text-decoration: none; color: var(--text-bright);
  border: 1px solid transparent; transition: background 0.15s ease, border-color 0.15s ease;
}
/* Only the linked row (Premium -> shop) reacts to hover; the plain rows are static. */
a.swallet-row:hover { background: #ffffff05; border-color: var(--border-hi); }
.swallet-ico { width: 16px; text-align: center; font-size: 0.82rem; flex-shrink: 0; filter: drop-shadow(0 0 5px currentColor); }
.swallet-val {
  flex: 1; font-family: 'Share Tech Mono', monospace; font-weight: 700;
  font-size: 0.93rem; letter-spacing: 0.02em; color: var(--text-bright);
  text-align: right; line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* Brief glow when the value changes (toggled by the daily-bonus handler reuse). */
.swallet-val { transition: color 0.25s ease, text-shadow 0.25s ease; }
.swallet-unit {
  width: 30px; flex-shrink: 0; font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.08em; color: var(--text-muted); text-transform: uppercase;
}

/* Nav */
.sidebar-nav { flex: 1; padding: 0.4rem 0; }
.nav-section  { margin-top: 0.25rem; }
/* The global link color is :where()-wrapped (zero specificity, top of this file), so a
   bare `.nav-item` (0,1,0) already outranks it for the neutral default — no need to spell
   out :link/:visited the way this rule used to. */
.nav-item {
  display: flex; align-items: center; gap: 0.7rem; padding: 0.58rem 1.2rem;
  /* Three tiers of emphasis: FADED by default (--text-muted) so the menu sits
     quietly, brightening toward white on hover, and only the ACTIVE page wears the
     theme accent (green/amber) — so the screen you're on is the one thing that pops. */
  color: var(--text-muted); text-decoration: none; font-size: 1rem; font-weight: 600;
  /* Letter-spacing trimmed from 0.08em when the font-size grew from 0.82rem
     to 1rem (15px, per the Rajdhani sizing pass) so uppercase labels like
     "Communication" keep fitting the 218px sidebar on one line. */
  letter-spacing: 0.03em; text-transform: uppercase; border-left: 2px solid transparent;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color 0.13s, background 0.13s, border-color 0.13s;
}
.nav-item:hover  { color: var(--text-bright); background: #ffffff04; border-left-color: var(--border-hi); }
.nav-item.active { color: var(--accent-hi); background: #8da02015; border-left-color: var(--accent); }
.nav-item svg { flex-shrink: 0; }
.nav-fa-icon { width: 16px; text-align: center; flex-shrink: 0; font-size: 0.8rem; }

/* Sidebar nav groups (expandable submenus) */
.nav-item-end { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.nav-group-toggle { font-size: 0.7rem; color: var(--text-muted); transition: transform 0.2s ease; flex-shrink: 0; }
.nav-group.open .nav-group-toggle { transform: rotate(180deg); }

/* Blinking attention dot — shown on Communication when there's an unread
   mailbox message or a pending PvP challenge (see includes/header.php). */
.nav-attn-dot {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  background: #ff4040; box-shadow: 0 0 6px 1px #ff404088;
  animation: nav-attn-blink 1.1s ease-in-out infinite;
}
@keyframes nav-attn-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
.nav-submenu {
  display: flex; flex-direction: column; max-height: 0; overflow: hidden;
  transition: max-height 0.22s ease; background: #00000020;
}
.nav-group.open .nav-submenu { max-height: 260px; }
/* The Admin Panel carries far more entries (~20+) than any game menu, so the 260px
   cap clips most tools out of reach. Give it a much taller open state and let it
   scroll inside the sidebar instead of overflowing off-screen. A thin styled
   scrollbar keeps it from looking like an accidental clip. */
.nav-group-admin_tools.open .nav-submenu {
  max-height: min(70vh, 620px); overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border-hi) transparent;
}
.nav-group-admin_tools.open .nav-submenu::-webkit-scrollbar { width: 7px; }
.nav-group-admin_tools.open .nav-submenu::-webkit-scrollbar-thumb {
  background: var(--border-hi); border-radius: 4px;
}
/* Bare class outranks the :where()-wrapped global link color — see the note on .nav-item. */
.nav-sub-item {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.46rem 1.2rem 0.46rem 2.3rem;
  /* Submenu rows are secondary, so they sit a touch dimmer than top-level items
     (var(--text-muted)) but still aren't accent-tinted until active/hover. */
  color: var(--text-muted); text-decoration: none; font-size: 0.93rem; font-weight: 600;
  /* Letter-spacing trimmed from 0.05em alongside the size bump (0.74rem ->
     0.93rem, ~14px) so long entries like "Underground Casino" still ellipsis
     gracefully instead of pushing the row wider. */
  letter-spacing: 0.03em; text-transform: uppercase; border-left: 2px solid transparent;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color 0.13s, background 0.13s, border-color 0.13s;
}
.nav-sub-item .nav-fa-icon { font-size: 0.7rem; opacity: 0.75; }
/* Transient "hacked" look for the player's own callsign while it Matrix-decodes
   into a birthday greeting (see the time-limited surprise script in header.php,
   shown only to Justin's accounts). Terminal-green mono, like the homepage
   hero/down-page hacks. Purely cosmetic + time-limited. */
.player-name.bday-hack {
  color: #34ff8f; text-shadow: 0 0 5px rgba(0, 255, 123, 0.35);
  font-family: 'Share Tech Mono', monospace;
}
.nav-sub-item:hover  { color: var(--text-bright); background: #ffffff04; border-left-color: var(--border-hi); }
.nav-sub-item.active { color: var(--accent-hi); background: #8da02015; border-left-color: var(--accent); }

/* Sidebar global search — quick-jump box for finding a player or alliance
   (clan) by name (see includes/header.php + api/search_global.php). The
   "SEARCH" banner artwork is a purely decorative label sitting above a
   plain text input, so typed text stays readable instead of overlapping
   the artwork; the dropdown of matches opens beneath the input. */
.sidebar-search { position: relative; padding: 0 1.2rem 0.6rem; margin-top: 0.2rem; }
.sidebar-search-banner { display: block; width: 100%; border-radius: 4px; margin-bottom: 0.5rem; }
.sidebar-search-input {
  display: block; width: 100%; padding: 0.5rem 0.7rem;
  background: #00000040; border: 1px solid var(--border-hi); border-radius: 4px;
  color: var(--text-bright); font-family: 'Rajdhani', sans-serif; font-size: 0.74rem;
  font-weight: 500; letter-spacing: 0.02em;
}
.sidebar-search-input::placeholder { color: var(--text-muted); }
/* Same specificity caveat as .form-input:focus below — (0,2,0) beats the global
 * :focus-visible rule, so this needs its own indicator. A border-colour change
 * alone was too subtle to read as focus, so it gets the matching accent ring. */
.sidebar-search-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-hi); }
.sidebar-search-results {
  position: absolute; left: 1.2rem; right: 1.2rem; top: 100%; margin-top: 0.3rem;
  background: rgba(10,14,8,0.97); border: 1px solid var(--border-hi); border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.6); max-height: 260px; overflow-y: auto;
  display: none; z-index: 210;
}
.sidebar-search-results.open { display: block; }
.sidebar-search-result {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.5rem 0.7rem; color: var(--text); text-decoration: none; font-size: 0.78rem;
  border-bottom: 1px solid var(--border);
}
.sidebar-search-result:last-child { border-bottom: none; }
.sidebar-search-result:hover { background: #ffffff08; color: var(--accent-hi); }
.sidebar-search-result-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-search-result-tag {
  flex-shrink: 0; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 0.15rem 0.4rem; border-radius: 3px; font-weight: 700;
}
.sidebar-search-result-tag.player { background: #8da02022; color: var(--accent-hi); }
.sidebar-search-result-tag.clan   { background: #d0484822; color: #e07a7a; }
.sidebar-search-empty { padding: 0.6rem 0.7rem; font-size: 0.74rem; color: var(--text-muted); }

/* Sidebar status — same small-uppercase legibility pass as the header stats:
 * these were 0.62-0.68rem at weight 400 (~9-10px against the 15px root), which is
 * below where condensed Rajdhani holds up in muted grey. */
.sidebar-status { padding: 0.9rem 1.2rem; border-top: 1px solid var(--border); font-size: 0.72rem; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-muted); }
.status-row    { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.55rem; }
.status-online { color: var(--green-ok); font-weight: 700; text-decoration: none; cursor: pointer; }
.status-online:hover { text-decoration: underline; }
.status-label { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; color: var(--text-muted); margin-top: 0.1rem; }
.status-time  { font-family: 'Share Tech Mono', monospace; font-size: 1.1rem; color: var(--text); line-height: 1; }
.status-date  { font-size: 0.72rem; font-weight: 500; color: var(--text-muted); margin-top: 0.2rem; }

/* ─────────────────────────────────────
   MAIN WRAPPER
───────────────────────────────────── */
.main-wrapper { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }

/* ─────────────────────────────────────
   TOP HEADER
───────────────────────────────────── */
.top-header {
  height: var(--header-h); background: var(--bg-sidebar); border-bottom: 1px solid var(--border);
  display: flex; align-items: center; padding: 0 1.25rem; gap: 0.5rem; flex-shrink: 0;
}

.menu-btn {
  background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 0.3rem;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; width: 34px; height: 34px; flex-shrink: 0;
}
.menu-btn:hover { color: var(--text); }

.ham-bar {
  display: block; width: 18px; height: 1.5px; background: currentColor; border-radius: 1px;
  transform-origin: center; pointer-events: none;
  transition: transform 0.24s ease, opacity 0.2s ease, width 0.2s ease;
}
.menu-btn.active .ham-bar:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.menu-btn.active .ham-bar:nth-child(2) { opacity: 0; width: 0; }
.menu-btn.active .ham-bar:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

.header-stats { display: flex; align-items: center; flex: 1; gap: 0; }
.hstat { display: flex; align-items: center; gap: 0.5rem; padding: 0 1rem; border-right: 1px solid var(--border); }
.hstat:first-child { padding-left: 0; }
.hstat-icon  { font-size: 1rem; flex-shrink: 0; line-height: 1; }
.hstat-text  { display: flex; flex-direction: column; gap: 1px; }
/* Muted uppercase micro-label — same legibility pass as .hstat-sub below: weight
 * 500 -> 600 so the condensed strokes survive at this size, and line-height off 1
 * so the caps aren't clipped tight against the value underneath. */
.hstat-label { font-size: 0.74rem; font-weight: 600; letter-spacing: 0.08em; color: var(--text-muted); text-transform: uppercase; line-height: 1.15; }
.hstat-value { font-size: 0.95rem; font-weight: 500; color: var(--text-bright); line-height: 1; letter-spacing: 0.02em; }
.hstat-value.coins   { color: var(--accent-hi); }
.hstat-value.shards  { color: var(--purple-hi); }
.hstat-value.premium { color: var(--gold); }
.hstat-value.avail   { color: var(--accent-hi); }
.hstat-value.active  { color: var(--gold); }

/* ── Rank tile (leftmost header stat: "level / power" + insignia) ──────────────
 * Relative so the insignia can pin to the tile's top-right corner, and padded a
 * little on the right so the badge never collides with the "/ power" number.
 * The tile is compact by design (no bar, short numbers) so it costs the row far
 * less width than the Health/Stamina tiles beside it. */
.hstat-rank { position: relative; outline: none; }
/* The "/" between Skill Rank and Power Level — dimmer and tighter than the two
 * numbers so "5 / 20" reads as one value, not two stats jammed together. */
.hstat-rank .hstat-value  { display: flex; align-items: baseline; gap: 0.18rem; }
.hstat-rank .hstat-sep    { color: var(--text-muted); font-weight: 400; }
.hstat-rank .js-power     { color: var(--accent-hi); }

/* Insignia badge. Every shape is a clip-path on the empty .rank-pip spans, so
 * PHP (military_rank_insignia_html) and JS (main.js rank refresh) only ever have
 * to emit the right COUNT of pips and set data-shape — no SVG, no images.
 *   chevron -> stacked bottom-up (like sleeve stripes)
 *   bar / diamond / star -> laid out side by side
 * Colour comes from data-tier via currentColor, so all pips of a rank match. */
.rank-insignia {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 2px; line-height: 0; color: var(--text-muted);
}
/* The insignia stands IN for this tile's icon — it occupies the same slot the
 * fa-heart-pulse / fa-bolt glyphs do on the neighbouring tiles, left of the
 * numbers. The dark plate + hairline border makes it read as a worn BADGE rather
 * than as decoration on the "LEVEL / POWER" label beside it, and the fixed
 * min-width/height keeps the plate from resizing as pip shapes change between
 * ranks (which would jitter the whole row's alignment on promotion). */
.rank-insignia-icon {
  flex: none;
  min-width: 26px; min-height: 22px; padding: 2px 3px;
  background: rgba(0,0,0,0.32); border: 1px solid var(--border);
  border-radius: 3px;
}
.rank-insignia[data-shape="chevron"] { flex-direction: column-reverse; gap: 1px; }
.rank-pip { display: block; background: currentColor; flex: none; }

/* Chevron: a "^" cut out of a small block, stacked like sleeve stripes. */
.rank-insignia[data-shape="chevron"] .rank-pip {
  width: 14px; height: 4px;
  clip-path: polygon(50% 0, 100% 100%, 78% 100%, 50% 40%, 22% 100%, 0 100%);
}
/* Bar: a plain vertical stripe (junior-officer style, worn side by side). */
.rank-insignia[data-shape="bar"] .rank-pip { width: 3px; height: 13px; border-radius: 1px; }
/* Diamond: rotated square, field-officer style. */
.rank-insignia[data-shape="diamond"] .rank-pip {
  width: 12px; height: 12px;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
/* Star: five-point, flag-officer style. */
.rank-insignia[data-shape="star"] .rank-pip {
  width: 13px; height: 13px;
  clip-path: polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

/* Keyboard focus: the tile is tabbable (it owns the rank tooltip), so give it a
 * visible ring — .hstat-rank kills the default outline to avoid a boxy focus rect
 * on mouse click, and this puts a deliberate one back for keyboard users. */
.hstat-rank:focus-visible { box-shadow: inset 0 0 0 1px var(--border-hi); border-radius: 3px; }

/* Tier colours — a visible promotion cue as you climb: dull steel while
 * enlisted, warming through amber/gold, ending on void purple at flag rank. */
.rank-insignia[data-tier="enlisted"] { color: #9aa88c; }
.rank-insignia[data-tier="nco"]      { color: var(--accent-hi); }
.rank-insignia[data-tier="officer"]  { color: var(--gold); filter: drop-shadow(0 0 3px rgba(212,175,55,0.35)); }
.rank-insignia[data-tier="command"]  { color: var(--purple-hi); filter: drop-shadow(0 0 4px rgba(144,96,224,0.45)); }

/* Rank tooltip reuses the .hstat-tooltip panel below verbatim — including its
 * position: fixed. That is NOT decorative: `.header-stats` is overflow:hidden on
 * desktop (see the ≥992px block) so an absolutely-positioned panel hanging below
 * the row would be clipped away. Fixed positioning escapes the clip, and main.js
 * sets the top/left from the tile's bounding rect on hover/focus. */

/* Region tile is a link into the Zone Map. Inherit the plain-stat look (no link
 * underline/colour) but give a subtle hover cue + the "Travel to zone" sub-line. */
.hstat-link { text-decoration: none; color: inherit; cursor: pointer; transition: background 0.15s ease; }
.hstat-link:hover { background: rgba(255,255,255,0.04); }

/* ── Action tile (a stat tile that is a <button>) ──
   Used by the Unspent XP badge: XP the player has earned from a bounty, mission
   or quest but not yet distributed across their attributes. Clicking it opens the
   allocator dialog (assets/js/reward-box.js binds .js-xp-pending globally), so it
   is a real button rather than a link — it does not navigate anywhere.
   Resets the browser's button chrome so it lays out identically to the .hstat
   divs beside it, then pulses so a fresh reward is noticed without being loud. */
.hstat-action {
  appearance: none; font: inherit; text-align: left; color: inherit; cursor: pointer;
  background: none; border: 0; border-right: 1px solid var(--border);
  transition: background 0.15s ease;
}
.hstat-action:hover { background: rgba(46,207,207,0.07); }
.hstat-action:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: -2px; }
.hstat-action .hstat-value { color: #2ecfcf; font-family: 'Share Tech Mono', monospace; font-weight: 700; }
.hstat-action .hstat-icon  { animation: hstat-xp-pulse 2.6s ease-in-out infinite; }
@keyframes hstat-xp-pulse {
  0%, 100% { text-shadow: none; }
  50%      { text-shadow: 0 0 10px #2ecfcf; }
}
/* Nothing outstanding — the tile leaves the layout entirely rather than sitting
   there reading zero. !important is required: the ≤991 dropdown sets
   `#header-stats .hstat { display: flex !important }` to force every tile visible
   in the collapsed panel, and an empty badge must stay hidden even there. */
.hstat-action.is-empty { display: none !important; }
@media (prefers-reduced-motion: reduce) {
  .hstat-action .hstat-icon { animation: none; }
}
.hstat-link:hover .hstat-value.js-region { text-decoration: underline; }
/* Legibility pass: this was 0.58rem/400 which, against the 15px root, rendered at
 * ~8.7px. Rajdhani is condensed with thin strokes, so uppercase at that size turned
 * to mush — contrast was never the issue (accent-hi on the sidebar is ~9:1), stroke
 * weight and x-height were. Bumped to ~10px at 600, with more tracking (condensed
 * uppercase needs air between letters to stay separable) and line-height off 1. */
.hstat-sub {
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.09em;
  color: var(--accent-hi); text-transform: uppercase; line-height: 1.2; margin-top: 2px;
}

/* Daily Bonus stat is a <button> (clickable when available) but needs to look
 * identical to the plain .hstat divs around it — strip the native button chrome. */
.hstat-daily-bonus {
  background: none; border: 0; border-right: 1px solid var(--border);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.hstat-daily-bonus:disabled { cursor: default; }
.hstat-daily-bonus:not(:disabled):hover .hstat-value.avail { text-decoration: underline; }

/* Hover/focus tooltip on the Daily Bonus stat — explains what the reward was
 * (already claimed) or will be (still available) without needing to click it.
 * JS (header.php) re-parents this onto <body> and toggles .visible, rather than
 * a CSS :hover rule — `.header-stats` has overflow:hidden (so a long stats row
 * can't push the player menu off-screen) which would otherwise clip it, since
 * overflow clipping applies to descendants regardless of position scheme. */
.hstat-tooltip {
  position: fixed; width: 230px; padding: 0.65rem 0.8rem; border-radius: 8px;
  background: rgba(10,14,8,0.97); border: 1px solid var(--border-hi);
  box-shadow: 0 8px 24px rgba(0,0,0,0.6);
  font-size: 0.72rem; line-height: 1.45; color: var(--text-muted);
  font-weight: 400; text-transform: none; letter-spacing: normal; white-space: normal;
  opacity: 0; pointer-events: none; transition: opacity 0.12s ease;
  z-index: 9000;
}
.hstat-tooltip.visible { opacity: 1; }
/* display:block so the title still owns its own line when the tooltip body is
 * built from inline <span>s (the rank tile) rather than block elements. */
.hstat-tooltip-title {
  display: block;
  font-size: 0.78rem; font-weight: 700; color: var(--gold);
  letter-spacing: 0.02em; margin-bottom: 0.3rem;
}
.hstat-tooltip .reward-line { display: block; margin-top: 0.3rem; color: var(--accent-hi); font-weight: 600; }
.hstat-tooltip strong { color: var(--text-bright); }

/* ── Stats wrapper / collapse toggle ──
   On desktop the wrapper is display:contents so .header-stats lays out exactly
   as the inline flex:1 row it always was, and the toggle pill is hidden. Both
   flip at the tablet breakpoint (<=991px) — see the media query below. */
.header-stats-wrap { display: contents; }
.stats-toggle {
  display: none; align-items: center; gap: 0.4rem;
  background: var(--bg-card2); border: 1px solid var(--border-hi); border-radius: 3px;
  color: var(--text); font-family: inherit; font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; padding: 0.42rem 0.7rem; cursor: pointer;
  transition: border-color 0.13s, color 0.13s;
}
.stats-toggle > i { color: var(--accent-hi); }
.stats-toggle:hover { border-color: var(--accent-hi); color: var(--text-bright); }

.xp-wrap      { display: flex; flex-direction: column; gap: 3px; }
/* Header vitals bars mirror the in-combat player-card HUD bars
   (assets/css/combat-ftl.css .cft-bar / .cft-bar-fill / .cft-bar-seg): a taller,
   dark-inset rounded track holding a glowing colored fill, overlaid by fine
   "segment" notches so the top-bar vitals read the same as the PvE/PvP cards.
   The fill stays width-driven by its inline style + the .js-*-bar hooks, so no
   header markup or JS change is needed — the notch overlay is a ::after pseudo
   on the track, painted above the fill (the track clips the fill's glow inward,
   exactly like .cft-bar's overflow:hidden). */
.xp-bar-track { position: relative; height: 11px; width: 78px; background: #0a0f0b;
  border: 1px solid #1d2c22; border-radius: 4px; overflow: hidden; }
.xp-bar-fill  { height: 100%; background: var(--accent); border-radius: 3px;
  box-shadow: 0 0 7px var(--accent); transition: width 0.4s ease; }
/* Segment notches: thin dark bands every ~11px, matching .cft-bar-seg's look. */
.xp-bar-track::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: repeating-linear-gradient(90deg, transparent 0 9px, #05080a 9px 11px); }
.hp-bar-fill   { background: #d04848; box-shadow: 0 0 7px rgba(208,72,72,0.6); }
.stam-bar-fill { background: #e0a93a; box-shadow: 0 0 7px rgba(224,169,58,0.6); }
.void-bar-fill { background: #9060e0; box-shadow: 0 0 7px rgba(144,96,224,0.6); }
.rested-xp-bar-fill { background: #8aa8e8; box-shadow: 0 0 7px rgba(138,168,232,0.6); }

/* ── Segmented HUD bar (canonical, reusable) ──────────────────────────────────
   The site-wide progress / stat bar style, matching the in-combat player cards
   (assets/css/combat-ftl.css .cft-bar). EVERY value bar in the game — XP, health,
   stamina, void, shields, quest/mission progress, meters, cooldowns — uses this
   look: a dark inset rounded track holding a glowing colored fill, overlaid by
   fine "segment" notches. NEW bars should use these classes directly:
       <div class="vf-bar"><span class="vf-bar-fill" style="width:60%"></span></div>
   Recolor a fill by overriding its background + a matching glow (the .hp/.xp/…
   modifier pattern above). Existing feature bars replicate this SAME treatment in
   their own CSS files so page-CSS load order stays correct — search the repo for
   "segment notches" to find them. The notch pitch (9px lit / 2px gap, over a
   #0a0f0b track with a #1d2c22 edge) is the canon: keep it identical everywhere
   so every bar reads as one system. */
.vf-bar { position: relative; height: 11px; background: #0a0f0b; border: 1px solid #1d2c22;
  border-radius: 4px; overflow: hidden; display: block; }
.vf-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--accent);
  box-shadow: 0 0 7px var(--accent); transition: width 0.4s ease; }
.vf-bar::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: repeating-linear-gradient(90deg, transparent 0 9px, #05080a 9px 11px); }

/* ── Shared toast (window.VF.toast) ──
   A single bottom-centre notification reused for brief, styled in-app messages
   (e.g. "Your health is already full.") in place of window.alert(). */
.vf-toast {
  position: fixed; left: 50%; bottom: 30px; transform: translateX(-50%) translateY(12px);
  z-index: 11000; max-width: 90vw;
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.7rem 1.1rem; border-radius: 8px;
  background: rgba(10,14,8,0.97); border: 1px solid var(--border-hi); border-left-width: 3px;
  color: var(--text-bright); font-size: 0.85rem; font-weight: 600; letter-spacing: 0.02em;
  box-shadow: 0 8px 28px rgba(0,0,0,0.6);
  opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.vf-toast.show       { opacity: 1; transform: translateX(-50%) translateY(0); }
.vf-toast-info       { border-left-color: var(--accent); }
.vf-toast-success    { border-left-color: var(--accent-hi); }
.vf-toast-error      { border-left-color: #d04848; }
.vf-toast::before {
  font-family: 'Font Awesome 6 Free'; font-weight: 900; font-size: 0.95rem; flex-shrink: 0;
}
.vf-toast-info::before    { content: '\f05a'; color: var(--accent-hi); }   /* circle-info */
.vf-toast-success::before { content: '\f058'; color: var(--accent-hi); }   /* circle-check */
.vf-toast-error::before   { content: '\f06a'; color: #d04848; }            /* circle-exclamation */

/* ── Requirements pop-up (window.VFReqPopup) ──
   Blocks an equip / quick-slot assignment when the character doesn't meet an item's
   stat/skill requirements, listing every requirement met (green) or unmet (red) with
   the character's current level. Built and shown by quickslots.js; one reused node is
   appended to <body>. z-index sits above the item colorbox (10000) and toast (11000). */
.vf-req-overlay {
  position: fixed; inset: 0; z-index: 12000;
  display: none; align-items: center; justify-content: center;
  background: rgba(2, 4, 2, 0.72); padding: 1rem;
}
.vf-req-overlay.open { display: flex; }
.vf-req-box {
  position: relative; width: 100%; max-width: 340px;
  max-height: 90vh; overflow-y: auto;
  padding: 1rem 1.1rem 1.1rem; border-radius: 10px;
  background: rgba(10, 14, 8, 0.98); border: 1px solid var(--border-hi);
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.75);
}
.vf-req-close {
  position: absolute; top: 6px; right: 8px;
  width: 24px; height: 24px; padding: 0; line-height: 22px;
  border: none; border-radius: 4px; background: rgba(0, 0, 0, 0.45);
  color: var(--text-muted); font-size: 18px; cursor: pointer;
  transition: color 0.12s, background 0.12s;
}
.vf-req-close:hover { color: var(--text-bright); background: rgba(0, 0, 0, 0.7); }
.vf-req-title {
  display: flex; align-items: center; gap: 0.45rem;
  font-size: 0.95rem; font-weight: 700; letter-spacing: 0.03em;
  color: #FF8A7A; margin-bottom: 0.4rem; padding-right: 1.2rem;
}
.vf-req-title i { color: #F5C542; }
.vf-req-sub {
  font-size: 0.78rem; color: var(--text-muted); margin-bottom: 0.7rem; line-height: 1.4;
}
.vf-req-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.vf-req-row {
  display: flex; align-items: center; gap: 0.45rem;
  font-size: 0.82rem; font-weight: 600; letter-spacing: 0.02em;
}
.vf-req-row i { flex-shrink: 0; width: 1rem; text-align: center; }
.vf-req-met   { color: #7CFF9E; }
.vf-req-unmet { color: #FF8A7A; }
.vf-req-have  { color: var(--text-muted); font-weight: 400; font-style: italic; }
.vf-req-ok {
  display: block; width: 100%; margin-top: 0.95rem; padding: 0.5rem 0.6rem;
  font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  border-radius: 4px; border: 1px solid var(--accent-hi);
  background: var(--accent-hi); color: var(--bg-card); cursor: pointer;
  transition: filter 0.12s;
}
.vf-req-ok:hover { filter: brightness(1.12); }

.server-time-bar {
  display: flex; align-items: center; gap: 0.5rem; margin-left: auto;
  padding: 0.35rem 0.85rem; background: var(--bg-card2); border: 1px solid var(--border-hi);
  border-radius: 4px; font-family: 'Share Tech Mono', monospace; font-size: 0.78rem;
  color: var(--text-bright); flex-shrink: 0;
}
.server-time-bar i { color: var(--accent-hi); }
.server-time-date { color: var(--text-muted); letter-spacing: 0.04em; }
.server-time-tz   { color: var(--accent-hi); font-weight: 700; letter-spacing: 0.06em; }

.player-menu-wrap { position: relative; }

.header-player {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.25rem 0.6rem;
  border: 1px solid var(--border); border-radius: 3px; cursor: pointer; user-select: none;
  transition: border-color 0.13s;
}
.header-player:hover,
.header-player.active { border-color: var(--border-hi); background: #ffffff03; }

.player-avatar {
  width: 34px; height: 34px; border-radius: 2px; background: var(--bg-card2);
  border: 1px solid var(--border-hi); overflow: hidden; flex-shrink: 0;
}

.avatar-img { width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; display: block; }

.player-info   { display: flex; flex-direction: column; gap: 1px; }
.player-name   { font-size: 0.88rem; font-weight: 700; color: var(--text-bright); letter-spacing: 0.06em; line-height: 1; }
.player-class  { font-size: 0.7rem; color: var(--text-muted); letter-spacing: 0.06em; text-transform: uppercase; }
.player-chevron { color: var(--text-muted); font-size: 0.7rem; display: inline-block; transition: transform 0.2s ease; }
.header-player.active .player-chevron { transform: rotate(180deg); }

/* ─────────────────────────────────────
   PLAYER DROPDOWN
───────────────────────────────────── */
.player-dropdown {
  position: absolute; top: calc(100% + 9px); right: 0; width: 306px;
  background: var(--bg-card); border: 1px solid var(--border-hi); border-radius: 3px;
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.72), 0 0 0 1px #8da02012;
  z-index: 500; display: flex; flex-direction: row;
  /* Scroll instead of clipping when the menu is taller than the viewport
     (e.g. mobile landscape). overflow-x stays hidden to preserve corner radius. */
  max-height: calc(100vh - var(--header-h) - 18px);
  max-height: calc(100dvh - var(--header-h) - 18px);
  overflow-y: auto; overflow-x: hidden;
  opacity: 0; transform: translateY(-6px) scale(0.98); pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
/* Quick-slot hotbar: a horizontal row sitting in the panel flow, below the
   Level/XP blocks. (It was once a vertical left rail; a row reads as a hotbar
   and keeps the menu a single clean column.) */
.pd-slotbar {
  display: flex; flex-direction: row; gap: 6px;
  padding: 0.55rem 1rem; background: var(--bg-card2);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.pd-slot {
  /* Equal-width square slots that share the row. min-width:0 lets them shrink to
     fit narrow phones; max-width stops 5 slots from ballooning on a wide menu. */
  flex: 1 1 0; min-width: 0; max-width: 54px; aspect-ratio: 1 / 1; background: #0a0e07;
  border: 1px solid var(--border); border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; position: relative; transition: border-color 0.12s, background 0.12s;
}
.pd-slot:hover { border-color: var(--accent); background: #111a07; }
/* Slot number sits top-left; the owned-stack count (.qs-qty) sits bottom-right. */
.pd-slot-num {
  font-size: 0.7rem; color: var(--text-muted); letter-spacing: 0.04em;
  position: absolute; top: 2px; left: 3px; line-height: 1;
}
/* Stack count: distinct gold so it reads separately from the muted slot number. */
.qs-qty {
  font-size: 0.7rem; color: var(--gold-hi); font-weight: 700; letter-spacing: 0.02em;
  position: absolute; bottom: 2px; right: 3px; line-height: 1; pointer-events: none;
}

/* ── Quick-slot states (shared by the dropdown bar and the inventory bar) ── */
.pd-slot.filled { border-color: var(--accent); background: #111a07; }
.pd-slot .qs-icon { font-size: 1rem; color: var(--accent-hi); pointer-events: none; }
/* Item artwork shown in place of the FontAwesome icon when an item has one. */
.pd-slot .qs-img { width: 78%; height: 78%; object-fit: contain; pointer-events: none; }
.pd-slot.qs-drop { border-color: var(--accent-hi); background: #16210a; box-shadow: 0 0 0 2px #8da02033; }
/* Requirement gate: a slotted item whose stat/skill requirements the character
 * doesn't meet gets a red transparency overlay so it reads as "owned but unusable".
 * pointer-events:none keeps the slot fully interactive (clear ×, click-to-use/open
 * all still work); the red border reinforces the locked state. Set server-side on
 * each bar and kept in sync by quickslots.js across assigns/rearranges. */
.pd-slot.qs-locked { border-color: #b3402f; }
.pd-slot.qs-locked::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: rgba(190, 45, 30, 0.42); pointer-events: none; z-index: 2;
}
/* ── Durability wear states (includes/durability.php → durability_state) ──
 * WORN (at or below DURABILITY_WARN_PCT = 10% of the copy's ceiling): an AMBER
 * overlay — the item still works, but it is one or two swings from snapping.
 * BROKEN (0 durability): a RED overlay plus the .qs-broken-mark glyph on top, and
 * the item cannot be used at all until it is repaired.
 *
 * Exactly ONE of qs-worn / qs-locked / qs-broken is ever set on a tile (the renderer
 * picks by precedence broken > requirement-lock > worn) because all three paint the
 * same ::after layer. These rules sit AFTER .qs-locked so that, if a stale/JS-toggled
 * tile ever carries two of them, the wear state still wins on equal specificity. */
.pd-slot.qs-worn { border-color: #c9971f; }
.pd-slot.qs-worn::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: rgba(214, 163, 39, 0.38); pointer-events: none; z-index: 2;
}
.pd-slot.qs-broken { border-color: #d04848; }
.pd-slot.qs-broken::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: rgba(190, 45, 30, 0.52); pointer-events: none; z-index: 2;
}
/* The broken glyph rides ABOVE the red overlay (z-index 3 vs the overlay's 2) and is
   click-through, so the slot stays draggable/clearable exactly as before. */
.pd-slot .qs-broken-mark {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  font-size: 0.95rem; color: #ffdede; text-shadow: 0 1px 3px rgba(0,0,0,0.9);
  pointer-events: none; z-index: 3;
}
/* Dim the item art under a broken overlay so the glyph reads clearly. */
.pd-slot.qs-broken .qs-img,
.pd-slot.qs-broken .qs-icon { opacity: 0.45; filter: grayscale(0.6); }
/* ══════════════════════════════════════════════════════════════════════════════
   RARE MARKER — "the swirl": a highlight travelling around the item's border
   ══════════════════════════════════════════════════════════════════════════════
   A rare weapon must read as rare WITHOUT a tooltip. Driven by items.vfx_aura, surfaced on
   every renderer as data-aura.

   ⚠ WHY THIS LIVES IN main.css AND NOT inventory.css

   Because main.css is the only sheet loaded on EVERY page (includes/header.php), and the
   marker's surfaces are not. The quick-slot bar is in the header dropdown site-wide; the
   paper doll is on inventory.php AND profile.php; the combat tiles are on the FTL board,
   which loads combat-ftl.css and NOT inventory.css (api/attack.php: $pageCss = levelup.css +
   combat-ftl.css). Defining the ring in a page sheet meant it appeared on one screen and
   silently vanished on the others — so the RING is defined here, once, and the per-surface
   sheets carry only the adjustments they genuinely need.

   WHY data-aura AND NOT .rarity-epic
   ----------------------------------
   items.rarity already has a job: since sql/2026_08_06_generic_part_classes_restored.sql it
   is the RESEARCH ROLL WEIGHT (research_rarity_weights). Styling the marker off rarity would
   mean a future retune of drop odds silently restyles the game, and would put a halo on every
   epic armour piece too. vfx_aura is set on exactly the ten rare weapons and nothing else.

   ⚠ HOW THE RING IS CONTAINED — and why this is not the ORIGINAL swirl's bug again.

   The first version of this effect painted a conic gradient across the tile's whole area and
   tried to carve a donut out of it with `radial-gradient(circle closest-side, transparent 72%,
   #000 82%, #000 100%)`. Its own comment claimed closest-side kept the arcs inside the tile.
   It does not: closest-side sizes the CIRCLE to min(half-width, half-height), but the mask
   still runs to the element's edges and every stop from 82% OUTWARD is opaque — including the
   four corners the circle never reaches. The arcs painted into those corners, so each rare
   tile grew two amber triangles hanging off it. That is what "the swirl looks wrong" was.

   This paints the same conic gradient but masks it with the ELEMENT'S OWN BOX GEOMETRY: two
   identical mask layers, one clipped to the content box and one to the border box, composited
   with `exclude`. What survives is exactly border-box-minus-content-box — the ring — at ANY
   size, ratio or corner radius. There is no circle involved and nothing that can spill,
   because the mask is defined by the same box that clips it.

   ⚠ WHY @property AND NOT transform: rotate()

   The mask lives on this element, so rotating the element rotates its mask with it and the
   ring never appears to move. Animating the gradient's own START ANGLE keeps the geometry
   pinned and sweeps only the colour — which is the effect: a bright head running the border.

   @property is Chrome 85+ / Safari 16.4+ / Firefox 128+. Where unsupported the custom property
   is not animatable, so the head parks at its initial angle and reads as a static highlight.
   The border tint and glow carry "this is rare" on their own, so an old browser loses the
   motion and nothing else.

   ⚠ AUTHORING NOTE — the orphaned-comment trap. Prose inserted mid-file can leave a stray
   */ /* which invalidates the NEXT selector and silently drops the rule. Brace-balance checks
   FALSE-PASS on that. Verify through the browser CSSOM, not by eye. */

/* The marker colour in one place: a future legendary aura is a new data-aura value plus an
   override of these two properties, not a second copy of everything below. */
[data-aura="rare_swirl"] { --vf-aura: 255, 182, 72; --vf-aura-edge: #7a5c24; }

@property --vf-aura-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
@keyframes vf-aura-sweep { to { --vf-aura-angle: 360deg; } }

/* THE RING ITSELF — deliberately unscoped, so a surface joins simply by emitting the
   attribute. It is safe to leave unscoped precisely because it cannot escape its own box. */
[data-aura="rare_swirl"] { position: relative; }
[data-aura="rare_swirl"]::before {
  content: '';
  position: absolute;
  inset: -1px;               /* sit ON the 1px border rather than inside the padding */
  border-radius: inherit;
  padding: 2px;              /* ring thickness — this is what the mask carves out */
  z-index: 3;                /* over the artwork; the ring is the whole point */
  pointer-events: none;      /* never eat a drag, a click or a hover */
  /* One bright head with a short warm tail and a long dead stretch, so it reads as ONE thing
     travelling rather than as a spinning pinwheel. */
  background: conic-gradient(from var(--vf-aura-angle),
      transparent 0deg,
      transparent 205deg,
      rgba(var(--vf-aura), .35) 245deg,
      rgba(var(--vf-aura), .95) 288deg,
      rgba(255, 248, 224, 1)   303deg,
      rgba(var(--vf-aura), .85) 318deg,
      rgba(var(--vf-aura), .25) 340deg,
      transparent 356deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  animation: vf-aura-sweep 2.6s linear infinite;
}

/* Static tint + glow for the two surfaces whose own sheets do not already carry one (the bag
   tile is handled in inventory.css, the combat tiles in combat-ftl.css — both need to outrank
   a rarity ladder local to those files).

   ⚠ The paper-doll selector doubles up on [data-rarity] on purpose. paperdoll.css sets
   `.equip-slot[data-rarity="epic"]` at (0,2,0) and loads AFTER this file, so a plain
   `.equip-slot[data-aura]` would tie and LOSE on source order — leaving a rare weapon on the
   doll wearing plain epic purple. The slot always carries data-rarity, so matching it costs
   nothing and buys the (0,3,0) needed to win. */
.pd-slot.quickslot[data-aura="rare_swirl"],
.equip-slot[data-rarity][data-aura="rare_swirl"] {
  border-color: var(--vf-aura-edge);
  box-shadow: inset 0 0 12px rgba(var(--vf-aura), .13), 0 0 6px rgba(var(--vf-aura), .16);
}
.pd-slot.quickslot[data-aura="rare_swirl"]:hover,
.equip-slot[data-rarity][data-aura="rare_swirl"]:hover {
  border-color: #e8b448;
  box-shadow: inset 0 0 15px rgba(var(--vf-aura), .30), 0 0 14px rgba(var(--vf-aura), .48);
}
/* A warm cast on the artwork so the item reads as lit BY the marker, not as sitting in front
   of a decal. This is the one piece of the original swirl that always worked. */
.pd-slot.quickslot[data-aura="rare_swirl"] > .qs-img,
.equip-slot[data-aura="rare_swirl"] img { filter: drop-shadow(0 0 4px rgba(var(--vf-aura), .45)); }

/* ⚠ REDUCED MOTION: the ring STAYS, the sweep STOPS. Dropping it outright would put a rare
   weapon back to looking like its neighbours, which is a worse accessibility outcome than a
   still ring — the same posture the rest of this file takes. Parked at 300deg so what remains
   on screen is the bright head, not the dead stretch. */
@media (prefers-reduced-motion: reduce) {
  [data-aura="rare_swirl"]::before { animation: none; --vf-aura-angle: 300deg; }
}

/* Filled slots can be dragged to another slot to rearrange the bar. */
.pd-slot.filled { cursor: grab; }
.pd-slot.dragging { opacity: 0.5; cursor: grabbing; }
.qs-clear {
  position: absolute; top: 1px; right: 2px;
  width: 13px; height: 13px; padding: 0; line-height: 11px;
  border: none; border-radius: 3px; background: rgba(0,0,0,0.55); color: #d98a6a;
  font-size: 12px; cursor: pointer; display: none;
}
.pd-slot.filled:hover .qs-clear { display: block; }
.pd-panel { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.player-dropdown.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.player-dropdown::before {
  content: ''; position: absolute; top: -5px; right: 22px; width: 9px; height: 9px;
  background: var(--bg-card2); border-left: 1px solid var(--border-hi); border-top: 1px solid var(--border-hi);
  transform: rotate(45deg);
}

/* Centred character card: thought bubble + class GIF stacked, then name/class
   below. A row layout broke here — the class GIF loads at its native 256px and
   shoved the name/class clean off the right edge of the 306px menu. */
.pd-head { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.45rem; padding: 0.85rem 1rem; border-bottom: 1px solid var(--border); background: var(--bg-card2); }
/* Cap the GIF so it can never blow out the menu width again. */
.pd-head-avatar  { flex-shrink: 0; width: 128px; max-width: 100%; border-radius: 2px; overflow: hidden; line-height: 0; }
.pd-head-avatar .avatar-img { width: 100%; height: auto; }
.pd-head-name    { font-size: 0.92rem; font-weight: 700; color: var(--text-bright); letter-spacing: 0.04em; line-height: 1.2; }
.pd-head-class   { font-size: 0.7rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 0.15rem; }

/* Avatar column: stacks thought bubble above the class GIF */
.pd-avatar-col { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; }

/* Thought bubble and its trailing dots */
.pd-think-wrap  { display: flex; flex-direction: column; align-items: center; margin-bottom: -10px; max-width: 100%; }
/* nowrap is fine now the head is centred (bubble has the full panel width), but
   cap + ellipsis guards against an over-long flavour line on a narrow phone. */
.pd-think-bubble { background: #fff; border: 1px solid #ccc; border-radius: 10px; padding: 3px 8px; font-size: 0.82rem; color: #111; white-space: nowrap; letter-spacing: 0.04em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; transition: opacity 0.3s; }
.pd-think-dots  { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: 3px; }
/* Three dots shrink as they approach the avatar, giving the classic thought-bubble tail */
.pd-think-dots span { display: block; background: #ccc; border-radius: 50%; }
.pd-think-dots span:nth-child(1) { width: 5px; height: 5px; }
.pd-think-dots span:nth-child(2) { width: 3px; height: 3px; }
.pd-think-dots span:nth-child(3) { width: 2px; height: 2px; }

.pd-stats { display: grid; grid-template-columns: repeat(2, 1fr); border-bottom: 1px solid var(--border); }
.pd-stats.pd-stats-single { grid-template-columns: 1fr; }
.pd-stat  { padding: 0.6rem 0.5rem; text-align: center; border-right: 1px solid var(--border); }
.pd-stat:last-child { border-right: none; }
.pd-stat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.18rem; }
/* Flex (not the plain text line it used to be) because the Level/Power row now
 * carries the military rank insignia to the LEFT of the numbers, exactly like the
 * header rank tile — the badge is a flex box itself and needs to sit on the same
 * centre line as the digits rather than on the text baseline. */
.pd-stat-val   { display: flex; align-items: center; justify-content: center; font-size: 0.9rem; font-weight: 700; color: var(--text-bright); font-family: 'Share Tech Mono', monospace; letter-spacing: 0.02em; }
.pd-stat-val .pd-stat-sep { color: var(--text-muted); margin: 0 0.35rem; font-weight: 400; }
/* Match the header tile: the badge is a plate to the left of the pair, and the
 * Power number carries the accent colour so the two readouts look identical. */
.pd-stat-val .rank-insignia-icon { margin-right: 0.45rem; }
.pd-stat-val .js-pd-power { color: var(--accent-hi); }
/* The dropdown has no hover tooltip (the header tile does), so the rank NAME is
 * printed under the numbers instead — otherwise the pips are unreadable here. */
.pd-stat-rank { margin-top: 0.2rem; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.09em; color: var(--text-muted); }
.pd-coins   { color: var(--accent-hi); }
.pd-shards  { color: var(--purple-hi); }
.pd-premium { color: var(--gold); }

.pd-xp-wrap   { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); }
.pd-xp-labels { display: flex; justify-content: space-between; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.38rem; }
.pd-xp-pct    { color: var(--accent-hi); }
/* Segmented HUD bar — see the canonical .vf-bar in this file. */
.pd-xp-track  { position: relative; height: 11px; background: #0a0f0b; border: 1px solid #1d2c22; border-radius: 4px; overflow: hidden; }
.pd-xp-fill   { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-hi)); border-radius: 3px; box-shadow: 0 0 7px #aac42466; }
.pd-xp-track::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: repeating-linear-gradient(90deg, transparent 0 9px, #05080a 9px 11px); }
.pd-xp-sub    { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.3rem; letter-spacing: 0.03em; }

/* ── Character class switcher (inside the player dropdown) ──────────────── */
.pd-chars { padding: 0.35rem 0; border-top: 1px solid var(--border); }
.pd-section-label {
  padding: 0.3rem 1rem 0.2rem;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-muted);
}

/* Collapsible section header (shared by "My Characters" and "Settings"): a
   full-width clickable row that expands the body below. The caret flips when
   open. Any .pd-collapse wrapper + .pd-collapse-toggle + .pd-collapse-body gets
   the same behaviour (toggled by js/main.js). */
.pd-collapse-toggle {
  display: flex; align-items: center; gap: 0.5rem;
  width: 100%; padding: 0.32rem 1rem 0.28rem;
  background: none; border: 0; font-family: inherit; cursor: pointer;
  text-align: left; transition: background 0.12s;
}
.pd-collapse-toggle:hover { background: #ffffff07; }
.pd-collapse-toggle .pd-section-label { padding: 0; flex: 0 0 auto; }
/* Active-callsign hint on the My Characters header, right-aligned so the
   collapsed state still reads who you're playing. */
.pd-chars-active {
  flex: 1 1 auto; min-width: 0; text-align: right;
  font-size: 0.72rem; font-weight: 700; color: var(--gold-hi);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* margin-left:auto pins the caret to the right edge whether or not the header
   has a middle element (My Characters has the callsign; Settings does not). */
.pd-collapse-caret {
  flex: 0 0 auto; margin-left: auto; font-size: 0.7rem; color: var(--text-muted);
  transition: transform 0.18s ease;
}
.pd-collapse-toggle[aria-expanded="true"] .pd-collapse-caret { transform: rotate(180deg); }
.pd-collapse-toggle[aria-expanded="true"] .pd-chars-active { color: var(--text-muted); }

/* Body is collapsed (max-height 0) until .pd-collapse-open is set by the toggle. */
.pd-collapse-body { max-height: 0; overflow: hidden; transition: max-height 0.22s ease; }
.pd-collapse.pd-collapse-open .pd-collapse-body { max-height: 360px; }

.pd-char-row {
  display: flex; align-items: center; gap: 0.5rem;
  width: 100%; padding: 0.45rem 1rem;
  background: none; border: 0; font-family: inherit; cursor: pointer;
  text-align: left; transition: background 0.12s;
}
.pd-char-row:not(:disabled):hover { background: #ffffff07; }
.pd-char-row:disabled { cursor: default; }
.pd-char-name {
  font-size: 0.82rem; font-weight: 700; color: var(--text-bright); flex-shrink: 0;
}
.pd-char-meta {
  font-size: 0.7rem; color: var(--text-muted); letter-spacing: 0.03em;
  flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pd-char-badge {
  margin-left: auto; flex-shrink: 0;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; padding: 0.12rem 0.4rem; border-radius: 99px;
  background: rgba(255, 200, 50, 0.12); color: var(--gold-hi);
  border: 1px solid rgba(255, 200, 50, 0.35);
}
.pd-char-active .pd-char-name { color: var(--gold-hi); }
.pd-char-create {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.42rem 1rem;
  font-size: 0.78rem; font-weight: 600; letter-spacing: 0.04em;
  color: var(--accent-hi); text-decoration: none;
  transition: background 0.12s, color 0.12s;
}
.pd-char-create:hover { background: #ffffff07; color: var(--text-bright); }
.pd-char-create i { color: var(--text-muted); font-size: 0.7rem; }

/* ── Party section (inside the player dropdown) ──────────────────────────────
   The same roster the Dashboard's PARTY panel shows, compacted into rows so it
   travels with the player onto every page. Markup comes from the shared renderer
   (includes/party_panel_render.php) and is swapped live by party-live.js, so
   these rules have to hold for content that appears and disappears. */
.pd-party { padding: 0.35rem 0; border-top: 1px solid var(--border); }

.pd-party-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 1rem; transition: background 0.12s;
}
.pd-party-row:hover { background: #ffffff07; }
/* Dot is decorative — the state WORD rides in the meta line beside it, so
   colour is never the only signal (same rule as the Dashboard cards). */
.pd-party-dot { width: 6px; height: 6px; flex: 0 0 6px; border-radius: 50%; }
.pd-party-name {
  display: flex; align-items: center; gap: 0.3rem; flex-shrink: 0;
  font-size: 0.8rem; font-weight: 700; color: var(--text-bright);
  max-width: 45%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pd-party-crown { font-size: 0.7rem; color: var(--gold-hi); }
.pd-party-meta {
  flex: 1; min-width: 0; font-size: 0.7rem; letter-spacing: 0.03em; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Leader tools. Kept faint until the row is hovered or one of them takes focus:
   destructive controls inside a list you mostly just read. focus-within is what
   keeps the keyboard path visible, where there is no hover to reveal them. */
.pd-party-ctl { display: flex; gap: 0.2rem; margin-left: auto; flex-shrink: 0; opacity: 0.35; transition: opacity 0.12s; }
.pd-party-row:hover .pd-party-ctl,
.pd-party-row:focus-within .pd-party-ctl { opacity: 1; }
.pd-party-ctl button {
  width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  font-size: 0.7rem; cursor: pointer; padding: 0;
}
.pd-party-ctl button:hover { border-color: var(--gold-hi); color: var(--gold-hi); }
.pd-party-kick:hover { border-color: #c03020 !important; color: #e06050 !important; }

.pd-party-empty { padding: 0.5rem 1rem; font-size: 0.72rem; color: var(--text-muted); }

/* The two footer actions wear the .pd-char-create shape (a quiet full-width
   row) rather than inventing a third button style for the dropdown. */
.pd-party-find, .pd-party-leave {
  display: flex; align-items: center; gap: 0.55rem; width: 100%;
  padding: 0.42rem 1rem; background: none; border: 0; font-family: inherit;
  font-size: 0.76rem; font-weight: 600; letter-spacing: 0.04em;
  text-align: left; cursor: pointer; transition: background 0.12s, color 0.12s;
}
.pd-party-find  { color: var(--accent-hi); }
.pd-party-find:hover  { background: #ffffff07; color: var(--text-bright); }
.pd-party-find i { color: var(--text-muted); font-size: 0.7rem; }
.pd-party-leave { color: var(--text-muted); }
.pd-party-leave:hover { background: #c0302012; color: #e06050; }
.pd-party-leave i { font-size: 0.7rem; }

.pd-actions { padding: 0.3rem 0; border-top: 1px solid var(--border); }
.pd-action  {
  display: flex; align-items: center; gap: 0.65rem; padding: 0.55rem 1rem;
  font-size: 0.8rem; font-weight: 600; letter-spacing: 0.04em;
  color: var(--text); text-decoration: none; transition: background 0.12s, color 0.12s;
}
.pd-action i { color: var(--text-muted); transition: color 0.12s; }
.pd-action:hover { background: #ffffff07; color: var(--text-bright); }
.pd-action:hover i { color: var(--accent); }
.pd-action-danger:hover   { background: #c0302012; color: #e06050; }
.pd-action-danger:hover i { color: #c03020; }
/* The Text Mode toggle is a <button> wearing the .pd-action look — strip the
   native button chrome so it sits flush with the anchor rows above/below it. */
button.pd-action { width: 100%; background: none; border: 0; font-family: inherit; text-align: left; cursor: pointer; }
.pd-textmode-state { margin-left: auto; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-muted); }
button.pd-action[aria-pressed="true"] .pd-textmode-state,
button.pd-action[aria-pressed="true"] i { color: var(--accent); }

/* ─────────────────────────────────────
   PAGE CONTENT
───────────────────────────────────── */
/* Extra bottom padding clears the fixed comm-link FAB (bottom-right, 52px + a
   0.75rem inset ≈ 64px tall) so the last row of page content is never hidden
   under it. Kept in sync across the responsive padding overrides below. */
.page-content { flex: 1; overflow-y: auto; padding: 1rem 1rem 4.5rem; }
.page-content::-webkit-scrollbar { width: 5px; }
.page-content::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 3px; }

/* ── Buttons ── */
.btn {
  display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.58rem 1.4rem;
  font-family: inherit; font-size: 1rem; font-weight: 700;
  /* Letter-spacing trimmed from 0.12em alongside the 0.78rem -> 1rem size
     bump so longer uppercase labels don't push buttons wider or wrap. */
  letter-spacing: 0.06em;
  text-transform: uppercase; cursor: pointer; border-radius: 2px; text-decoration: none;
  transition: all 0.13s; border: none;
}
.btn-primary { background: var(--accent); color: #080a06; border: 1px solid var(--accent-hi); }
.btn-primary:hover { background: var(--accent-hi); box-shadow: 0 0 18px #8da02044; }
.btn-ghost  { background: transparent; color: var(--text); border: 1px solid var(--border-hi); }
.btn-ghost:hover { border-color: var(--text-muted); color: var(--text-bright); }
.btn-sm { padding: 0.35rem 0.9rem; font-size: 0.87rem; }

/* Disabled buttons read as inert — greyed out, no hover, not clickable. */
.btn:disabled,
.btn[disabled] {
  opacity: 0.35; cursor: not-allowed;
  background: var(--bg-card2); color: var(--text-muted);
  border-color: var(--border); box-shadow: none;
  pointer-events: none;
}

/* ── Table scroll wrapper ── */
/* Wrap wide tables so they scroll horizontally on narrow screens instead of
   forcing the whole page to overflow. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ── Grid rows ── */
.card-row { display: grid; gap: 0.9rem; }
.cols-4   { grid-template-columns: repeat(4, 1fr); }
.cols-3   { grid-template-columns: 1.1fr 1.9fr 1.1fr; }
.cols-2   { grid-template-columns: repeat(2, 1fr); }
.cols-1   { grid-template-columns: 1fr; }

/* ── Base card ── */
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 3px; padding: 0.9rem; display: flex; flex-direction: column; gap: 0.7rem; transition: border-color 0.15s; }
.card:hover { border-color: var(--border-hi); }
/* .card's own `display: flex` (an author style) silently overrides the browser's default
   `[hidden] { display: none }` (a UA style — author rules always win regardless of
   specificity), so a `<div class="card ..." hidden>` used as a JS-toggled placeholder
   (e.g. pages/clans/profile.php's #clan-attack-banner) renders as an empty bordered box
   instead of actually being hidden. Restore the real hidden behavior. */
.card[hidden] { display: none; }
.card-title {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--accent); display: flex; align-items: center; justify-content: space-between;
}
.card-title svg { color: var(--text-muted); cursor: pointer; }
.card-title svg:hover { color: var(--text); }

/* ─────────────────────────────────────
   SIDEBAR BACKDROP
───────────────────────────────────── */
.sidebar-backdrop { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.6); z-index: 199; cursor: pointer; }
.sidebar-backdrop.active { display: block; }

/* ─────────────────────────────────────
   RESPONSIVE BREAKPOINTS
───────────────────────────────────── */

@media (min-width: 992px) {
  .menu-btn { display: none; }
  /* Safety net: the stats row is flex:1, so without this it grows to its content
     width and shoves the clock + player menu (and its dropdown) off the right edge
     on narrower desktops. min-width:0 lets it shrink; overflow:hidden clips a stat
     at worst instead of pushing the player menu off-screen. The breakpoint trims
     below handle the clean whole-stat hiding so this rarely has to clip anything. */
  .header-stats { min-width: 0; overflow: hidden; }
}

/* Progressive trim of the stats row so it always fits and the player menu never
   gets pushed off-screen. Tile order inside #header-stats is: 1 Rank (level/power),
   2 Unspent XP, 3 Health, 4 Stamina, 5 Void (class-gated — absent for non-void
   classes, which shifts everything after it up one), 6 Rested XP. Region is now a
   separate sibling (#header-region, see includes/header.php) rather than the row's
   last tile, so it is targeted directly instead of by nth-child position — and
   :last-child reliably means Rested XP regardless of whether Void is present.
   The Rank tile is narrow (two short numbers, no progress bar), so the first
   threshold only drops Region; the new 1360px step also drops Rested XP.
   Unspent XP is counted here even when it is empty — it stays in the DOM and is
   hidden by .is-empty rather than being omitted, precisely so these nth-child
   positions do not shift underneath the player as they earn and spend XP. */
@media (max-width: 1475px) {
  #header-region { display: none; }
}
@media (max-width: 1360px) {
  #header-region { display: none; }
  .header-stats .hstat:last-child { display: none; }
}

@media (max-width: 1200px) {
  .cols-4 { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 991px) {
  .sidebar { position: fixed; left: 0; top: 0; height: 100%; z-index: 200; transform: translateX(-100%); transition: transform 0.28s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
  .sidebar.open { transform: translateX(0); box-shadow: 8px 0 48px rgba(0, 0, 0, 0.8); }
  .sidebar-status { display: none; }
  .sidebar-brand  { padding: 0.8rem 1.1rem; }
  .brand-void     { font-size: 1.7rem; }
  .nav-item       { padding: 0.44rem 1.1rem; }
  .main-wrapper   { width: 100%; }

  /* ── Collapse the stats row into a "Stats" dropdown ──
     The wrapper becomes a flex row (flex:1 keeps the player menu pushed to the
     right edge, like the old full-width row did) holding the toggle pill and the
     Region tile side by side; .header-stats turns into an absolutely-positioned
     panel under the toggle, anchored by the wrapper's position:relative.
     #header-stats (an ID) outranks the .header-stats .hstat:nth-child trim rules
     in the smaller breakpoints, so EVERY stat still shows in the dropdown. */
  .header-stats-wrap { display: flex; align-items: center; gap: 0.5rem; flex: 1; position: relative; }
  .stats-toggle { display: inline-flex; }

  /* Region stays out of the dropdown and rides next to the toggle instead — it's
     the one stat players reach for on the move (it's the Zone Map shortcut), so it
     isn't worth burying a tap deeper. #header-region overrides the 1360px trim
     above by id-specificity + source order (both plain id selectors, this one
     later in the file), and switches from the desktop tile's block .hstat layout
     to inline-flex so it sits beside the toggle instead of wrapping under it. */
  /* min-width:0 overrides the flex item's default auto-min (which floors at the
     content's full width whenever overflow is visible) so the zone-name ellipsis
     below can actually shrink the tile instead of pushing the player card off the
     row — zone names are DB-authored (world_zones.name) and run much longer than
     "Greenhollow" in practice (e.g. "Coolant Mains Sewers"). */
  #header-region {
    display: inline-flex; border-right: 0; padding: 0.3rem 0.6rem; min-width: 0; max-width: 160px;
    background: var(--bg-card2); border: 1px solid var(--border-hi); border-radius: 3px;
  }
  #header-region .hstat-text { min-width: 0; }
  #header-region .hstat-value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Drop the "Travel to zone" sub-line here — it's the third line of a tile that
     now has to fit the fixed-height top bar (--header-h) beside the toggle pill
     rather than the tall, unconstrained dropdown/desktop row it used to sit in. */
  #header-region .hstat-sub { display: none; }

  #header-stats {
    position: absolute; top: calc(100% + 9px); left: 0; z-index: 510;
    flex-direction: column; align-items: stretch; flex: none; gap: 0;
    width: min(290px, calc(100vw - 1.5rem));
    padding: 0.35rem;
    background: var(--bg-card); border: 1px solid var(--border-hi); border-radius: 6px;
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.72), 0 0 0 1px #8da02012;
    opacity: 0; transform: translateY(-6px); pointer-events: none;
    transition: opacity 0.16s ease, transform 0.16s ease;
  }
  #header-stats.open { opacity: 1; transform: none; pointer-events: auto; }

  /* Show every stat, full width, as stacked rows (overrides the inline trim). */
  #header-stats .hstat {
    display: flex !important; width: 100%; padding: 0.55rem 0.6rem;
    border-right: 0; border-bottom: 1px solid var(--border);
  }
  #header-stats .hstat:first-child { padding-left: 0.6rem; }
  #header-stats .hstat:last-child  { border-bottom: 0; }
  #header-stats .xp-bar-track { width: 100%; }
}

@media (max-width: 767px) {
  .page-content { padding: 0.7rem 0.7rem 4.5rem; }
  .top-header   { padding: 0 0.75rem; }
  .server-time-bar { display: none; }
  /* Phones keep the two most-glanced readouts — Rank (level/power) and Health —
     plus Unspent XP, which sits between them and is an ACTION the player needs to
     be able to reach on a phone, not just a readout. Everything else drops so the
     header fits. nth-child selector keeps specificity equal to the ≤991 rule above
     so this reliably wins by source order. */
  .header-stats .hstat:nth-child(n+4) { display: none; }
  /* Phones are tightest of all: hamburger + toggle + Region + the player card all
     share this one row. Drop Region's "REGION" caption so the tile is just an icon
     and the zone name, single line, instead of the two-line desktop-style stack,
     and shrink the ellipsis cap further to guarantee the player card stays on-screen. */
  #header-region { padding: 0.3rem 0.5rem; max-width: 80px; }
  #header-region .hstat-label { display: none; }
  #header-region .hstat-value { font-size: 0.82rem; }
  .cols-3 { grid-template-columns: 1fr; }
  .cols-2 { grid-template-columns: 1fr; }
  .sidebar-brand { padding: 0.65rem 1rem; }
  .brand-void    { font-size: 1.5rem; }
  .nav-item      { padding: 0.36rem 1rem; font-size: 0.93rem; gap: 0.6rem; }
  .btn           { padding: 0.48rem 0.9rem; font-size: 0.93rem; }
}

/* ─────────────────────────────────────
   COLOUR THEMES
   Override only the background / accent variables; utility colours (green-ok,
   gold, red, purple for void shards) are intentionally left unchanged so badges,
   bars, and error states stay legible across all themes.
───────────────────────────────────── */

/* Burgundy — deep wine, blood-red accent */
[data-theme="burgundy"] {
  --bg:         #090507;
  --bg-sidebar: #0c0608;
  --bg-card:    #10070a;
  --bg-card2:   #160a0d;
  --border:     #280e18;
  --border-hi:  #481828;
  --accent:     #b82040;
  --accent-hi:  #d83050;
  --text:       #d0bec2;
  --text-muted: #9c7e82;
  --text-bright:#ede6e8;
  --bg-input:   #0c0608;
}

/* Gray — cold steel, industrial */
[data-theme="gray"] {
  --bg:         #080808;
  --bg-sidebar: #0c0c0c;
  --bg-card:    #101010;
  --bg-card2:   #141414;
  --border:     #1e1e1e;
  --border-hi:  #2e2e2e;
  --accent:     #8898a8;
  --accent-hi:  #a8bac8;
  --text:       #c0c8d0;
  --text-muted: #808890;
  --text-bright:#e0e8f0;
  --bg-input:   #0c0c0c;
}

/* Purple — void energy, cosmic */
[data-theme="purple"] {
  --bg:         #07050f;
  --bg-sidebar: #0a0714;
  --bg-card:    #0e0a1c;
  --bg-card2:   #130d22;
  --border:     #1c1535;
  --border-hi:  #2c2050;
  --accent:     #8050d8;
  --accent-hi:  #a070f8;
  --text:       #bdb8d5;
  --text-muted: #8578a8;
  --text-bright:#e5e0f5;
  --bg-input:   #0a0714;
}

/* Cyberpunk — deep teal night, hot-magenta accent.
   Follows the dark pattern of the other themes: a near-black teal-tinted backdrop
   with the neon teal (#0a9fa7) reserved for borders/highlights and hot magenta
   (#c10577) as the action accent. */
[data-theme="cyberpunk"] {
  --bg:         #02100f;
  --bg-sidebar: #04211f;
  --bg-card:    #052a28;
  --bg-card2:   #073431;
  --border:     #0a504c;
  --border-hi:  #0a9fa7;
  --accent:     #c10577;
  --accent-hi:  #e8208f;
  --text:       #c2dedc;
  --text-muted: #7ba8a4;
  --text-bright:#e6f6f4;
  --bg-input:   #04211f;
}

/* Amber — warm rust/amber salvage-dock glow, matches the game's own art
   palette (docs/pixellab-art-pipeline.md's "warm amber light"). */
[data-theme="amber"] {
  --bg:         #0c0904;
  --bg-sidebar: #100c05;
  --bg-card:    #140f06;
  --bg-card2:   #1a1308;
  --border:     #2c2010;
  --border-hi:  #4a3318;
  --accent:     #d8862c;
  --accent-hi:  #f5a542;
  --text:       #d8c8b0;
  --text-muted: #a08860;
  --text-bright:#f5ecd8;
  --bg-input:   #100c05;
}

/* Frost — icy cyan coolant glow (the "frost-cyan" accent used for cold/
   coolant mobs in the art pipeline), distinct from Cyberpunk's magenta. */
[data-theme="frost"] {
  --bg:         #050a0c;
  --bg-sidebar: #081012;
  --bg-card:    #0a1418;
  --bg-card2:   #0e1a20;
  --border:     #16323c;
  --border-hi:  #1c5468;
  --accent:     #2da8c8;
  --accent-hi:  #4ccdf0;
  --text:       #bcdce4;
  --text-muted: #6a98a4;
  --text-bright:#e0f4fa;
  --bg-input:   #081012;
}

/* Crimson — bright ember red-orange, distinct from Burgundy's deep wine. */
[data-theme="crimson"] {
  --bg:         #0c0504;
  --bg-sidebar: #100706;
  --bg-card:    #160908;
  --bg-card2:   #1c0c0a;
  --border:     #341210;
  --border-hi:  #5c1c18;
  --accent:     #d8302c;
  --accent-hi:  #f8453c;
  --text:       #d8c0bc;
  --text-muted: #a07a76;
  --text-bright:#f5e4e0;
  --bg-input:   #100706;
}

/* Sea — deep sea-green with a soft chartreuse accent. Follows the dark pattern
   of the other themes: a near-black green backdrop with the requested sea green
   (#07553B) reserved as the card/panel tone and lime (#CED46A) as the accent. */
[data-theme="sea"] {
  --bg:         #030f0a;
  --bg-sidebar: #04170f;
  --bg-card:    #07553B;
  --bg-card2:   #086345;
  --border:     #0e7d5a;
  --border-hi:  #14996e;
  --accent:     #CED46A;
  --accent-hi:  #dde388;
  --text:       #cfe4d8;
  --text-muted: #8fb3a1;
  --text-bright:#eef7f0;
  --bg-input:   #04170f;
}

/* Slate — cool blue-gray, distinct from Gray's neutral steel. */
[data-theme="slate"] {
  --bg:         #07090c;
  --bg-sidebar: #0a0d10;
  --bg-card:    #0d1116;
  --bg-card2:   #11161c;
  --border:     #1c2530;
  --border-hi:  #2c3c4e;
  --accent:     #5a7ea0;
  --accent-hi:  #7aa0c8;
  --text:       #c0c8d2;
  --text-muted: #7a8a98;
  --text-bright:#e4eaf0;
  --bg-input:   #0a0d10;
}

.site-copyright {
  text-align: center;
  font-size: 0.7rem;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  padding: 1rem 1rem 0.5rem;
  border-top: 1px solid var(--border);
  margin-top: 2rem;
}

@media (max-width: 368px) {
  .page-content   { padding: 0.5rem 0.5rem 4.5rem; }
  .top-header     { padding: 0 0.55rem; gap: 0.3rem; }
  .header-stats .hstat { padding: 0 0.5rem; }
  .cols-4         { grid-template-columns: 1fr; }
  .sidebar        { width: 200px; }
  .sidebar-brand  { padding: 0.5rem 0.85rem; }
  .brand-void     { font-size: 1.3rem; }
  .brand-frontier { letter-spacing: 0.18em; }
  .brand-divider  { margin: 0.2rem 0; }
  .nav-item       { padding: 0.28rem 0.85rem; font-size: 0.72rem; gap: 0.5rem; }
  .nav-fa-icon    { font-size: 0.72rem; }
  .card           { padding: 0.65rem; gap: 0.5rem; }
  .card-row       { gap: 0.5rem; }
  /* On the narrowest phones, anchor the menu to the VIEWPORT instead of the
     (off-centre, right-aligned) trigger. A near-full-width menu anchored
     right:0 to the trigger hung ~7px off the left screen edge and clipped the
     slot rail. The header is static at top:0, so a fixed menu sits just under
     it with an even 8px gutter on both sides. */
  .player-dropdown {
    position: fixed; top: var(--header-h); left: 8px; right: 8px; width: auto;
    max-height: calc(100vh - var(--header-h) - 16px);
    max-height: calc(100dvh - var(--header-h) - 16px);
  }
  /* The little pointer arrow can't track the trigger once we're viewport-anchored. */
  .player-dropdown::before { display: none; }
}

/* ─────────────────────────────────────
   TOUCH & SMALL-SCREEN ERGONOMICS
───────────────────────────────────── */

/* Inputs must be ≥16px or iOS Safari force-zooms the viewport on focus
   and never zooms back out. */
@media (max-width: 767px) {
  input, select, textarea,
  .form-input, .s-input, .admin-input,
  .buyin-row input[type="number"], .party-search input { font-size: 16px; }
}

/* Bump primary controls to the 44px touch-target minimum on touch devices,
   without affecting mouse/desktop density. */
@media (max-width: 991px) and (pointer: coarse) {
  .menu-btn { width: 44px; height: 44px; }
}

/* ═════════════════════════════════════════════════════════════
   PRE-AUTH SHARED
   Starfield, nebula base, nav bar, page body, fadeIn
═════════════════════════════════════════════════════════════ */

#stars { position: fixed; inset: 0; pointer-events: none; z-index: 0; }

.nebula { position: fixed; border-radius: 50%; filter: blur(85px); pointer-events: none; z-index: 0; }

.page-nav {
  position: relative; z-index: 10;
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 2rem; border-bottom: 1px solid var(--border); background: var(--bg);
}
.nav-brand { font-size: 0.9rem; font-weight: 700; letter-spacing: 0.25em; text-transform: uppercase; text-decoration: none; color: var(--text-muted); }
/* Space the wordmark as VOID FRONTIER (the accent half gets a small leading
   gap) so the brand reads as two words. Set on the shared component so /index,
   /beta-signup and /login all match. */
.nav-brand span { color: var(--accent); margin-left: 0.4em; }
.nav-back,
.nav-link {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.78rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-muted); text-decoration: none; transition: color 0.13s;
}
.nav-back:hover,
.nav-link:hover { color: var(--accent-hi); }

.page-body {
  position: relative; z-index: 2;
  min-height: calc(100vh - 57px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 2.5rem 1.5rem;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ─────────────────────────────────────
   AUTH CARD CHROME (login, signup, admin tools)
───────────────────────────────────── */

.card-header { padding: 1.5rem 1.75rem 1.25rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 1rem; }
.card-label  { font-size: 0.7rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.1rem; }
.card-body   { padding: 1.5rem 1.75rem; }
.card-footer { padding: 1rem 1.75rem; border-top: 1px solid var(--border); font-size: 0.74rem; color: var(--text-muted); text-align: center; }
.card-footer a { color: var(--accent); text-decoration: none; }
.card-footer a:hover { color: var(--accent-hi); }

/* ─────────────────────────────────────
   AUTH FORMS — shared across login, signup, create_character
───────────────────────────────────── */

.alert-error {
  display: flex; align-items: center; gap: 0.55rem; padding: 0.65rem 0.75rem;
  background: #c0302015; border: 1px solid #c0302044; border-radius: 2px; font-size: 0.8rem; color: #e06050;
}

.divider-row {
  display: flex; align-items: center; gap: 0.75rem;
  font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted);
}
.divider-row::before, .divider-row::after { content: ''; flex: 1; height: 1px; background: var(--border); }

.form-row    { display: grid; gap: 1rem; margin-bottom: 1rem; }
.form-row-2  { grid-template-columns: 1fr 1fr; }
.form-group  { display: flex; flex-direction: column; gap: 0.35rem; }
.form-label  { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-muted); }
.form-label .req { color: var(--accent); }
.form-label .optional { color: var(--text-muted); font-weight: 400; text-transform: none; letter-spacing: normal; opacity: 0.7; }

.form-input {
  width: 100%; background: var(--bg-input); border: 1px solid var(--border-hi); border-radius: 2px;
  color: var(--text); padding: 0.62rem 0.75rem; font-family: inherit; font-size: 0.88rem;
  transition: border-color 0.13s, box-shadow 0.13s; appearance: none;
}
.form-input::placeholder { color: var(--border-hi); }
/* The glow was #8da02020 — 12.5% alpha, effectively invisible on the near-black
 * --bg, so a focused input was almost indistinguishable from an unfocused one.
 * Now a solid 2px ring in the themeable accent, which follows the border-radius
 * (an outline would not).
 *
 * SPECIFICITY NOTE: `.form-input:focus` is (0,2,0) and therefore beats the global
 * `:focus-visible` rule at (0,1,0) — the global ring does NOT reach inputs. This
 * box-shadow IS the focus indicator here, for mouse and keyboard alike, so it has
 * to stay visible on its own. Don't lower its alpha assuming the global rule is
 * backing it up. */
.form-input:focus  { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-hi); }
.form-input.error  { border-color: var(--red); box-shadow: 0 0 0 2px #c0302018; }
textarea.form-input { resize: vertical; min-height: 90px; }
select.form-input  { cursor: pointer; }
select.form-input option { background: #111609; }

.input-wrap { position: relative; }
.input-icon {
  position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%);
  color: var(--text-muted); font-size: 0.8rem; pointer-events: none;
}
.input-wrap .form-input { padding-left: 2.2rem; }

.password-wrap { position: relative; }
.toggle-pw {
  position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%);
  background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 0.8rem; transition: color 0.13s;
}
.toggle-pw:hover { color: var(--accent); }

.form-error { font-size: 0.7rem; color: var(--red); display: flex; align-items: center; gap: 0.3rem; }

.submit-btn {
  width: 100%; padding: 0.8rem 2rem; background: var(--accent); color: #080a06;
  border: 1px solid var(--accent-hi); border-radius: 2px; font-family: inherit; font-size: 1rem;
  /* Letter-spacing trimmed from 0.14em alongside the 0.9rem -> 1rem size bump. */
  font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  cursor: pointer; transition: all 0.15s; display: flex; align-items: center; justify-content: center; gap: 0.5rem;
}
.submit-btn:hover  { background: var(--accent-hi); box-shadow: 0 0 20px #aac42444; }
.submit-btn:active { transform: translateY(1px); }

@media (max-width: 560px) {
  .page-nav    { padding: 0.85rem 1rem; }
  .page-body   { padding: 1.5rem 1rem; }
  .card-header { padding: 1.1rem 1.25rem 1rem; }
  .card-body   { padding: 1.25rem; }
  .card-footer { padding: 0.85rem 1.25rem; }
  .form-row-2  { grid-template-columns: 1fr; }
  /* Landing + signup carry an emblem + a right-side link in the nav; tighten the
     wordmark on phones so the brand never crowds the Login/Back link. */
  .pg-landing .nav-brand, .pg-signup .nav-brand { letter-spacing: 0.12em; font-size: 0.82rem; }
}

/* Ultra-narrow phones: collapse the nav's right-side link to its icon so the
   brand + link can never collide (the icon keeps the title tooltip). */
@media (max-width: 340px) {
  .nav-link-label { display: none; }
}

/* ── Party invite overlay — REMOVED ──────────────────────────────────────────
   It was a bespoke overlay rendered into every page by footer.php, with a
   hard-coded title. The invite is a branded vfdialog colorbox now
   (assets/js/party-invites.js), built on demand and titled with the callsign of
   whoever is actually inviting you — the same surface the Junkyard crew muster
   uses, so the two prompts that arrive unannounced look like one system. Styles
   live with that card, in assets/css/vfdialog.css. */

/* ── Glossary term: ICE (Intrusion Countermeasures Electronics) ──
   Any hacking surface wraps the jargon term "ICE" in <abbr class="ice-term">.
   Style it as a defined term — a dotted underline + help cursor — so players
   know it's explained on hover, alongside the spelled-out text next to it. */
abbr.ice-term {
  text-decoration: underline dotted var(--accent-hi, #8fd14f);
  text-underline-offset: 2px;
  cursor: help;
  letter-spacing: 0.02em;
}
