/* ============================================================
   41shots global styles
   ============================================================ */

/* --- Webfonts ---
   Manrope (body) and Oswald (display) are vendored under SIL Open Font
   License 1.1. Source files come from @fontsource-variable/{manrope,oswald}
   and live in /public/fonts. We keep latin + latin-ext only -- enough for
   every western and central-european player name; cyrillic/greek are
   skipped to keep first-paint cheap. Variable-axis files cover every
   weight in one request, so swapping between 400 and 700 needs no extra
   download. License files alongside the WOFF2s (manrope-OFL.txt,
   oswald-OFL.txt). */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-display: swap;
  font-weight: 200 800;
  src: url('/public/fonts/manrope-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-display: swap;
  font-weight: 200 800;
  src: url('/public/fonts/manrope-latin-ext-wght-normal.woff2') format('woff2-variations');
  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: 'Oswald';
  font-style: normal;
  font-display: swap;
  font-weight: 200 700;
  src: url('/public/fonts/oswald-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Oswald';
  font-style: normal;
  font-display: swap;
  font-weight: 200 700;
  src: url('/public/fonts/oswald-latin-ext-wght-normal.woff2') format('woff2-variations');
  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;
}

/* `[hidden]` should always remove the element from layout. Without
   `!important` any class with an explicit `display` (e.g. `display: flex`)
   wins on cascade order and the element stays visible while toggled
   "hidden" from JS. Standard CSS-reset entry, see HTML5 Boilerplate. */
/* stylelint-disable-next-line declaration-no-important */
[hidden] { display: none !important; }

/* --- Icon system --- */
.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  flex-shrink: 0;
}
.icon--sm  { width: 0.85em; height: 0.85em; }
.icon--lg  { width: 1.3em;  height: 1.3em;  }
.icon--xl  { width: 1.6em;  height: 1.6em;  }

/* Hides the inline SVG sprite definition at the top of <body>. Was
   `style="display:none"` until Tier-3 CSP hardening replaced it with a
   class -- strict `style-src 'self'` on /staff/* forbids inline styles. */
.icon-sprite { display: none; }

/* The :root token block is the single source of truth for all colours,
   surface tints, shadows, borders. Hex and rgba(255,255,255,*) /
   rgba(0,0,0,*) are intentionally allowed *here only*; the rest of the
   file must reach for these tokens via var(--...). */
/* stylelint-disable color-no-hex, declaration-property-value-disallowed-list */
:root {
  --bg:          #090909;
  --red:         #e03535;
  --text:        #efefef;
  --text-muted:  #a0a0a0;
  --text-dim:    #686868;

  /* Transparent text colours for content over tinted backgrounds
     (popups, tooltips, overlays). Unlike the solid --text / --text-muted
     tokens, these mix with whatever surface sits below, which is what
     gives leaflet popups and POI panels their lifted look. Migrated
     from a dozen 0.3-0.9 alpha micro-variants. */
  --text-overlay-dim:    rgba(255,255,255,0.35);
  --text-overlay-muted:  rgba(255,255,255,0.6);
  --text-overlay-strong: rgba(255,255,255,0.88);
  --font:        -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Radius scale -- five tokens cover every shape we draw.
     sm   subtle box rounding (stat-bars, heatmap cells, mini-pills)
     md   mid box rounding (search-results, tooltips, list items)
     lg   card rounding (KPI cards, hero blocks, main panels)
     full perfect circle (dots, avatars, round markers)
     pill capsule with semicircle caps (badges, buttons, role-pills) */
  --radius-sm:    4px;
  --radius-md:    8px;
  --radius-lg:   16px;
  --radius-full: 50%;
  --radius-pill: 999px;

  /* Z-index scale -- eight named tiers replace nine raw values that
     drifted into similar but slightly different numbers (1000 vs 1001
     and so on). Each tier has a clear job; gaps between tiers leave
     room for future intermediate layers without renumbering.
     The nav sits above the map-control tier so descendants in the
     nav's stacking context (e.g. the user-menu dropdown) can render
     over the map sidebar -- otherwise the dropdown was clipped behind
     it on /map. */
  --z-base:         1;    /* hovered/focused element within local stack */
  --z-overlay:     10;    /* in-component floating control (lightbox nav) */
  --z-popover:     20;    /* small dropdown / search results overlay */
  --z-map-layer: 1000;    /* map sidebars + active markers */
  --z-map-control:1010;   /* map toolbars, legends, map-side controls */
  --z-nav:       1100;    /* sticky nav, tooltip flyouts */
  --z-float:     1500;    /* pinned floating CB-radio window (above nav, below modal) */
  --z-modal:     2000;    /* lightbox, full-page search results */

  /* Sizing scale for marker dots, avatars and icons. One scale covers
     all three because they share the same physical px steps in the
     design; the semantics live in the selector name (.foo__dot vs
     .foo-avatar vs .foo-icon), not in the token. Drift values like
     7px / 9px / 12px collapsed onto the nearest step. */
  --size-xs:     8px;     /* small dots (badge, live, list, chip) */
  --size-sm:    10px;     /* standard map markers, default dots */
  --size-md:    14px;     /* active markers, small avatars, sort icons */
  --size-lg:    16px;     /* selected markers, search-icon, follow */
  --size-xl:    18px;     /* close-icon, small-avatar (hall-of-fame links) */
  --size-2xl:   22px;     /* prominent nav-icon (lightbox) */
  --size-3xl:   28px;     /* small avatars */
  --size-4xl:   32px;     /* circular button (map-tb-chip, econ-sort-dir) */
  --size-5xl:   44px;     /* medium avatars */
  --size-6xl:   64px;     /* hero avatars */

  /* Transition durations -- four-step ladder replacing nine drifted
     values. Most hovers used 0.15s (base); the strays at 0.12 / 0.22 /
     0.25 / 0.3 / 0.4 were tiny inconsistencies that nobody picked on
     purpose. fast = press feedback, base = default hover, slow = larger
     element changes, animate = fade-in / longer movements. */
  --motion-fast:    0.1s;
  --motion-base:   0.15s;
  --motion-slow:    0.2s;
  --motion-animate: 0.35s;
  --nav-h:       58px;

  /* --- Type tokens -- Hybrid scale ---
     Body uses comfortable 15.2px for prose, headlines, cards. Data-dense
     surfaces (tables, sidebars, KPI sub-rows) read --fs-dense which stays
     pinned at 12.5px so list-heavy pages keep their information density.
     Pages migrate from hardcoded font-size values to these tokens one at
     a time. Until that migration completes, hardcodes win. */
  --fs-xs:    0.72rem;   /* 11.5px -- micro labels, tags */
  --fs-sm:    0.82rem;   /* 13.1px -- secondary text */
  --fs-base:  0.95rem;   /* 15.2px -- body, paragraphs, card content */
  --fs-md:    1.10rem;   /* 17.6px -- card titles, form input */
  --fs-lg:    1.35rem;   /* 21.6px -- KPI value, sub-heading */
  --fs-xl:    1.75rem;   /* 28px   -- detail page title */
  --fs-2xl:   2.40rem;   /* 38.4px -- hero / page title */
  --fs-dense: 0.78rem;   /* 12.5px -- tables, sidebars, dense data UIs */

  --fw-regular: 400;
  --fw-medium:  600;
  --fw-bold:    700;
  --fw-hero:    800;

  --lh-tight: 1.18;
  --lh-body:  1.58;

  /* --- Font tokens ---
     --font-body / --font-display split lets pages set headline-vs-body
     family in one place. */
  --font-body:    'Manrope', var(--font);
  --font-display: 'Oswald', 'Impact', 'Arial Narrow', sans-serif;

  /* Page-width tiers (see .page / .page--wide / .map-page below):
     content   = reading-focused pages (Rules, Support, Home, 41PD)
     dashboard = data-focused pages (Statistics, Economy, Housing, Newspaper, Gallery) */
  --max-w-content:   1160px;
  --max-w-dashboard: 1600px;

  /* Role colours -- single source of truth, used by:
       .role-pill, .resident-tile__role-dot, .map-player__role,
       and the residents-page filter buttons (data-filter="...").
     Transparent backgrounds/borders are derived via color-mix so a
     palette change here propagates everywhere automatically. */
  --role-admin:      #FFD23F;
  --role-police:     #1E5BFF;
  --role-enthusiast: #22E6C9;
  --role-none:       #4E9F3D;

  /* Map-marker colours for non-player buildings. The marker background
     stays constant; status info (e.g. housing expiry) is conveyed via
     text colour next to the marker, not through the marker itself. */
  --map-housing: #4E9F3D;
  --map-poi:     #deb204;

  /* Economy map flow direction: the dots carry the cargo colour, the riding
     triangles carry the direction so inbound vs outbound reads at a glance. */
  --econ-flow-in:  #38bdf8;
  --econ-flow-out: #f59e0b;

  /* Active wildfire markers on the map and the matching heat-slider /
     heat-mode controls in the sidebar. Distinct from --status-orange
     (#ff9800) which is an informational tone -- fire has more red so
     it reads as "danger / heat" rather than "warning / pending".
     --fire-extinguished is the cool blue used for already-put-out
     fire markers, so the two states read as opposites at a glance. */
  --fire:              #ff5500;
  --fire-extinguished: #4090ff;

  /* Player-track route visualisation (route-toggle button and matching
     map-legend swatch). Kept distinct from --role-police so an active
     route never looks like a police marker. Material Blue 500. */
  --route: #2196f3;

  /* Staff movement-replay highlight palette: the cycle of colours assigned
     to players an admin selects for comparison (dot ring + trail line +
     list row). Read by staff-replay-app.js at runtime, so CSS and the
     Leaflet polylines stay in sync by construction. Reuses existing map
     hues where a distinct one exists; the magenta and lime are new (no
     other map meaning uses them, which is the point -- an investigation
     highlight must not read as police/fire/route). */
  --replay-sel-1: var(--heat-hot);
  --replay-sel-2: var(--role-enthusiast);
  --replay-sel-3: var(--role-admin);
  --replay-sel-4: #e879f9;
  --replay-sel-5: var(--route);
  --replay-sel-6: #a3e635;

  /* Status colours for KPI labels and tabular state badges. Used by the
     housing page expiry buckets and the "available" highlight. Reusing
     --red for danger keeps the brand-red consistent everywhere.
     --status-muted is a 20% white overlay for inactive/zero states
     (e.g. a town with no payment bonus, a row whose status no longer
     applies). Lives in the status family because that is how callers
     reach for it -- data-status="muted" sits alongside red/orange/etc. */
  --status-red:    var(--red);
  --status-orange: #ff9800;
  --status-yellow: var(--map-poi);
  --status-green:  #4caf50;
  --status-muted:  rgba(255, 255, 255, 0.2);

  /* Motor Town skill class colours. Deliberate duplication with
     src/public/skills.js (which exports them for JS-side colour math
     via dimHex()); keep both in sync on retune. CSP forbids inline
     style="--accent:..." so CSS-side consumers reach these via the
     [data-skill="CL_X"] selector family further down in this file.
     Police stays blue on purpose but is NOT identical to --role-police --
     the role pill tints differently on purpose, see
     project_skill_colors memory. */
  --skill-driver:  #A1A1AA;
  --skill-taxi:    #EAB308;
  --skill-bus:     #22C55E;
  --skill-truck:   #C2410C;
  --skill-racer:   #EF4444;
  --skill-wrecker: #D97706;
  --skill-police:  #3B82F6;

  /* Third-party brand colours, used by .badge--* on the home page. Kept
     separate from role/map tokens because they belong to external brands
     and must not drift when our own palette is retuned. */
  --brand-motortown: #deb204;
  --brand-discord:   #5865f2;
  --brand-steam:     #66c0f4;

  /* Movement-track heatmap palette: shared by the map track polyline
     (drawn from JS in map-app.js trackColor()) and the legend bar in
     the sidebar. JS reads these via getComputedStyle so any retune
     here propagates to both the line and the legend automatically. */
  --heat-cold: #3c50b4;
  --heat-cool: #00aad2;
  --heat-mid:  #3cc83c;
  --heat-warm: #f0c800;
  --heat-hot:  #ff5000;

  /* Surface tints and borders for cards, container blocks and overlays.
     Five tokens replace ~280 hardcoded rgba(255,255,255,X) variants that
     had drifted into 0.01-step micro-differences without visible intent.
     surface-1/2/3 climb in elevation (quiet base, raised, active);
     border-soft/strong pair with them. */
  --surface-1:     rgba(255,255,255,0.04);
  --surface-2:     rgba(255,255,255,0.06);
  --surface-3:     rgba(255,255,255,0.10);
  --border-hairline: rgba(255,255,255,0.06);
  --border-soft:     rgba(255,255,255,0.08);
  --border-strong:   rgba(255,255,255,0.14);
  --border-emphasis: rgba(255,255,255,0.25);

  /* Native <select> dropdown caret. appearance:none (form-basics reset below)
     strips the OS arrow, so we paint the site's chevron back via a data-URI
     background-image. The colour is BAKED into the SVG (currentColor does not
     inherit into a background image), so the url lives here in :root where the
     no-hex-outside-:root stylelint rule allows it. Muted, to match the
     nav/map/card carets. */
  --select-caret: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23a0a0a0'%20stroke-width='2.5'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='m6%209%206%206%206-6'/%3E%3C/svg%3E");

  /* Shadow tokens layered by how far the element sits from the page.
     The previous code had ~10 micro-variants (0.22 vs 0.25, 0.4 vs 0.45)
     which were drift, not intent. card sits on the page, popup floats
     above small dropdowns/markers, modal stacks over the whole page.
     highlight-top tiers are the 1px inner-top accent: base is the
     hairline used on quiet surfaces, mid is the standard for cards
     and buttons, strong is the brighter hover state. */
  --shadow-card:                 0 4px 24px rgba(0,0,0,0.22);
  --shadow-popup:                0 2px 10px rgba(0,0,0,0.45);
  --shadow-modal:                0 8px 28px rgba(0,0,0,0.55);
  --shadow-button:               0 1px 3px rgba(0,0,0,0.3);
  --shadow-highlight-top:        inset 0 1px 0 rgba(255,255,255,0.06);
  --shadow-highlight-top-mid:    inset 0 1px 0 rgba(255,255,255,0.12);
  --shadow-highlight-top-strong: inset 0 1px 0 rgba(255,255,255,0.18);

  /* Frosted-glass recipe for anchored floating panels (see the
     "Floating-panel chrome" rule). One token so the blur strength
     cannot drift per panel again (it had wandered 8-24px). */
  --backdrop-panel: blur(20px) saturate(180%);

  /* Dark glass overlay used by popups, tooltips, sidebars, modal
     backdrops and any other floating surface that sits on top of the
     page or the map. One token covers all of them because on the
     normal --bg the difference between 0.85 and 0.96 is invisible;
     on the map both still feel like "frosted glass". The sticky-nav
     and map-search input use a lower-alpha override directly with a
     stylelint-disable next to them -- they need the underlying page
     to bleed through their backdrop-filter, which this token does not. */
  --surface-overlay: rgba(9,9,9,0.92);

  /* Map glow: same visual concept (a soft dark halo) applied via two
     different CSS properties so map markers (box-shadow) and labels
     /direction arrows (text-shadow) keep parity. Bumped from the old
     0.6 / 0.8 alphas because both were too faint on bright satellite
     tiles (beach, asphalt) where the markers used to wash out. */
  --map-glow-box:  0 0 6px rgba(0,0,0,0.85);
  --map-glow-text: 0 0 6px rgba(0,0,0,0.85);

  /* Soft white halo for interactive icons that "light up" on hover/focus
     (e.g. the map-pin / profile icons next to player names). Used as a
     drop-shadow filter value. */
  --glow-white: 0 0 4px rgba(255,255,255,0.7);

  /* Backdrop scrim for modal overlays. Deliberately NOT mixed from
     --surface-*: backdrops carry their own meaning (they darken the
     content beneath instead of presenting content themselves) and must
     not inherit the surface scale. Own token instead of an extra
     stylelint-disable at every modal site. */
  --scrim: rgba(0,0,0,0.6);

  /* Tow distress-call colour. One tone for the whole tow family --
     the specific reason is conveyed by the icon, not by a per-reason
     hue. The --tow-reason-* tokens are kept as aliases so existing
     consumers (REASON_META in assist-app.js, the radio swatches
     in the modal) keep working without a sweep; flip the alias values
     here if you ever want per-reason tones back. */
  --tow-marker:             #E08A3A;
  --tow-reason-stuck:       var(--tow-marker);
  --tow-reason-flipped:     var(--tow-marker);
  --tow-reason-out-of-fuel: var(--tow-marker);

  /* "41 Mobile Fuelers" fuel-station colours: two corp hues (which fuel) for the
     map pins, plus the two fill states the roster surfaces (full = published,
     low = draft/unpublished). */
  --fuel-diesel:    #D4A12C;
  --fuel-gasoline:  #4FA35A;
  --fuel-fill-full: #3FB45A;
  --fuel-fill-low:  #E0A23A;

  /* Community-event type colours. One hue per event type, consumed by the
     [data-event-type] selector family below (calendar chips, cards, create
     radios) -- finite set, so a data-attr family, never inline styles (CSP).
     Distinct enough to tell apart at a glance in the month grid. */
  --event-convoy:  #3B82F6;
  --event-race:    #EF4444;
  --event-carmeet: #A855F7;
  --event-free:    #64748B;
  /* Production builds: construction amber -- distinct from the four above on
     map markers, cards, radios and calendar chips alike. */
  --event-production: #F59E0B;
  /* Generic marker hue for the create-panel preview pin + closure accents. */
  --event-marker:  var(--event-convoy);
}
/* stylelint-enable color-no-hex, declaration-property-value-disallowed-list */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* Anchors the rem unit; every --fs-* token resolves against this base.
   scrollbar-gutter reserves the vertical scrollbar's space even on
   pages that fit the viewport without scrolling -- without it, the
   nav and centred content jump sideways by ~8px when switching from
   a long page (scrollbar present) to a short page (scrollbar gone). */
/* Root rem base. Lowered 16px -> 15px to shrink every rem-based size
   (all --fs-* tokens, spacing, sizing) proportionally by ~6% -- the type
   hierarchy is preserved, unlike a per-token px subtraction. The handful of
   px-hardcoded sizes (Leaflet map popups) keep their own scale by design.
   font-size here is the rem anchor, not a type token -- exempt from the
   token allow-list. */
/* stylelint-disable-next-line declaration-property-value-allowed-list */
html { font-size: 15px; scroll-behavior: smooth; scrollbar-gutter: stable; }
html:has(.map-page) { overflow: hidden; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body, var(--font));
  line-height: 1.65;
  min-height: 100vh;
}
a { color: var(--red); text-decoration: none; }
a:hover { color: var(--text); }
img { display: block; max-width: 100%; }
ul { list-style: none; }

/* ============================================================
   Navigation
   ============================================================ */
.nav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  /* Sticky nav lets the page below bleed through so scrolled content
     is faintly visible behind it -- backdrop-filter only works when
     the surface is partially transparent. --surface-overlay (0.92)
     would block too much; the dedicated low-alpha value lives here. */
  /* stylelint-disable-next-line declaration-property-value-disallowed-list */
  background: rgba(9,9,9,0.72);
  border-bottom: 1px solid var(--border-hairline);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
}
.nav__inner {
  max-width: var(--max-w-content);
  margin: 0 auto;
  padding: 0 1.5rem;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
}
.nav__logo {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.nav__logo-img {
  height: 36px;
  width: auto;
  display: block;
  mix-blend-mode: screen;
}
.nav__links {
  display: flex;
  align-items: center;
  gap: 0.1rem;
  flex: 1;
}
/* The Support-us CTA sits at the right end of the link row, next to the account
   area: margin-auto eats the free space the flex:1 above opens up. In the mobile
   drawer it is reset to a normal full-width row (see the drawer block). */
.nav__support { margin-inline-start: auto; }
/* Donation CTA -- a warm green pill so it reads as an invitation, not just a
   nav link. Always green (overrides the red .active), the heart icon gets a
   little breathing room. */
.nav__support .nav__link {
  gap: 0.4rem;
  color: color-mix(in srgb, var(--status-green) 62%, white);
  border-color: color-mix(in srgb, var(--status-green) 32%, transparent);
  background: color-mix(in srgb, var(--status-green) 12%, transparent);
}
.nav__support .nav__link:hover {
  color: color-mix(in srgb, var(--status-green) 80%, white);
  border-color: color-mix(in srgb, var(--status-green) 45%, transparent);
  background: color-mix(in srgb, var(--status-green) 20%, transparent);
}
.nav__link {
  /* inline-flex (not the default inline for <a>) so that the <a> links and the
     group <button> triggers -- which both carry .nav__link -- are the SAME box
     type. A bare <a> and a <button> align differently as flex items (baseline
     vs. content box); making every nav item an inline-flex/center box removes
     that mismatch instead of pixel-tuning the button. */
  display: inline-flex;
  align-items: center;
  padding: 0.36rem 0.8rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  border: 1px solid transparent;
  white-space: nowrap;  /* nav items are atomic -- never wrap a multi-word label */
  transition: color var(--motion-base), background var(--motion-base), border-color var(--motion-base);
}
.nav__link:hover {
  color: var(--text);
  background: var(--surface-2);
}
.nav__link.active {
  color: color-mix(in srgb, var(--red) 55%, white);
  background: color-mix(in srgb, var(--red) 10%, transparent);
  border-color: color-mix(in srgb, var(--red) 22%, transparent);
}

/* Auth slot in the top nav. Sits to the right of .nav__links and shows
   either the user chip (avatar + name + Log-out button) or the
   Sign-in-with-Steam CTA. Sign-in and Log-out share .btn .btn--compact
   so the brand-red CTA palette stays consistent with Gallery/Newspaper/
   Ko-fi. Steam branding lives in the icon, not the colour. */
.nav__auth {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}
/* Mobile menu trigger. Hidden on desktop; revealed + wired to the drawer in
   the <=768px responsive block. 44px square so it is a comfortable tap target
   (it is only ever shown on touch/narrow viewports). */
.nav__burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--motion-base), background var(--motion-base);
}
.nav__burger .icon { width: 1.5rem; height: 1.5rem; }
.nav__burger:hover { color: var(--text); background: var(--surface-2); }
/* Lock page scroll behind the open drawer. JS only sets this class under the
   drawer breakpoint and clears it when crossing back to desktop width. */
body.nav-drawer-open { overflow: hidden; }
.nav__user {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  max-width: 14rem;
  text-decoration: none;
  transition: color var(--motion-base);
}
a.nav__user:hover { color: var(--text); }
.nav__user-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Avatar element shared between img, initial-bubble and logo-fallback.
   28px matches the .btn--compact height so the chip aligns with the
   Log-out button to its right. */
.nav-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--size-3xl);
  height: var(--size-3xl);
  border-radius: var(--radius-full);
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  object-fit: cover;
  flex-shrink: 0;
  overflow: hidden;
}
/* stylelint-disable-next-line declaration-property-value-allowed-list */
.nav-avatar--bubble { font-size: 0.85rem; font-weight: var(--fw-bold); color: var(--text); }
.nav-avatar--logo   { color: var(--text-muted); }
.nav-avatar--logo .icon { width: 1rem; height: 1rem; }

/* User-chip dropdown. The chip itself is a <button> that toggles
   #nav-user-dropdown; nav-auth.js owns the open/close logic. Anchored
   to the right edge of the auth area so the menu does not push the
   logo / links around. z-popover keeps it above page content but
   below modals (~mirrors residents/staff modal layering). */
.nav__user-menu {
  position: relative;
  display: inline-flex;
}
.nav__user--trigger {
  background: transparent;
  border: 0;
  padding: 0.15rem 0.4rem;
  border-radius: var(--radius-md);
  cursor: pointer;
  color: var(--text-muted);
  font: inherit;
  transition: background var(--motion-base), color var(--motion-base);
}
.nav__user--trigger:hover,
.nav__user--trigger[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--text);
}
.nav__user-caret {
  width: 0.85rem;
  height: 0.85rem;
  color: var(--text-muted);
  transition: transform var(--motion-base);
}
.nav__user--trigger[aria-expanded="true"] .nav__user-caret,
.nav__group-trigger[aria-expanded="true"] .nav__user-caret {
  transform: rotate(180deg);
}
/* The user dropdown and the topic-group dropdowns share one frosted surface
   (same panel, different anchor). Listing both keeps the glass look in one
   place -- no second copy of the surface values to drift out of sync. */
.nav__user-dropdown,
.nav__group-dropdown {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  min-width: 11rem;
  list-style: none;
  margin: 0;
  padding: 0.3rem;
  /* Chrome from the shared floating-panel rule: --surface-overlay plus
     the backdrop blur keep the dropdown readable when the page content
     behind it is busy (notably the map). */
  z-index: var(--z-popover);
}
.nav__user-dropdown[hidden],
.nav__group-dropdown[hidden] { display: none; }
.nav__user-dropdown-item {
  display: block;
  width: 100%;
  padding: 0.5rem 0.7rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--motion-base);
}
.nav__user-dropdown-item:hover,
.nav__user-dropdown-item:focus-visible {
  background: var(--surface-3, var(--surface-1));
  outline: none;
}
/* Active sub-page in a group dropdown -- same tinted-red treatment as
   .nav__link.active so "where am I" reads identically inside the menu. */
.nav__user-dropdown-item.active {
  color: color-mix(in srgb, var(--red) 55%, white);
  background: color-mix(in srgb, var(--red) 10%, transparent);
}

/* "More" dropdown thematic grouping (base.eta): a muted caption above each
   block and a hairline rule between blocks. Both are purely visual; the entries
   stay standard .nav__user-dropdown-item links. */
.nav__group-label {
  padding: 0.45rem 0.7rem 0.2rem;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.nav__group-sep {
  height: 1px;
  margin: 0.3rem 0.4rem;
  background: var(--border-hairline);
}

/* Members-only entry shown to non-members: present but inert (recruiting cue).
   Muted with a lock icon; it is a <span>, so it is not focusable or clickable --
   the real access gate is server-side. */
.nav__user-dropdown-item--locked {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  color: var(--text-muted);
  cursor: not-allowed;
}
.nav__user-dropdown-item--locked:hover {
  background: transparent;
}
.nav__lock {
  flex-shrink: 0;
  opacity: 0.7;
}

/* Topic-group dropdowns (Community / Insights / Garage). The group is a
   relative wrapper like .nav__user-menu, but it sits mid-nav, so the panel
   anchors to its LEFT edge instead of the right (see .nav__group-dropdown).
   The trigger reuses .nav__link for the pill look + .active red; what a
   <button> still needs on top of that is on .nav__group-trigger below. */
.nav__group {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.nav__group-dropdown {
  left: 0;
  right: auto;
}
.nav__group-trigger {
  /* Layout (inline-flex/center) comes from .nav__link, which the button also
     carries. Type does NOT carry over the way it does for a sibling <a>: a
     <button> has the UA `font` shorthand (own family/size/weight), so the type
     is re-stated here to match .nav__link exactly. Plus the button chrome is
     cleared and a small gap separates the label from the caret. */
  gap: 0.3rem;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
}
.nav__group-trigger[aria-expanded="true"] {
  color: var(--text);
  background: var(--surface-2);
}

/* ============================================================
   CB-radio: site-wide live-activity panel (nav trigger)
   ============================================================
   Mirrors the user-menu disclosure (.nav__user-menu + dropdown): a
   relative wrapper, a pill trigger, and an absolutely-positioned panel
   on the same frosted --surface-overlay + blur so it stays readable
   over the map (a plain --surface-* tint reads transparent on satellite
   tiles -- see the user-dropdown note above). Phase 2: trigger + empty
   scaffold; the feed list, filter tabs, unread badge and pin-to-float
   "CB radio" mode arrive in later phases. */
.cb-radio {
  position: relative;
  display: inline-flex;
}
/* The nav quick-widgets (CB radio, rate star) size their trigger icons up
   so they hold their own next to the logo, the support pill and the
   avatar -- the 1em default reads like body text there. */
.cb-radio__trigger > .icon,
.rate-nav__trigger > .icon {
  width: 1.4rem;
  height: 1.4rem;
}

/* Transmit row inside the CB panel: "a CB radio receives AND sends" --
   quick-create links into the Dispatch Center, one icon-btn per service
   (shared icon-button standard). Label chrome from the micro-label family. */
.cb-radio__call {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.45rem 0.8rem;
  border-bottom: 1px solid var(--border-hairline);
}
.cb-radio__call-label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.cb-radio__call-actions {
  display: inline-flex;
  gap: 0.35rem;
}

.cb-radio__trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0.5rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font: inherit;
  cursor: pointer;
  transition: background var(--motion-base), color var(--motion-base);
}
.cb-radio__trigger:hover,
.cb-radio__trigger[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--text);
}
.cb-radio__panel {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  width: 21rem;
  max-width: calc(100vw - 2rem);
  z-index: var(--z-popover);
}
.cb-radio__panel[hidden] { display: none; }
.cb-radio__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--border-hairline);
}
.cb-radio__title {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0;
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--text);
}
.cb-radio__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.2rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--motion-base), color var(--motion-base);
}
.cb-radio__close:hover {
  background: var(--surface-2);
  color: var(--text);
}
.cb-radio__body {
  max-height: 26rem;
  padding: 0.8rem;
  overflow-y: auto;
}
/* Unread-count badge on the trigger. Small red pill; hidden at zero (the
   `hidden` attribute) so it never shows an empty circle. */
.cb-radio__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.3rem;
  border-radius: var(--radius-pill);
  background: var(--red);
  color: var(--text);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: 1;
}
.cb-radio__badge[hidden] { display: none; }
/* Filter tabs sit between the header and the scrolling list; the group keeps
   its own bottom divider so the list reads as a separate region. */
.cb-radio__filters {
  margin: 0.6rem 0.8rem;
}
/* The feed reuses the garage .stream-event row. In the narrow 21rem panel the
   garage's 11ch fixed time column is too wide, so let the row wrap and show a
   compact time instead of crowding the body text. */
.cb-radio__body .stream-event {
  flex-wrap: wrap;
  padding: 0.5rem 0.2rem;
}
.cb-radio__body .stream-event__time {
  min-width: 0;
  flex-basis: 100%;
  order: 3;
  margin-left: 1.7rem;
}
/* In the wrapping panel row the jump-links sit on the first line after the body
   (order:1), before the dismiss X (order:2) and the wrapped time (order:3). */
.cb-radio__body .stream-event__links { order: 1; }

/* Per-row dismiss (X). CB-radio only -- the shared stream-event renderer never
   emits this, so the garage histories stay unaffected. order:2 parks it at the
   right end of the first row (before the wrapped time at order:3). Muted by
   default; brightens on row hover / button focus so it stays discoverable
   without cluttering the ambient feed. */
.cb-radio__dismiss {
  order: 2;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.1rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--motion-fast) ease, color var(--motion-fast) ease;
}
.cb-radio__body .stream-event:hover .cb-radio__dismiss,
.cb-radio__dismiss:focus-visible {
  opacity: 1;
}
.cb-radio__dismiss:hover,
.cb-radio__dismiss:focus-visible {
  color: var(--text);
}
.cb-radio__dismiss .icon {
  width: 0.9rem;
  height: 0.9rem;
}

/* --- Phase 4: pin -> floating window --------------------------------------
   The pin button detaches the panel from the nav into a free, fixed-position
   window that survives navigation; the dock buttons (non-drag alternative,
   WCAG 2.2) snap it to a screen corner; the header is a drag handle. JS sets
   left/top in px via the CSSOM (CSP-safe, like applyBarWidths). */
.cb-radio__tools {
  display: inline-flex;
  align-items: center;
  gap: 0.1rem;
}
.cb-radio__pin,
.cb-radio__home,
.cb-radio__popout,
.cb-radio__dock-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.2rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--motion-base), color var(--motion-base);
}
.cb-radio__pin:hover,
.cb-radio__home:hover,
.cb-radio__popout:hover,
.cb-radio__dock-btn:hover {
  background: var(--surface-2);
  color: var(--text);
}

/* The home (reset-to-default) button exists only in float mode, like the dock. */
.cb-radio__home { display: none; }
.cb-radio__panel--float .cb-radio__home { display: inline-flex; }
/* Pressed pin = panel is floating; brighten it so the state reads at a glance. */
.cb-radio__pin[aria-pressed="true"] {
  background: var(--surface-2);
  color: var(--text);
}

/* Dock buttons exist only in float mode: a 2x2 grid of one corner glyph,
   rotated to point at each corner. */
.cb-radio__dock { display: none; }
.cb-radio__panel--float .cb-radio__dock {
  display: grid;
  grid-template-columns: repeat(2, auto);
  gap: 0.05rem;
  margin-right: 0.15rem;
}
.cb-radio__dock-btn { padding: 0.15rem; }
.cb-radio__dock-btn .icon { width: 0.85rem; height: 0.85rem; }
.cb-radio__dock-btn--tr .icon { transform: rotate(90deg); }
.cb-radio__dock-btn--br .icon { transform: rotate(180deg); }
.cb-radio__dock-btn--bl .icon { transform: rotate(270deg); }

/* Floating mode. position:fixed + a z-index above the nav; JS overrides
   left/top (the values here are only a pre-JS fallback). */
.cb-radio__panel--float {
  position: fixed;
  top: 4rem;
  left: 1rem;
  right: auto;
  bottom: auto;
  z-index: var(--z-float);
  width: 22rem;
}
.cb-radio__panel--float .cb-radio__head {
  cursor: grab;
  touch-action: none;   /* let the drag own the gesture instead of scrolling */
  user-select: none;
}
.cb-radio__panel--float.is-dragging { user-select: none; }
.cb-radio__panel--float.is-dragging .cb-radio__head { cursor: grabbing; }

/* Phone: the CB panel becomes a fixed near-full-width top overlay. The anchored
   dropdown is pinned to the trigger (right:0), which sits mid-nav, so a 21rem
   panel would otherwise run off the LEFT edge; the pinned/float window would
   likewise be a fiddly finger-drag. Both modes share one overlay here -- the
   float base is already position:fixed, the anchored base is absolute, so we set
   fixed for both. JS clears the float's inline left/top and skips the drag
   handlers below this width (memory: "Handy: Overlay ohne Drag"). */
@media (max-width: 40rem) {
  .cb-radio__panel {
    position: fixed;
    top: 3.75rem;
    left: 0.6rem;
    right: 0.6rem;
    width: auto;
    max-width: none;
  }
  .cb-radio__panel--float .cb-radio__head {
    cursor: auto;
    touch-action: auto;
  }
}

/* Respect reduced-motion: no slide when the floating window is repositioned
   (it never animates position anyway, but kill any inherited transition). */
@media (prefers-reduced-motion: reduce) {
  .cb-radio__panel--float { transition: none; }
}

/* --- Phase 7: pop-out into its own window (Document Picture-in-Picture) -----
   When popped out, the panel IS the whole window: it fills it, drops the
   dropdown chrome (border/shadow/radius), and the body grows to scroll. The
   pin/dock/home/pop-out controls make no sense in a dedicated window, so they
   hide -- only the title + close (= bring it back) remain. */
.cb-radio__panel--popout {
  position: static;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100vh;
  max-width: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.cb-radio__panel--popout .cb-radio__body {
  max-height: none;
  flex: 1 1 auto;
}
.cb-radio__panel--popout .cb-radio__home,
.cb-radio__panel--popout .cb-radio__dock,
.cb-radio__panel--popout .cb-radio__pin,
.cb-radio__panel--popout .cb-radio__popout {
  display: none;
}
/* The pop-out window's <body> (class added by JS). style.css is copied into
   that window via its <link>, so these tokens resolve there too. Solid --bg
   because the window has nothing behind it for the panel's blur to pick up. */
.cb-radio-popout-body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
}

/* ============================================================
   Layout helpers
   ============================================================ */
.container {
  max-width: var(--max-w-content);
  margin: 0 auto;
  padding: 0 1.5rem;
}
.main { flex: 1; }

/* ============================================================
   Content-card chrome (single source)
   ============================================================
   Every static content card on the site shares this exact glass
   surface; the per-component blocks below keep ONLY their layout
   (padding, flex, overflow). A NEW content card JOINS this selector
   list instead of redeclaring the chrome -- the music-page review
   (2026-06-13) caught the fifth hand-rolled copy of these five
   declarations, one of which had already drifted. Interactive link
   cards (section-card, gallery items) are a separate family: they
   carry hover-lift behavior and their own chrome. */
.issue-card,
.support-card,
.pd-section,
.rule,
.music-consent {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--shadow-card), var(--shadow-highlight-top);
}

/* ============================================================
   Chip chrome (single source)
   ============================================================
   Neutral interactive pill chips (selectable tabs, filter chips,
   type radios). Same join-the-list rule as the content cards above;
   member blocks keep only their typography and selected states.
   font: inherit matters for the <button>/<label> members (UA font
   shorthand) and is a no-op for the <a> members. NOT in this family:
   .assist-card__btn (an in-card action button -- its surface-2 base is
   deliberate contrast on a surface-1 card, not drift). */
.resident-char-tab,
.resident-property,
.econ-chip,
.event-type-radio {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius-pill);
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--motion-base), border-color var(--motion-base), color var(--motion-base);
}
.resident-char-tab:hover,
.resident-property:hover,
.econ-chip:hover,
.event-type-radio:hover {
  border-color: var(--border-emphasis);
  background: var(--surface-2);
}

/* ============================================================
   Micro-label chrome (single source)
   ============================================================
   The small uppercase labels: form-field captions, table headers,
   count badges next to panel titles, card section labels. One canon
   (fs-xs, bold, 0.08em tracking, muted) -- letter-spacing had
   drifted 0.06-0.09em across the clones and the economy page used
   dim where every sibling used muted. Member blocks keep only
   layout (margins, padding, display) and deliberate colour
   overrides. New labels join this list. */
.map-sidebar__section-head,
.sortable-table thead th,
.kpi-card__label,
.resident-kpi__label,
.resident-properties__label,
.resident-card__sub,
.resident-activity__lbl,
.econ-list-pane__count,
.econ-metric__label,
.econ-col__title,
.econ-jobs__table th,
.econ-filters__label,
.leaderboard-table thead th,
.assist-page__count,
.garage-events__count,
.history-stream__count,
.assist-create-panel__label,
.police-start-panel__label,
.staff-table thead th,
.staff-roster-edit__label,
.cb-radio__call-label,
.garage-police__count,
.prod-kpi__label,
.prod-stage__title,
.prod-event__staff-label,
.prod-demand thead th {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

/* ============================================================
   Floating-panel chrome (single source)
   ============================================================
   Anchored dropdowns and floating panels: nav menus, CB-radio panel,
   map layers/search/legend, search type-aheads, event site picker.
   Frosted glass so the panel stays readable over busy content (map
   tiles especially). The -webkit- twin is load-bearing: older Safari
   applies backdrop-filter only with the prefix. Members keep
   position, sizing and stacking -- z-index varies deliberately
   (popover vs map stacking contexts). */
.nav__user-dropdown,
.nav__group-dropdown,
.cb-radio__panel,
.rate-nav__panel,
.map-layers-dropdown,
.map-search__results,
.map-legend,
.stats-search-results,
.event-site-picker__results {
  background: var(--surface-overlay);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  backdrop-filter: var(--backdrop-panel);
  -webkit-backdrop-filter: var(--backdrop-panel);
  box-shadow: var(--shadow-popup);
}

/* ============================================================
   Rate-nav: site-wide "rate your recent jobs" indicator (logged-in)
   ============================================================
   A nav disclosure that mirrors the CB-radio trigger/badge, shown only while
   the player has pending ratings (rate-nav.js toggles [hidden]). The panel joins
   the floating-panel chrome group above; the cards reuse .assist-rating. */
.rate-nav {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.rate-nav[hidden] { display: none; }
.rate-nav__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.25rem 0.4rem;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--motion-base), background var(--motion-base);
}
.rate-nav__trigger:hover,
.rate-nav__trigger[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--text);
}
.rate-nav__badge {
  position: absolute;
  top: -0.15rem;
  right: -0.15rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.05rem;
  height: 1.05rem;
  padding: 0 0.25rem;
  border-radius: var(--radius-pill);
  background: var(--red);
  color: var(--text);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: 1;
}
.rate-nav__badge[hidden] { display: none; }
.rate-nav__panel {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  width: 22rem;
  max-width: calc(100vw - 2rem);
  z-index: var(--z-popover);
}
.rate-nav__panel[hidden] { display: none; }
.rate-nav__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--border-hairline);
}
.rate-nav__title {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
}
.rate-nav__body {
  padding: 0.8rem;
}

/* ============================================================
   Empty-state text (single source)
   ============================================================
   "Nothing here yet" notices in lists and panels: centered, muted,
   small. The three list-level variants add a dashed placeholder box.
   The canon ends real drift (econ used dim + fs-base, paddings
   wandered, one box was radius-lg + italic). New empty states join
   the list. Inline one-line empties inside dropdowns/sidebars stay
   per-component: they share too little to be this family. */
.history-stream__loading,
.history-stream__empty,
.garage-police__empty,
.garage-events__empty,
.econ-list__empty,
.residents-online-strip__empty,
.garage-leaderboard__empty,
.assist-page__empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}
.residents-online-strip__empty,
.garage-leaderboard__empty,
.assist-page__empty {
  background: var(--surface-1);
  border: 1px dashed var(--border-soft);
  border-radius: var(--radius-md);
}

/* ============================================================
   Hint text (single source)
   ============================================================
   Small muted helper/footnote lines under panels and cards; member
   blocks keep only margins and wrapping. The second group is the
   italic de-emphasis used INSIDE micro-labels (see above): it undoes
   the label's uppercase/tracking for a trailing soft note. */
.music-credit,
.assist-card__note,
.staff-roster__hint,
.staff-events__hint,
.garage-events__login-hint {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.resident-kpi__hint,
.resident-card__sub-note,
.residents-hall__hint,
.resident-skills-invalid__hint {
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  font-style: italic;
  color: var(--text-dim);
  letter-spacing: 0;
  text-transform: none;
}

/* ============================================================
   Tag-pill chrome (single source)
   ============================================================
   Small uppercase status tags (role badges, playstyle tags, the
   housing "available" tag). Colour recipes stay per member -- the
   shape and typography are the family. The tinted satellites with
   their own structure (residents-live-pill with its dot animation,
   staff-roster__flag, residents-block__hint) are deliberately not
   members. */
.role-pill,
.resident-profile-tag,
.resident-company-tag,
.assist-card__service,
.housing-owner__available {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.65rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
}

/* Neutral meta badges in table rows and list headers (rank pill,
   "no badge" sentinel, event status badge, sidebar section count). */
.map-sidebar__section-count,
.staff-table__rank-pill,
.staff-table__sentinel,
.event-card__badge {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

/* ============================================================
   Notice banner chrome (single source)
   ============================================================
   Inline status banners on the garage pages. Tinted variants set
   --accent and share one colour recipe; the neutral members
   (events info, tow login hint) keep their surface colours locally. */
.assist-page__banner,
.garage-leaderboard__error,
.garage-police__banner,
.garage-events__banner,
.assist-page__login-hint {
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius-md);
  font-size: var(--fs-sm);
}
.assist-page__banner,
.garage-leaderboard__error,
.garage-police__banner {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  color: color-mix(in srgb, var(--accent) 35%, white);
}

/* ============================================================
   Form basics
   ============================================================
   Global resets for native form elements so every new <input>,
   <select> or <textarea> picks up the dark-theme styling without
   needing a component-level class. Component classes win on
   specificity (0,1,0 > 0,0,1), so any existing styled control
   keeps its bespoke look. */

select,
textarea,
input[type="text"],
input[type="search"],
input[type="number"],
input[type="email"],
input[type="password"] {
  appearance: none;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
  transition: border-color var(--motion-fast);
}
select {
  cursor: pointer;
  /* Affordance: appearance:none above removed the native arrow. Paint the
     site's caret back (the same chevron the nav/map/card dropdowns use) with a
     hairline divider before it, so a <select> reads as "click to open" like
     every other dropdown instead of a static field. Two layered backgrounds:
     the caret, and a 1px vertical line acting as the separator. */
  background-image: var(--select-caret),
                    linear-gradient(var(--border-strong), var(--border-strong));
  background-repeat: no-repeat, no-repeat;
  background-position: right 0.65rem center, right 1.75rem center;
  background-size: 0.85rem, 1px 55%;
  padding-right: 2.3rem;
}
select:hover { border-color: var(--border-strong); }
select:focus,
textarea:focus,
input[type="text"]:focus,
input[type="search"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="password"]:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--red) 55%, transparent);
}
select option {
  background: var(--surface-overlay);
  color: var(--text);
}

/* ============================================================
   Breadcrumbs
   ============================================================
   Optional secondary navigation rendered above .main when the
   layout receives it.breadcrumbs from the route. */
.breadcrumbs {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0.6rem 1.5rem 0;
  font-size: var(--fs-sm);
}
.breadcrumbs__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.breadcrumbs__item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.breadcrumbs__link {
  color: var(--text-muted);
  text-decoration: none;
  transition: color var(--motion-fast);
}
.breadcrumbs__link:hover { color: var(--text); }
.breadcrumbs__sep {
  color: var(--text-dim);
  flex-shrink: 0;
}
.breadcrumbs__current {
  color: var(--text);
  font-weight: var(--fw-medium);
}

/* ============================================================
   Hero
   ============================================================ */
.hero {
  min-height: 70vh;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  text-align: center;
  padding: 0 1.5rem 3.5rem;
  background:
    linear-gradient(to bottom,
      color-mix(in srgb, var(--bg) 10%, transparent) 0%,
      transparent 25%,
      color-mix(in srgb, var(--bg) 55%, transparent) 60%,
      var(--bg) 100%),
    url('/public/logo-hero.png') center 30% / cover no-repeat;
}
.hero__inner { max-width: 680px; }
.hero__subtitle {
  font-size: var(--fs-md);
  color: var(--text-muted);
  margin-bottom: 2.5rem;
  letter-spacing: 0.01em;
}
.hero__badges {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.65rem;
  flex-wrap: wrap;
}

/* --- Badge --- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 1rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--shadow-highlight-top);
}
.badge--online {
  border-color: color-mix(in srgb, var(--status-green) 28%, transparent);
  background:   color-mix(in srgb, var(--status-green) 10%, transparent);
  color:        color-mix(in srgb, var(--status-green) 60%, white);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--status-green) 10%, transparent),
    0 0 18px       color-mix(in srgb, var(--status-green)  8%, transparent);
}
.badge--offline {
  border-color: color-mix(in srgb, var(--red) 28%, transparent);
  background:   color-mix(in srgb, var(--red) 10%, transparent);
  color:        color-mix(in srgb, var(--red) 60%, white);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--red) 10%, transparent),
    0 0 18px       color-mix(in srgb, var(--red)  8%, transparent);
}
.badge--discord, .badge--motortown, .badge--steam {
  text-decoration: none;
  gap: 0.45rem;
  transition: background var(--motion-base), color var(--motion-base);
}
.badge--discord    { border-color: color-mix(in srgb, var(--brand-discord) 40%, transparent);  background: color-mix(in srgb, var(--brand-discord) 12%, transparent);  color: color-mix(in srgb, var(--brand-discord) 55%, white); }
.badge--motortown  { border-color: color-mix(in srgb, var(--map-poi)        35%, transparent);  background: color-mix(in srgb, var(--map-poi)         8%, transparent);  color: var(--brand-motortown); }
.badge--steam      { border-color: color-mix(in srgb, var(--brand-steam)    30%, transparent);  background: color-mix(in srgb, var(--brand-steam)     8%, transparent);  color: var(--brand-steam); }
.badge--discord:hover   { --accent: var(--brand-discord); background: color-mix(in srgb, var(--brand-discord) 22%, transparent); }
.badge--motortown:hover { background: color-mix(in srgb, var(--map-poi)       18%, transparent); color: color-mix(in srgb, var(--map-poi)       60%, white); }
.badge--steam:hover     { background: color-mix(in srgb, var(--brand-steam)   18%, transparent); color: color-mix(in srgb, var(--brand-steam)   55%, white); }
.badge__dot {
  width: var(--size-xs);
  height: var(--size-xs);
  border-radius: var(--radius-full);
  background: currentColor;
  animation: pulse 2s infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.3; }
}

/* ============================================================
   Section cards (homepage)
   ============================================================ */
/* padding-BLOCK only -- keeps .container's side gutters so the card grid
   ends on the same content edge as the live strip / teasers above (the
   old `padding: 3rem 0 5rem` zeroed the gutters and made this section
   visibly wider than the rest of the page). */
.section-cards {
  padding-block: 3rem 5rem;
}
.section-cards__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 1rem;
}
.section-card {
  display: block;
  padding: 1.6rem 1.75rem;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  box-shadow: var(--shadow-card), var(--shadow-highlight-top-mid);
  transition: background var(--motion-slow), border-color var(--motion-slow), transform var(--motion-slow), box-shadow var(--motion-slow);
  color: var(--text);
}
.section-card:hover {
  background: var(--surface-3);
  border-color: var(--border-emphasis);
  color: var(--text);
  transform: translateY(-3px);
  box-shadow: var(--shadow-modal), var(--shadow-highlight-top-strong);
}
.section-card__label {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--red);
  margin-bottom: 0.55rem;
  opacity: 0.8;
}
.section-card h2 {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  margin-bottom: 0.45rem;
  letter-spacing: 0.01em;
}
.section-card p {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  line-height: var(--lh-body);
}

/* Non-link section-cards drop the hover-lift: --static is the generic
   modifier (home "New faces" panel); .staff-setting predates it and shares
   the one neutralising rule below instead of cloning it. */
.staff-setting {
  margin-top: 1.5rem;
}
.staff-setting:hover,
.section-card--static:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
  transform: none;
  box-shadow: var(--shadow-card), var(--shadow-highlight-top-mid);
}

/* Media variant: the newspaper cover fills the right HALF of the card,
   edge to edge. Half a card is ~the thumbnail's native 280px, so the
   front page stays sharp and recognisable -- full-card would mean 2x+
   upscaling and a blur to mask it, defeating the point of showing it.
   The ::after gradient blends the cover's left edge into the card surface
   so the text column never collides with the artwork. */
.section-card--media {
  position: relative;
  overflow: hidden;
  min-height: 10rem;
}
.section-card__bg {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  /* Image width, the seam gradient below and the body max-width are coupled:
     the cover's left edge (100% - width) must sit just right of the seam so the
     copy never overlaps the image. Tuned for the narrower 3-column home grid. */
  width: 40%;
  object-fit: cover;
  /* Show the masthead: the top of the front page is the recognisable part. */
  object-position: top center;
  transition: transform var(--motion-slow);
  transform-origin: top right;
}
.section-card--media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to right,
    var(--surface-2) 58%,
    transparent 78%);
}
.section-card--media:hover .section-card__bg { transform: scale(1.05); }
.section-card__media-body {
  display: block;
  /* Keep the copy inside the solid part of the seam gradient (left of the cover
     image's edge -- see the coupling note on .section-card__bg). */
  max-width: 56%;
  position: relative;
  z-index: 1;
}

/* ===== Home: live zone (stat strip + map/radio teasers) ================== */

.home-live {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
/* Every strip tile is a link into its section (residents, police/tow
   dispatch, events); keep the stats-card look and add a hover cue. The long
   event title truncates instead of stretching the strip. */
.home-live a.stats-card { color: var(--text); }
.home-live a.stats-card:hover { border-color: var(--border-emphasis); }
.home-live__event-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-live__event-live { color: var(--status-green); }

.home-live__duo {
  /* One height for BOTH duo cards (map teaser + radio). The radio card must
     not size itself from its list content: a full day of calls would grow
     the grid row far past the map (that regression shipped once -- content-
     based flex collapse does not cap a grid row). A shared definite height
     is the single source; the list scrolls inside it. */
  --home-live-h: clamp(280px, 36vw, 420px);
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 1rem;
  align-items: stretch;
}

/* Mini-map teaser: a fixed-height window onto a SQUARE inner stage (the
   2x2 zoom-1 tile grid). Tiles and dots share identical stage geometry --
   width 100%, aspect 1/1, vertically centred -- so the percent-positioned
   dots line up with the world exactly even though the window crops the
   square top and bottom. No Leaflet: the teaser is a picture, /map is the
   app. */
.home-map {
  position: relative;
  display: block;
  overflow: hidden;
  height: var(--home-live-h);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--bg);
}
.home-map__tiles,
.home-map__dots {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  aspect-ratio: 1 / 1;
  transform: translateY(-50%);
}
.home-map__tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  transition: transform var(--motion-slow);
}
.home-map__tiles img {
  width: 100%;
  height: 100%;
  display: block;
}
.home-map:hover .home-map__tiles { transform: translateY(-50%) scale(1.02); }
.home-map__dot {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: var(--radius-full);
  background: var(--red);
  border: 1px solid var(--text);
  box-shadow: var(--map-glow-box);
  transform: translate(-50%, -50%);
}
.home-map__caption {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 1.75rem 1.25rem 1rem;
  background: linear-gradient(to top, color-mix(in srgb, var(--bg) 92%, transparent), transparent);
}
.home-map__title {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--text);
}
.home-map__sub {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.home-map__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.3rem;
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--red);
}

/* CB-radio teaser: header row + the shared stream list (rows rendered by
   stream-events.js, identical to the garage histories / radio panel). */
.home-radio {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
  height: var(--home-live-h);
}
.home-radio__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.home-radio__title {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
}
/* The card height is pinned to --home-live-h (shared with the map teaser);
   the list fills the rest of the card and scrolls inside it. min-height:0
   lets the flex item shrink below its content.
   Both classes in the selector on purpose: the ul also carries
   .history-stream__list, whose `overflow: hidden` (a SHORTHAND, so it sets
   overflow-y too) sits later in this file and would win the cascade at
   equal specificity -- that shipped once as a clipped, unscrollable list. */
.home-radio__list.history-stream__list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--surface-3) transparent;
}

/* ===== Home: fresh zone (newspaper + reputation + new faces) ============ */

.home-fresh__grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: 1rem;
  align-items: stretch;
}
/* Shared compact "name + short meta" player-row list, used by BOTH the New
   faces and Reputation cards (the meta is a joined date or a rank + points). */
.home-faces {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.home-faces__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}
.home-faces__name {
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
a.home-faces__name:hover { color: var(--red); }
.home-faces__joined,
.home-faces__meta {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  white-space: nowrap;
}

@media (max-width: 768px) {
  .home-live__duo,
  .home-fresh__grid { grid-template-columns: 1fr; }
  /* Stacked, both duo cards keep the shared --home-live-h -- the radio card
     needs no special footprint any more. */
}
.staff-setting__toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1rem;
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--text);
}
.staff-setting__toggle input {
  width: 1.1rem;
  height: 1.1rem;
  cursor: pointer;
}

/* Police chat notifications: a master toggle with an indented per-code sublist.
   The sublist dims when the master is off (toggled by staff-app.js) -- the codes
   keep their own value but cannot broadcast while the master gates them. */
.staff-setting__group {
  margin-top: 1rem;
}
.staff-setting__toggle--master {
  font-weight: var(--fw-medium);
}
.staff-setting__sublist {
  display: flex;
  flex-direction: column;
  margin-left: 1.7rem;
  padding-left: 0.85rem;
  border-left: 1px solid var(--border-soft);
}
.staff-setting__sublist--muted {
  opacity: 0.45;
}
.staff-setting__toggle--sub {
  margin-top: 0.55rem;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

/* ============================================================
   Community intro
   ============================================================ */
/* padding-BLOCK only: the horizontal gutters come from .container and must
   survive, so this section shares the exact content edge of every other
   home section (a `padding: 3.5rem 0` shorthand would zero them and make
   the block run wider than its neighbours). Text capped at a readable
   measure; left edges aligning is what makes the page read as one column. */
.community {
  padding-block: 3.5rem;
}
.community__heading {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  margin-bottom: 1.1rem;
  letter-spacing: 0.01em;
}
.community__text {
  max-width: 70ch;
  color: var(--text-muted);
  font-size: var(--fs-base);
  line-height: 1.85;
  margin-bottom: 0.85rem;
}
.community__text:last-child { margin-bottom: 0; }

/* ============================================================
   Divider
   ============================================================ */
/* Home-page section separators. Aligned to the .container content edges
   (border-box: 1160px minus the 1.5rem gutters) instead of full-bleed --
   full-width lines made the constrained sections read as "narrower than
   the page" even though every layout section shares the same container. */
.divider {
  border: none;
  border-top: 1px solid var(--border-hairline);
  width: min(calc(100% - 3rem), calc(var(--max-w-content) - 3rem));
  margin: 0 auto;
}

/* ============================================================
   Footer
   ============================================================ */
.footer {
  padding: 1.5rem 0;
  border-top: 1px solid var(--border-hairline);
  color: var(--text-dim);
  font-size: var(--fs-sm);
}
.footer__inner {
  max-width: var(--max-w-content);
  margin: 0 auto;
  padding: 0 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.footer__logo-img {
  height: 28px;
  width: auto;
  opacity: 0.4;
  mix-blend-mode: screen;
}
.footer__legal {
  max-width: 62ch;
  margin: 0.8rem auto 0;
  padding: 0 1.5rem;
  text-align: center;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--text-dim);
  opacity: 0.55;
}

/* ============================================================
   Page layout (Rules, Police, Support etc.)
   ============================================================ */
.page {
  max-width: var(--max-w-content);
  margin: 0 auto;
  padding: 3rem 1.5rem;
}
.page--wide { max-width: var(--max-w-dashboard); }
.page__header {
  margin-bottom: 2.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border-hairline);
}
.page__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-2xl);
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
}
.page__title span { color: var(--red); }
.page__subtitle { color: var(--text-muted); margin-top: 0.45rem; font-size: var(--fs-base); }

/* ============================================================
   Buttons
   Single call-to-action style shared by Ko-fi, newspaper download
   and the gallery "load more" link. Visually echoes
   .nav__link.active so a click target reads as the same "tinted
   red pill" anywhere on the page; hover deepens the tint. No
   variants needed -- every .btn in the codebase wants the same
   "click me, something happens" affordance.
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.58rem 1.3rem;
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  letter-spacing: 0.01em;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  background:   color-mix(in srgb, var(--red) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--red) 22%, transparent);
  color:        color-mix(in srgb, var(--red) 55%, white);
  transition:
    background   var(--motion-base),
    border-color var(--motion-base),
    color        var(--motion-base),
    transform    var(--motion-fast);
}
.btn:hover {
  background:   color-mix(in srgb, var(--red) 20%, transparent);
  border-color: color-mix(in srgb, var(--red) 45%, transparent);
  color:        color-mix(in srgb, var(--red) 70%, white);
}
.btn:active { transform: scale(0.97); }
.btn[disabled] { opacity: 0.6; cursor: progress; }

/* Compact variant for nav-height contexts (Sign-in / Log-out chip).
   Same brand-red palette inherited from .btn; only padding and font-
   weight tighten so the button sits at the same height as the
   neighbouring .nav-avatar. */
.btn--compact {
  padding: 0.36rem 0.85rem;
  font-weight: var(--fw-medium);
}

/* ============================================================
   Icon button
   ============================================================
   Square, label-less button for a single icon action (open profile,
   jump to map, ...). Shared so icon-only actions look and behave the same
   everywhere instead of each surface inventing its own. ALWAYS pair with an
   aria-label/title -- there is no visible text.

   Default look: no resting box, just the dimmed icon; on hover/focus the
   surface + border fade in and the icon lights up white (var(--glow-white)).
   This keeps dense lists (online strip, map sidebar) quiet at rest while
   still giving a clear hover/tap affordance.

   Modifiers:
     --boxed  surface + border are always visible (for an isolated or
              weightier action that should read as a button at rest). No
              consumer yet -- provided as the standard for future use.
     --lg     larger target + icon (for a primary-ish standalone action,
              e.g. the profile's "show on map").
   Disabled: add .is-disabled (for <a> without href) or [disabled] (for
   <button>) -- greyed, not-allowed, pointer-events off. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  transition: color var(--motion-base), background var(--motion-base), border-color var(--motion-base), filter var(--motion-base);
}
.icon-btn .icon { width: 1.25rem; height: 1.25rem; }
.icon-btn:hover,
.icon-btn:focus-visible {
  color: white;
  background: var(--surface-2);
  border-color: var(--border-soft);
  filter: drop-shadow(var(--glow-white));
}
/* Always-visible box (no consumer yet; see header comment). */
.icon-btn--boxed {
  background: var(--surface-2);
  border-color: var(--border-soft);
  color: var(--text);
}
/* Larger standalone target. */
.icon-btn--lg { width: 2.5rem; height: 2.5rem; }
.icon-btn--lg .icon { width: 1.6rem; height: 1.6rem; }
/* Disabled: <a> drops its href so .is-disabled carries the state; <button>
   uses the native attribute. not-allowed (not the .btn "progress" cursor)
   because the action is unavailable, not in flight. */
.icon-btn.is-disabled,
.icon-btn[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
  color: var(--text-dim);
  background: transparent;
  border-color: transparent;
  filter: none;
}

/* ============================================================
   Newspaper page
   ============================================================ */
.reader {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--nav-h));
}
.reader__bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--border-hairline);
  flex-shrink: 0;
}
.reader__back {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  white-space: nowrap;
}
.reader__back:hover { color: var(--text); }
.reader__title {
  flex: 1;
  font-weight: var(--fw-bold);
  font-size: var(--fs-base);
  text-align: center;
}
.reader__frame {
  flex: 1;
  width: 100%;
  border: none;
  background: var(--surface-1);
}

.issues__heading {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  margin-bottom: 1.25rem;
  letter-spacing: 0.01em;
}
.issues__empty {
  color: var(--text-muted);
  font-size: var(--fs-base);
}
.issue-card {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: transform var(--motion-slow), background var(--motion-slow), border-color var(--motion-slow);
}
.issue-card__cover { transition: transform var(--motion-animate); }

.issues__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1.25rem;
}
.issues__grid > li:first-child {
  border-color: color-mix(in srgb, var(--red) 45%, transparent);
  background: color-mix(in srgb, var(--red) 7%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--red) 20%, transparent), 0 4px 28px color-mix(in srgb, var(--red) 12%, transparent), var(--shadow-highlight-top);
}
.issues__grid > li:first-child .issue-card__edition { color: color-mix(in srgb, var(--red) 50%, white); }

.issues__latest-badge {
  position: absolute;
  top: 0.65rem;
  left: 0.65rem;
  padding: 0.2rem 0.65rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--red) 88%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text);
  box-shadow: var(--shadow-popup);
}
.issue-card:hover {
  border-color: var(--border-emphasis);
  background: var(--surface-2);
  transform: translateY(-3px);
  position: relative;
  z-index: var(--z-base);
}
.issue-card:hover .issue-card__cover {
  transform: scale(1.03);
}
.issue-card__cover-wrap {
  position: relative;
  background: var(--surface-1);
  aspect-ratio: 1 / 1.414;
  overflow: hidden;
  cursor: pointer;
  display: block;
}
.issue-card__placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--text-dim);
}
.issue-card__cover {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
.issue-card__info {
  padding: 0.75rem 1rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.issue-card__edition {
  font-weight: var(--fw-bold);
  font-size: var(--fs-base);
}
.issue-card__date {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
/* ============================================================
   Support page
   ============================================================ */

.support-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1.25rem;
}
.support-card {
  padding: 1.75rem 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.support-card__heading {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
}
.support-card__intro {
  color: var(--text-muted);
  font-size: var(--fs-base);
}
.support-list {
  list-style: disc;
  padding-left: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.support-list li {
  color: var(--text-muted);
  font-size: var(--fs-base);
}
.support-card__note {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  border-top: 1px solid var(--border-hairline);
  padding-top: 0.75rem;
}
.support-card__cta {
  align-self: flex-start;
  margin-top: 0.25rem;
}

/* ============================================================
   Police Department page
   ============================================================ */
.pd-sections {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.pd-section {
  padding: 1.75rem 2rem;
}
.pd-section__heading {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  margin-bottom: 1rem;
  letter-spacing: 0.01em;
}
.pd-list {
  list-style: disc;
  padding-left: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.pd-list li {
  color: var(--text-muted);
  font-size: var(--fs-base);
}
.pd-text {
  color: var(--text-muted);
  font-size: var(--fs-base);
  line-height: 1.8;
}

/* ============================================================
   Rules page
   ============================================================ */
.rules-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.rule {
  padding: 1.75rem 2rem;
}
.rule__header {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: 1rem;
}
.rule__number {
  font-size: var(--fs-xs);
  font-weight: var(--fw-hero);
  letter-spacing: 0.1em;
  color: var(--red);
  white-space: nowrap;
  opacity: 0.75;
}
.rule__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
}
.rule__body p {
  color: var(--text-muted);
  margin-bottom: 0.75rem;
  font-size: var(--fs-base);
}
.rule__body p:last-child { margin-bottom: 0; }
.rule__body strong { color: var(--text); }
.rule__body ul {
  list-style: disc;
  padding-left: 1.25rem;
  margin-bottom: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.rule__body ul li {
  color: var(--text-muted);
  font-size: var(--fs-base);
}
/* Higher selector specificity (.rule__body .rule__note instead of just
   .rule__note) so the note's smaller dimmer text wins against the
   sibling .rule__body p rule above, without needing !important. */
.rule__body .rule__note {
  font-size: var(--fs-sm);
  color: var(--text-dim);
  font-style: italic;
  border-top: 1px solid var(--border-hairline);
  padding-top: 0.75rem;
  margin-top: 0.75rem;
}
.rules-footer {
  margin-top: 2.5rem;
  text-align: center;
  color: var(--text-muted);
  font-size: var(--fs-base);
  padding-bottom: 1rem;
}

/* ============================================================
   Gallery
   ============================================================ */
.gallery__empty {
  color: var(--text-muted);
  font-size: var(--fs-base);
  padding: 2rem 0;
}

.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 0.85rem;
}

.gallery__item {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-lg);
  cursor: pointer;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  box-shadow: var(--shadow-card);
  transition: transform var(--motion-slow), box-shadow var(--motion-slow), border-color var(--motion-slow);
}
.gallery__item:hover {
  transform: translateY(-3px) scale(1.01);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-modal);
}
.gallery__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--motion-animate);
}
.gallery__item:hover img { transform: scale(1.04); }
.gallery__item--hidden   { display: none; }

.gallery__filters { margin-bottom: 1.25rem; }

.gallery__more {
  display: flex;
  justify-content: center;
  padding: 2.5rem 0 1rem;
}
/* The author display:flex above beats the UA [hidden] rule, so spell it out for
   when gallery.js hides the control on a filtered set that fits in one page. */
.gallery__more[hidden] { display: none; }
.gallery__more-count {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  margin-left: 0.25rem;
}

/* --- Lightbox --- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-overlay);
  backdrop-filter: blur(20px) saturate(120%);
  -webkit-backdrop-filter: blur(20px) saturate(120%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-slow);
}
.lightbox--open {
  opacity: 1;
  pointer-events: all;
}

.lightbox__stage {
  max-width: calc(100vw - 140px);
  max-height: calc(100vh - 80px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.lightbox__img {
  max-width: 100%;
  max-height: calc(100vh - 80px);
  object-fit: contain;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  user-select: none;
  display: block;
}

.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 52px;
  height: 52px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--text);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-popup), var(--shadow-highlight-top-mid);
  transition: background var(--motion-base), border-color var(--motion-base), transform var(--motion-base);
  z-index: var(--z-overlay);
}
.lightbox__nav:hover {
  background: var(--surface-3);
  border-color: var(--border-emphasis);
  transform: translateY(-50%) scale(1.08);
}
.lightbox__nav--prev { left: 1.25rem; }
.lightbox__nav--next { right: 1.25rem; }
.lightbox__nav .icon   { width: var(--size-2xl); height: var(--size-2xl); }
.lightbox__close .icon { width: var(--size-xl); height: var(--size-xl); }

.lightbox__close {
  position: absolute;
  top: 1.1rem;
  right: 1.1rem;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--text-muted);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-popup), var(--shadow-highlight-top-mid);
  transition: background var(--motion-base), color var(--motion-base);
  z-index: var(--z-overlay);
}
.lightbox__close:hover { background: var(--surface-3); color: var(--text); }

.lightbox__counter {
  position: absolute;
  bottom: 1.4rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.38rem 1rem;
  border-radius: var(--radius-pill);
  background: var(--surface-overlay);
  border: 1px solid var(--border-soft);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: var(--shadow-popup), var(--shadow-highlight-top);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--text);
  pointer-events: none;
  white-space: nowrap;
}
.lightbox__sep { color: var(--text-muted); margin: 0 0.1rem; }

/* ============================================================
   Music page
   ============================================================
   Two-click consent wrapper for the SoundCloud playlist embed: the
   card below is fully local; music-app.js swaps it for the official
   iframe widget only after the visitor clicks the load button. */
.music-embed,
.music-notice,
.music-credit { max-width: 720px; }

/* Chrome comes from the shared content-card rule (see "Content-card
 * chrome" near the top of this file); only layout lives here. */
.music-consent {
  display: flex;
  align-items: center;
  gap: 1.4rem;
  padding: 1.4rem;
}
.music-consent__cover {
  width: 160px;
  height: 160px;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-hairline);
}
.music-consent__heading {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  margin: 0 0 0.5rem;
}
.music-consent__text {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  margin: 0 0 1rem;
}
.music-consent__text a { color: var(--text); }

.music-embed__iframe {
  display: block;
  width: 100%;
  border: 0;
  border-radius: var(--radius-lg);
}

/* AI-content transparency note: readable (full text colour, not the muted
 * credit grey) so it reads as a disclosure, but small enough not to hamper
 * enjoyment of the page. */
.music-notice {
  margin-top: 1rem;
  font-size: var(--fs-sm);
  color: var(--text);
}

.music-credit {
  margin-top: 0.85rem;
}
.music-credit a { color: var(--text); }

@media (max-width: 480px) {
  .music-consent {
    flex-direction: column;
    text-align: center;
  }
}

/* ============================================================
   Map page
   ============================================================ */
.map-page {
  --map-sidebar-w:           260px;
  --map-sidebar-collapsed-w: 26px;

  position: relative;
  /* dvh (dynamic viewport) so the map's lower edge and bottom controls stay
     above iOS Safari's dynamic bottom toolbar. Plain 100vh measures as if that
     bar were hidden, which pushes the lower edge behind it. */
  height: calc(100dvh - var(--nav-h));
  overflow: hidden;
}

/* --- Toolbar --- */
.map-toolbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 54px;
  z-index: var(--z-map-control);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.75rem;
  pointer-events: none;
}
.map-toolbar > * { pointer-events: auto; }


/* --- Town bonus panel --- */
.map-town-bonuses {
  position: absolute;
  top: 12px; right: calc(var(--map-sidebar-w) + 12px);
  z-index: var(--z-map-control);
  transition: right var(--motion-slow) ease;
  display: flex; flex-direction: column; gap: 4px;
  pointer-events: none;
}
.map-tb-chip {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 0.5rem;
  height: 26px;
  padding: 0 0.6rem 0 0.55rem;
  border-radius: var(--radius-pill);
  background: var(--surface-overlay);
  border: 1px solid var(--border-soft);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: var(--shadow-popup);
  min-width: 150px;
}
.map-tb-chip__fill {
  position: absolute; top: 0; left: 0; bottom: 0;
  transition: width var(--motion-animate) ease;
  opacity: 0.18;
  border-radius: var(--radius-pill);
  background: var(--accent);
  width: var(--fill, 0%);
}
.map-tb-chip__name {
  position: relative; z-index: var(--z-base);
  font-size: var(--fs-xs); font-weight: var(--fw-medium);
  color: var(--text-overlay-muted);
  white-space: nowrap; flex: 1;
}
.map-tb-chip__val {
  position: relative; z-index: var(--z-base);
  font-size: var(--fs-xs); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums; white-space: nowrap;
  color: var(--accent);
}

/* --- Layers bar --- */
.map-layers-bar { display: flex; gap: 0.35rem; flex-shrink: 0; }

/* Group trigger pill -- one per category (POI / Live / Housing).
   Opens a dropdown with the layer checkboxes for that group. */
.map-layers-group { position: relative; }
.map-layers-group__trigger {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
  padding: 0.32rem 0.9rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--map-poi) 8%, var(--surface-overlay));
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  box-shadow: var(--shadow-popup);
  color: var(--text-overlay-muted);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  cursor: pointer; white-space: nowrap;
  transition: background var(--motion-base), color var(--motion-base), border-color var(--motion-base);
}
.map-layers-group__trigger:hover {
  background: color-mix(in srgb, var(--map-poi) 14%, var(--surface-overlay));
}
.map-layers-group__trigger--on {
  background: color-mix(in srgb, var(--map-poi) 18%, var(--surface-overlay));
  border-color: color-mix(in srgb, var(--map-poi) 40%, transparent);
  color: var(--map-poi);
}
.map-layers-group__caret {
  display: inline-flex;
  align-items: center;
  color: var(--text-overlay-muted);
  transition: transform var(--motion-base);
}
.map-layers-group__trigger--on .map-layers-group__caret {
  color: var(--map-poi);
}
.map-layers-group__trigger--open .map-layers-group__caret {
  transform: rotate(180deg);
}

/* Dropdown panel: column of checkbox items. Positioned absolutely
   under the trigger; high z so it floats above the map. */
.map-layers-dropdown {
  position: absolute;
  top: calc(100% + 0.4rem);
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.4rem;
  z-index: var(--z-popover);
  min-width: 11rem;
}

/* Items inside the dropdown: checkbox-style. The item stays the same
   visual shape it had on the flat bar -- only the layout context
   (column inside a panel) changes. */
.map-layers-item {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.32rem 0.7rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-overlay-muted);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  cursor: pointer; white-space: nowrap;
  transition: background var(--motion-base), color var(--motion-base), border-color var(--motion-base);
  text-align: left;
}
.map-layers-item:hover {
  background: color-mix(in srgb, var(--map-poi) 14%, transparent);
  color: var(--text-overlay-muted);
}
.map-layers-item--on  {
  background: color-mix(in srgb, var(--map-poi) 18%, transparent);
  border-color: color-mix(in srgb, var(--map-poi) 40%, transparent);
  color: var(--map-poi);
}
.map-layers-label { flex: 1; }
.map-layers-check { font-size: var(--fs-xs); opacity: 0; transition: opacity var(--motion-base); }
.map-layers-item--on .map-layers-check { opacity: 1; color: var(--map-poi); }

/* --- Search --- */
.map-search {
  position: relative;
  flex-shrink: 0;
}
.map-search__wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.map-search__icon {
  position: absolute;
  left: 0.6rem;
  color: var(--text-muted);
  pointer-events: none;
}
input.map-search__input {
  width: 200px;
  /* Sits over the map tiles, so --surface-1 (a 4% white wash) would
     still let the tiles bleed through. --bg is the only token that
     resolves to a solid colour in the palette. */
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
  padding: 0.32rem 0.75rem 0.32rem 2rem;
  outline: none;
  box-shadow: var(--shadow-popup), var(--shadow-highlight-top);
  transition: border-color var(--motion-base), width var(--motion-slow);
}
input.map-search__input:focus {
  border-color: color-mix(in srgb, var(--red) 50%, transparent);
  width: 240px;
}
input.map-search__input::placeholder { color: var(--text-muted); }

.map-search__results {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 280px;
  overflow: hidden;
  /* Above the map toolbar/controls, unlike the popover-tier siblings. */
  z-index: var(--z-modal);
  max-height: 220px;
  overflow-y: auto;
}
.map-search__item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.85rem;
  cursor: pointer;
  font-size: var(--fs-sm);
  transition: background var(--motion-fast);
}
.map-search__item:hover { background: var(--surface-2); }
.map-search__type {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
  width: 48px;
  flex-shrink: 0;
}
.map-search__type--online  { color: var(--status-green); }
.map-search__type--offline { color: var(--text-muted); }
.map-search__name  { color: var(--text); flex: 1; }
.map-search__empty { padding: 0.7rem 0.85rem; font-size: var(--fs-sm); color: var(--text-muted); }

/* --- Map canvas --- */
.map {
  position: absolute;
  top: 0; left: 0; right: var(--map-sidebar-w); bottom: 0;
  background: var(--bg);
  transition: right var(--motion-slow) ease;
}
.map-page:has(.map-sidebar--collapsed) .map             { right: var(--map-sidebar-collapsed-w); }
.map-page:has(.map-sidebar--collapsed) .map-town-bonuses { right: calc(var(--map-sidebar-collapsed-w) + 12px); }

/* --- Player sidebar --- */
.map-sidebar {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: var(--map-sidebar-w);
  background: var(--surface-overlay);
  border-left: 1px solid var(--border-hairline);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  display: flex; flex-direction: row;
  z-index: var(--z-map-layer);
  transition: transform var(--motion-slow) ease;
}
.map-sidebar--collapsed {
  transform: translateX(calc(100% - var(--map-sidebar-collapsed-w)));
}
.map-sidebar__toggle {
  width: 26px;
  flex-shrink: 0;
  background: none;
  border: none;
  border-right: 1px solid var(--border-hairline);
  color: var(--text-overlay-muted);
  font-size: var(--fs-sm);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.map-sidebar__toggle:hover {
  color: var(--text);
  background: var(--surface-1);
}
.map-sidebar__inner {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.map-sidebar__panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.map-sidebar__list {
  flex: 1; overflow-y: auto;
  padding: 0.35rem 0;
  scrollbar-width: thin;
  scrollbar-color: var(--surface-3) transparent;
}
.map-sidebar__empty {
  padding: 1rem 0.9rem;
  font-size: var(--fs-sm); color: var(--text-dim);
  font-style: italic;
}

/* Sidebar sections: collapsible groups (Tow / Police / Players).
   Header is a button that flips the --collapsed modifier; the body
   hides via display:none when collapsed. State persists via
   localStorage so the user's preference survives reloads. */
.map-sidebar__section + .map-sidebar__section {
  margin-top: 0.5rem;
}
.map-sidebar__section-head {
  display: flex; align-items: center; gap: 0.4rem;
  width: 100%;
  padding: 0.4rem 0.75rem 0.25rem;
  background: transparent;
  border: 0;
  /* family only -- the font shorthand would reset the shared
     micro-label size/weight on this <button> */
  font-family: inherit;
  cursor: pointer;
  text-align: left;
}
.map-sidebar__section-head:hover { color: var(--text); }
.map-sidebar__section-caret {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform var(--motion-base);
}
.map-sidebar__section--collapsed .map-sidebar__section-caret {
  transform: rotate(-90deg);
}
.map-sidebar__section-title { flex: 1; }
.map-sidebar__section--collapsed .map-sidebar__section-body { display: none; }

/* Activity rows: shared shape for Tow + Police entries. Two-line
   layout matches the player rows below so the whole list reads as one
   visual rhythm. */
.map-activity {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0.75rem;
  border-bottom: 1px solid var(--border-hairline);
  transition: background var(--motion-fast);
}
.map-activity--locatable { cursor: pointer; }
.map-activity--locatable:hover { background: var(--surface-1); }
.map-activity__icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  width: 1.5rem; height: 1.5rem;
  border-radius: var(--radius-full);
  color: var(--text);
}
/* Assist rows tint per reason via the shared [data-reason] accent rules --
   the same single source the Dispatch Center cards use, so both surfaces
   stay in step. The amber fallback covers an unknown reason (no rule yet);
   police keeps its role color. */
.map-activity[data-reason] .map-activity__icon { background: var(--accent, var(--tow-marker)); }
.map-activity--police      .map-activity__icon { background: var(--role-police); }
.map-activity__info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.map-activity__name {
  font-size: var(--fs-dense); font-weight: var(--fw-medium);
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.map-activity__detail {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* --- Map bottom controls (heatmap + track bars) --- */
.map-bottom-controls {
  position: absolute;
  bottom: 2.5rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  z-index: var(--z-map-layer);
  pointer-events: none;
}

/* --- Map legend --- */
/* Floating button at the bottom-right corner of the map area (just
   inside the sidebar). Click toggles the legend popover above it.
   Same offset (12px) as the town-bonuses panel keeps the right edge
   visually aligned. */
.map-legend-btn {
  position: absolute;
  right: calc(var(--map-sidebar-w) + 12px);
  bottom: 12px;
  z-index: var(--z-map-control);
  width: var(--size-4xl); height: var(--size-4xl);
  border-radius: var(--radius-full);
  border: 1px solid var(--border-emphasis);
  background: var(--surface-overlay);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text);
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  cursor: pointer;
  transition: right var(--motion-slow) ease, background var(--motion-base), border-color var(--motion-base);
}
.map-legend-btn:hover { background: color-mix(in srgb, var(--surface-3) 50%, var(--surface-overlay)); border-color: var(--border-emphasis); }
.map-legend-btn--open { background: color-mix(in srgb, var(--route) 22%, transparent); border-color: var(--route); color: var(--text); }
.map-page:has(.map-sidebar--collapsed) .map-legend-btn {
  right: calc(var(--map-sidebar-collapsed-w) + 12px);
}

.map-legend {
  position: absolute;
  right: calc(var(--map-sidebar-w) + 12px);
  bottom: 52px;   /* above the legend button (32px + 12px + 8px gap) */
  z-index: var(--z-map-control);
  width: 220px;
  padding: 0.85rem 1rem;
  font-size: var(--fs-xs);
  color: var(--text);
  transition: right var(--motion-slow) ease;
}
.map-page:has(.map-sidebar--collapsed) .map-legend {
  right: calc(var(--map-sidebar-collapsed-w) + 12px);
}
.map-legend__title {
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  margin: 0 0 0.5rem 0;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}
.map-legend__section + .map-legend__section { margin-top: 0.7rem; }
.map-legend__heading {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text-dim);
  margin-bottom: 0.3rem;
}
.map-legend__row {
  display: flex; align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0;
  color: var(--text);
}
.map-legend__swatch {
  display: inline-block;
  width: var(--size-sm); height: var(--size-sm);
  border-radius: var(--radius-full);
  border: 1.5px solid var(--text);
  flex-shrink: 0;
}
.map-legend__swatch--player        { background: var(--red); }
.map-legend__swatch--player-follow { background: var(--red); border-color: color-mix(in srgb, var(--red) 60%, var(--bg)); }
.map-legend__swatch--fire          { background: var(--fire); }
/* Building swatches mirror the marker shape: square instead of round. */
.map-legend__swatch--poi,
.map-legend__swatch--housing {
  border-radius: var(--radius-sm);
}
.map-legend__swatch--poi     { background: var(--map-poi); }
.map-legend__swatch--housing { background: var(--map-housing); }

/* Composite swatch: player dot with a small badge stuck to its
   bottom-right, matching the live marker rendering. */
.map-legend__composite {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--size-sm); height: var(--size-sm);
  flex-shrink: 0;
}
.map-legend__composite .map-legend__swatch { display: block; }
.map-legend__badge {
  position: absolute;
  top: 55%; left: 55%;
  display: inline-flex; align-items: center; justify-content: center;
  width: 12px; height: 12px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--text);
  color: var(--text);
}
.map-legend__badge .icon { width: 8px; height: 8px; }
.map-legend__badge--police { background: var(--role-police); }
.map-legend__badge--tow    { background: var(--tow-marker); }
.map-legend__badge--helper { background: var(--status-green); }
.map-legend__heat-bar {
  height: 8px;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    to right,
    var(--heat-cold) 0%,
    var(--heat-cool) 35%,
    var(--heat-mid)  65%,
    var(--heat-warm) 85%,
    var(--heat-hot)  100%
  );
  margin-bottom: 0.2rem;
}
.map-legend__heat-scale {
  display: flex; justify-content: space-between;
  font-size: var(--fs-xs);
  color: var(--text-dim);
}
/* Map bottom-bar chrome (single source) -- heat slider, route bar and the
   replay player share one look; member blocks below keep only their
   positioning and wrap behaviour. */
.heat-slider-bar,
.track-bar,
.replay-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  padding: 0.45rem 1rem;
  pointer-events: all;
  white-space: nowrap;
}
.heat-slider-bar__title {
  font-size: var(--fs-xs); color: var(--text-dim);
  text-transform: uppercase; letter-spacing: 0.04em;
}
/* The track bar wraps on narrow screens so the route cluster never pushes
   off-canvas; otherwise it stays a slim single line that hugs its content. */
.track-bar { flex-wrap: wrap; }
/* Route cluster (window pills + summary + play), revealed by Show Route. */
.track-bar__route {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
}
.map-window {
  display: inline-flex;
  gap: 2px;
}
/* Media-control colours win over the icon-btn default/hover greys: green
   invites the play (track bar), red marks the stop (replay player bar).
   Same hues as the project status tokens -- no new colour meaning. */
.track-replay-btn,
.track-replay-btn:hover,
.track-replay-btn:focus-visible {
  color: var(--status-green);
}
.track-replay-btn--stop,
.track-replay-btn--stop:hover,
.track-replay-btn--stop:focus-visible {
  color: var(--red);
}

.heat-slider-bar__range { width: 140px; cursor: pointer; accent-color: var(--fire); }

.heat-slider-bar__player,
.track-bar__player {
  font-size: var(--fs-xs);
  color: var(--status-orange);
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Window summary next to the pills: "12.4 km - 38 min - avg 47 km/h". */
.track-bar__stats {
  font-size: var(--fs-xs);
  color: var(--text-overlay-muted);
  white-space: nowrap;
}
.heat-mode-group {
  display: flex;
  gap: 2px;
}
/* Small map chip buttons -- ONE chrome for the three families (heatmap
   mode, sidebar sort, route window); the third sibling triggered the
   shared rule (feedback_pattern_check). Only the --on accent hue stays
   per family: fire = heatmap, selection red = sort, route blue = window. */
.heat-mode-btn,
.map-sort__btn,
.map-window__btn,
.staff-replay__pill {
  padding: 2px 8px;
  font-size: var(--fs-xs);
  background: var(--surface-2);
  border: 1px solid var(--border-emphasis);
  border-radius: var(--radius-sm);
  color: var(--text-overlay-muted);
  cursor: pointer;
  white-space: nowrap;
}
.heat-mode-btn--on {
  background: color-mix(in srgb, var(--fire) 30%, transparent);
  border-color: var(--fire);
  color: var(--text);
}
.map-sort__btn--on {
  background: color-mix(in srgb, var(--red) 25%, transparent);
  border-color: var(--red);
  color: var(--text);
}
.map-window__btn--on {
  background: color-mix(in srgb, var(--route) 30%, transparent);
  border-color: var(--route);
  color: var(--text);
}
/* Staff replay's display toggles (labels): same route-blue accent as the
   window pills -- a view option, not a data filter. */
.staff-replay__pill--on {
  background: color-mix(in srgb, var(--route) 30%, transparent);
  border-color: var(--route);
  color: var(--text);
}
.heat-tip-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.heat-tip-wrap::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  width: 220px;
  padding: 7px 10px;
  background: var(--surface-overlay);
  border: 1px solid var(--border-emphasis);
  border-radius: var(--radius-sm);
  color: var(--text-overlay-strong);
  font-size: var(--fs-xs);
  line-height: 1.45;
  pointer-events: none;
  white-space: normal;
  opacity: 0;
  transition: opacity var(--motion-base);
  z-index: var(--z-nav);
}
.heat-tip-wrap:hover::after {
  opacity: 1;
}
.route-toggle-btn {
  padding: 3px 10px;
  font-size: var(--fs-xs);
  background: var(--surface-2);
  border: 1px solid var(--border-emphasis);
  border-radius: var(--radius-sm);
  color: var(--text-overlay-muted);
  cursor: pointer;
  white-space: nowrap;
}
.route-toggle-btn:hover {
  background: var(--surface-3);
  color: var(--text);
}
.route-toggle-btn--active {
  background: color-mix(in srgb, var(--route) 20%, transparent);
  border-color: var(--route);
  color: var(--route);
}
.route-toggle-btn--active:hover {
  background: color-mix(in srgb, var(--route) 32%, transparent);
}
.resume-follow-btn {
  padding: 3px 10px;
  font-size: var(--fs-xs);
  border-radius: var(--radius-sm);
  cursor: pointer;
  white-space: nowrap;
  background: color-mix(in srgb, var(--status-orange) 20%, transparent);
  border: 1px solid var(--status-orange);
  color: var(--status-orange);
}
.resume-follow-btn:hover {
  background: color-mix(in srgb, var(--status-orange) 35%, transparent);
}
.resume-follow-btn--paused {
  background: color-mix(in srgb, var(--status-green) 20%, transparent);
  border-color: var(--status-green);
  color: var(--status-green);
}
.resume-follow-btn--paused:hover {
  background: color-mix(in srgb, var(--status-green) 35%, transparent);
}
.heat-slider-bar__label {
  font-size: var(--fs-sm);
  color: var(--text);
  min-width: 4rem;
  text-align: right;
}
.track-bar__date {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  white-space: nowrap;
}
.track-bar__hint {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  white-space: nowrap;
}

/* ----- Cinema replay player -----
   Video-player bar while a route replay runs; map-app hides
   .map-bottom-controls for the duration, so the two never stack. Sits at
   the same height as those control bars and spans only the VISIBLE map:
   the right edge tracks the sidebar exactly like .map does (custom-property
   offsets + the :has(--collapsed) flip below, same 12px inset family as the
   town bonuses / legend button). Transport left, timeline centre (flexes to
   all spare width), HUD (position / time of day / vehicle) right. Chrome
   comes from the shared map bottom-bar rule above. */
.replay-bar {
  position: absolute;
  left: 12px;
  right: calc(var(--map-sidebar-w) + 12px);
  bottom: 2.5rem;   /* same height as .map-bottom-controls */
  z-index: var(--z-map-control);
  flex-wrap: wrap;
  transition: right var(--motion-slow) ease;
}
.map-page:has(.map-sidebar--collapsed) .replay-bar {
  right: calc(var(--map-sidebar-collapsed-w) + 12px);
}
.replay-bar__speed {
  /* Right padding leaves room for the global <select> caret + divider; the
     component padding shorthand would otherwise reset it to 4px and the digits
     would run under the caret. */
  padding: 2px 2.1rem 2px 6px;
  font-size: var(--fs-xs);
  min-height: 0;
}
/* Timeline = the seek input plus a marker overlay. The wrapper carries the
   flex sizing; the overlay paints recording gaps and ignores the pointer so
   scrubbing works straight through it. */
.replay-bar__timeline {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 240px;
  min-width: 160px;
}
.replay-bar__seek {
  width: 100%;
  cursor: pointer;
  accent-color: var(--route);
}
.replay-bar__gaps {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* One zone per recording gap (disconnect / play break > 5 min): the stretch
   of timeline with no samples, which the replay fast-forwards over. Position
   and width are stamped per marker via the DOM API (CSP-safe, htmlBar
   pattern); min-width keeps even a short break visible in a 7-day window. */
.replay-bar__gap {
  position: absolute;
  top: 50%;
  height: 8px;
  min-width: 3px;
  transform: translateY(-50%);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--status-orange) 45%, transparent);
}
/* Width reservation for live-changing HUD texts: the visible text and an
   invisible sizer twin share one grid cell, and the sizer holds the widest
   string the replay can show (longest vehicle name, full "total / total"
   duration -- stamped at replay start). The cell takes the max width, so
   updates never re-flow the flexing timeline next to it. tabular-nums keeps
   the digits themselves from wiggling. */
.replay-bar__stack { display: inline-grid; }
.replay-bar__stack > * {
  grid-area: 1 / 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.replay-bar__sizer { visibility: hidden; }

.replay-bar__pos {
  font-size: var(--fs-sm);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* Time of day inside the recording; fixed min-width so the bar does not
   wiggle as digits change. */
.replay-bar__time {
  font-size: var(--fs-sm);
  color: var(--status-orange);
  min-width: 4.5em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.replay-bar__vehicle {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: var(--fs-sm);
  color: var(--text-overlay-muted);
  white-space: nowrap;
  max-width: 220px;
}

/* ----- Mobile map (<=768px) -----
   /map is a full-bleed app surface: toolbar pinned top, player sidebar pinned
   right, control bars centred at the bottom. On phones the 260px sidebar would
   leave almost no map, so the map goes full-width and the sidebar floats over it
   from the right -- the same right-anchored translateX collapse the desktop
   toggle uses, just over the map instead of beside it. Collapsed it shows only a
   thin edge handle; tapping it slides the player list in. map-app.js starts it
   collapsed on phones so the map is fully visible on load. GestureHandling is
   NOT needed here -- .map-page is overflow:hidden, there is no page scroll to
   trap (that only matters for the embedded mini-maps). */
@media (max-width: 768px) {
  .map,
  .map-page:has(.map-sidebar--collapsed) .map { right: 0; }

  /* The map is full-width here (the sidebar floats over it), so the replay
     player follows suit. Both selectors, like .map above -- the bare class
     alone would lose to the :has() rule's specificity. */
  .replay-bar,
  .map-page:has(.map-sidebar--collapsed) .replay-bar { left: 8px; right: 8px; }

  /* Town-bonus panel is hidden on phones (too cramped to be useful there).
     The legend button + popover move to the real right edge. */
  .map-town-bonuses { display: none; }
  .map-legend-btn,
  .map-page:has(.map-sidebar--collapsed) .map-legend-btn,
  .map-legend,
  .map-page:has(.map-sidebar--collapsed) .map-legend { right: 12px; }

  /* A touch more width for the overlaid list. */
  .map-sidebar { width: min(86vw, 320px); }

  /* Compact single-row toolbar so the map still reads as fullscreen. The layer
     pills keep their width (flex-shrink:0 = priority) and the search takes the
     leftover space, shrinking if needed -- both stay visible without the search
     pushing the pills off the row or forcing a tall second row. */
  .map-toolbar { gap: 0.4rem; padding: 0 0.6rem; }
  .map-layers-bar { flex-shrink: 0; }
  .map-search { flex: 1 1 6rem; min-width: 0; }
  input.map-search__input,
  input.map-search__input:focus { width: 100%; }
  .map-search__results { min-width: 0; width: max(220px, 70vw); }
  .map-layers-dropdown { max-width: calc(100vw - 1.5rem); }

  /* Control bars wrap instead of overflowing the viewport. */
  .heat-slider-bar,
  .track-bar {
    flex-wrap: wrap;
    white-space: normal;
    justify-content: center;
    max-width: 100%;
  }
  .heat-slider-bar__range { width: min(140px, 40vw); }
}
.track-arrow {
  /* Leaflet's L.divIcon paints its own background and border on the
     wrapper; the only way to render a transparent SVG glyph is to
     null those out, which requires !important to beat Leaflet's
     bundled CSS specificity. */
  /* stylelint-disable-next-line declaration-no-important */
  background: none !important;
  /* stylelint-disable-next-line declaration-no-important */
  border: none !important;
  /* Glyph for a Leaflet marker; sized in px to stay independent of
     the document type scale (markers must look the same regardless
     of which page they appear on). */
  /* stylelint-disable-next-line declaration-property-value-allowed-list */
  font-size: 16px;
  line-height: 1;
  text-shadow: var(--map-glow-text);
  pointer-events: none;
}
/* Rotation lives on an inner wrapper, not the .track-arrow root: Leaflet
   owns the root element's `transform` (it re-writes translate3d() on every
   pan/zoom), so a rotate() there is silently overwritten. The inner element
   is never touched by Leaflet, so its transform survives. --angle is set on
   the root via DOM-API (no inline style="" -> no CSP 'unsafe-inline') and
   inherits down to this child as a CSS custom property. */
.track-arrow__glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  transform: rotate(var(--angle, 0deg));
  transform-origin: center;
}
/* Players-section sort pills. Button chrome + active state live in the
   shared map-chip rule next to .heat-mode-btn (one chrome, per-family
   accent -- see feedback_pattern_check). */
.map-sort {
  display: flex;
  gap: 2px;
  padding: 0.35rem 0.9rem 0.15rem;
}

.map-player {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.45rem 0.9rem;
  cursor: pointer;
  border-left: 2px solid transparent;   /* placeholder so the layout doesn't shift on selection */
  transition: background var(--motion-base);
}
.map-player:hover { background: var(--surface-1); }
/* Selection follows the project's "active control" pattern (see
   .heat-mode-btn--on and .route-toggle-btn--active): saturated tint on
   the background plus a coloured border in the same hue. The hue
   matches the red player marker on the map. */
.map-player--follow {
  background: color-mix(in srgb, var(--red) 22%, transparent);
  border-left-color: var(--red);
}
.map-player--follow:hover { background: color-mix(in srgb, var(--red) 30%, transparent); }
/* Fixed-width column on the left so player names line up across rows
   regardless of how many roles each player carries. Roles align right
   within the column so the gap between names and roles stays constant
   whether a player has 0 or 3 roles. Width fits three 8px dots (incl.
   their white border) with 3px gaps. The white border echoes the
   player markers on the map so the dots read as "this is a player". */
.map-player__roles {
  display: inline-flex; align-items: center; justify-content: flex-end;
  gap: 3px;
  width: 30px;
  flex-shrink: 0;
}
.map-player__role {
  width: var(--size-xs); height: var(--size-xs);
  border-radius: var(--radius-full);
  border: 1.5px solid var(--text);
  box-sizing: content-box;
  flex-shrink: 0;
}
/* Role colours come from the global tokens (see :root). */
.map-player__role--police     { background: var(--role-police); }
.map-player__role--admin      { background: var(--role-admin); }
.map-player__role--enthusiast { background: var(--role-enthusiast); }
.map-player__role--none       { background: var(--role-none); }
/* On-duty pulse: a Police-role player who is currently driving a police
   vehicle. The expanding ring draws the eye without dominating the row.
   The police hue comes from the role token via color-mix so it stays in
   sync with --role-police. */
.map-player__role--on-duty {
  animation: police-on-duty 1.6s ease-in-out infinite;
}
@keyframes police-on-duty {
  0%, 100% { box-shadow: 0 0 0 0   color-mix(in srgb, var(--role-police) 70%, transparent); }
  50%      { box-shadow: 0 0 0 4px color-mix(in srgb, var(--role-police) 0%,  transparent); }
}
.map-player__info { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.map-player__name {
  font-size: var(--fs-dense); font-weight: var(--fw-medium); color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.map-player__vehicle {
  font-size: var(--fs-xs); color: var(--text-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.map-player__vehicle--idle {
  color: var(--text-muted);
  font-style: italic;
}
.map-player__speed {
  margin-left: auto; flex-shrink: 0;
  font-size: var(--fs-xs); font-weight: var(--fw-bold);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* Open-profile icon at the row's right edge. The row click follows the
   player; this link opens the resident profile (its click is excluded from
   the row handler in map-app.js). margin-left:auto pins it right when there
   is no speed value; with a speed value present, the speed's own auto-margin
   already pushed the group right and this sits just after it. */
.map-player__profile {
  margin-left: auto;
  flex-shrink: 0;
  width: 1.7rem;
  height: 1.7rem;
}
.map-player__profile .icon { width: 1.05rem; height: 1.05rem; }
.map-player__speed + .map-player__profile { margin-left: 0.4rem; }

/* --- Markers --- */
.player-marker,
.poi-marker      { background: transparent; border: none; }

.player-marker__dot {
  display: block;
  width: var(--size-sm);
  height: var(--size-sm);
  border-radius: var(--radius-full);
  background: var(--red);
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
}

.player-marker--follow .player-marker__dot {
  width: var(--size-lg);
  height: var(--size-lg);
  /* Selected marker keeps the regular red fill so the only colour on
     the player layer is red -- the pulse + larger size are what signal
     selection, not a different hue. */
  border-color: color-mix(in srgb, var(--red) 60%, var(--bg));
  animation: follow-pulse 1.6s ease-in-out infinite;
}

@keyframes follow-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 60%, transparent), var(--map-glow-box); }
  50%      { box-shadow: 0 0 0 8px color-mix(in srgb, var(--red) 0%,  transparent), var(--map-glow-box); }
}

/* Badge layer: small icons stuck to the bottom-right of the player
   dot. Renders inline inside the divIcon HTML so badges move with
   the player on every map tick. */
.player-marker__badges {
  position: absolute;
  top: 60%;
  left: 60%;
  display: flex;
  gap: 2px;
  pointer-events: auto;
}
.player-marker__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--text);
  box-shadow: var(--map-glow-box);
  color: var(--text);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: 1;
}
.player-marker__badge .icon { width: 10px; height: 10px; }
.player-marker__badge--police { background: var(--role-police); }
.player-marker__badge--tow,
.player-marker__badge--fuel,
.player-marker__badge--fire,
.player-marker__badge--water,
.player-marker__badge--escort { background: var(--tow-marker); }
.player-marker__badge--helper { background: var(--status-green); }
.player-marker__badge--more   {
  background: var(--surface-2);
  color: var(--text);
}

/* Direction-of-travel chevron: orbits the player dot, rotated to the
   heading via --angle. The angle is stamped through the DOM API on the
   marker's root element after every setIcon (see setPlayerIcon in
   map-app.js) -- same CSP-safe root-element pattern as .track-arrow.
   Pixel sizing on purpose: markers live in Leaflet's px sizing context. */
.player-marker__heading {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 32px;
  height: 32px;
  margin: -16px 0 0 -16px;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  transform: rotate(var(--angle, 0deg));
  pointer-events: none;
}
.player-marker__heading svg {
  width: 10px;
  height: 10px;
  fill: var(--text);
  filter: drop-shadow(var(--map-glow-text));
}

/* POI and housing markers share the same square shape so all "buildings"
   on the map read as the same kind of object; only colour distinguishes
   delivery sites from residential plots. */
.poi-marker__dot {
  display: block;
  width: var(--size-sm);
  height: var(--size-sm);
  border-radius: var(--radius-sm);
  background: var(--map-poi);
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
}

.housing-marker { background: transparent; border: none; }
.housing-marker__dot {
  display: block;
  width: var(--size-sm);
  height: var(--size-sm);
  border-radius: var(--radius-sm);
  background: var(--accent, var(--map-housing));
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
}

/* Leaflet popup widgets keep px font sizes -- they live inside Leaflet's
   own sizing context (popup width, anchor offsets, tip arrows are all px)
   and should not scale with the document type scale. */
/* stylelint-disable declaration-property-value-allowed-list */
.housing-popup { font-size: 12px; min-width: 160px; }
.housing-popup__name { font-weight: var(--fw-medium); margin-bottom: 4px; color: var(--text); }
.housing-popup__row { display: flex; justify-content: space-between; gap: 8px; margin-top: 2px; }
.housing-popup__label { color: var(--text-overlay-muted); }
.housing-popup__value { color: var(--accent, var(--text-overlay-strong)); }
/* stylelint-enable declaration-property-value-allowed-list */

.fire-marker { background: transparent; border: none; }
.fire-marker__dot {
  display: block;
  width: var(--size-md);
  height: var(--size-md);
  border-radius: var(--radius-full);
  background: radial-gradient(circle,
    color-mix(in srgb, var(--map-poi) 55%, transparent) 10%,
    color-mix(in srgb, var(--fire)    45%, transparent) 65%);
  border: 2px solid color-mix(in srgb, var(--fire) 50%, transparent);
  box-shadow: 0 0 6px color-mix(in srgb, var(--fire) 40%, transparent);
}
.fire-marker__dot--ext {
  background: radial-gradient(circle,
    color-mix(in srgb, var(--fire-extinguished) 50%, white)       10%,
    color-mix(in srgb, var(--fire-extinguished) 40%, transparent) 65%);
  border-color: color-mix(in srgb, var(--fire-extinguished) 50%, transparent);
  box-shadow: 0 0 6px color-mix(in srgb, var(--fire-extinguished) 40%, transparent);
}

/* ACTIVE fires pulse softly so "burning right now" reads at a glance;
   extinguished ones stay static (the --ext rule above wins the cascade by
   order and carries no animation). Expanding-ring pattern mirrors
   follow-pulse. Gated on no-preference so reduced-motion users keep the
   static dot. */
@media (prefers-reduced-motion: no-preference) {
  .fire-marker__dot:not(.fire-marker__dot--ext) {
    animation: fire-pulse 1.8s ease-in-out infinite;
  }
}

@keyframes fire-pulse {
  0%, 100% {
    box-shadow:
      0 0 6px color-mix(in srgb, var(--fire) 40%, transparent),
      0 0 0 0 color-mix(in srgb, var(--fire) 45%, transparent);
  }
  50% {
    box-shadow:
      0 0 10px color-mix(in srgb, var(--fire) 60%, transparent),
      0 0 0 7px color-mix(in srgb, var(--fire) 0%, transparent);
  }
}

/* While a replay runs the map tiles lose most of their saturation (old
   newsreel feel) so the scene reads as "watching the past" -- markers,
   the route line and the ghost keep their colours and pop against the
   muted ground. Toggled via .map--replay on the #map element. */
.map--replay .leaflet-tile-pane {
  filter: saturate(0.3) contrast(0.95);
}
/* Scoped to the main map: the embedded mini-maps (housing, events) never
   enter replay mode, so they get no pointless transition hook. */
.map .leaflet-tile-pane {
  transition: filter var(--motion-slow);
}

/* Replay ghost: re-drives the visible route window when the user presses
   Replay in the track bar. Route-blue so it reads as "the route, moving",
   distinct from the orange end-of-track dot and the red live markers. */
.track-replay-marker { background: transparent; border: none; }
.track-replay-marker__dot {
  display: block;
  width: var(--size-md);
  height: var(--size-md);
  border-radius: var(--radius-full);
  background: var(--route);
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
}

/* Tow-fallback marker: stays at the crash position while the
   requester is offline (no player marker to anchor the badge to).
   Police shifts use no fallback -- if the officer disconnects, the
   background job ends the shift after the grace window. */
.assist-layer-marker { background: transparent; border: none; }
.assist-layer-marker__disc {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-full);
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
  color: var(--text);
  background: var(--tow-marker);
}
/* Drop-off variant (taxi destination): inverted disc -- assist-orange glyph
   on a dark fill -- so pickup and drop-off are distinguishable at a glance. */
.assist-layer-marker--dest .assist-layer-marker__disc {
  background: var(--bg);
  border-color: var(--tow-marker);
  color: var(--tow-marker);
}

.assist-layer-popup {
  display: grid;
  gap: 0.2rem;
  font-size: var(--fs-sm);
  color: var(--text-overlay-strong);
}
.assist-layer-popup__title {
  font-weight: var(--fw-medium);
  color: var(--text-overlay-strong);
}
.assist-layer-popup__row {
  display: flex;
  gap: 0.4rem;
}
.assist-layer-popup__label { color: var(--text-overlay-muted); min-width: 5rem; }
.assist-layer-popup__note  { color: var(--text-overlay-muted); font-style: italic; }

/* Dark Leaflet popups + tooltips.
   Leaflet ships its own CSS in leaflet.css with high-specificity rules
   that win against our class selectors. The only reliable way to skin
   .leaflet-popup-close-button, .leaflet-tooltip and friends is with
   !important; without it the upstream values bleed through. The whole
   block is wrapped so each !important does not need its own disable. */
/* stylelint-disable declaration-no-important */

/* Leaflet's default container background is #ddd, which flashes light-
   grey around / beneath the tiles (during pan, while tiles load, or
   if the viewport is briefly larger than the tiled bounds). Override
   with our --bg so the gap blends into the rest of the page. */
.leaflet-container { background: var(--bg); }

.leaflet-popup-content-wrapper {
  background: var(--surface-overlay);
  color: var(--text);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: var(--shadow-modal);
}
.leaflet-popup-tip          { background: var(--surface-overlay); }
.leaflet-popup-close-button { color: var(--text-muted) !important; top: 6px !important; right: 8px !important; }
.leaflet-popup-close-button:hover { color: var(--text) !important; background: transparent !important; }
.leaflet-popup-content      { margin: 0.75rem 1rem; }

.leaflet-tooltip {
  background: var(--surface-overlay) !important;
  color: var(--text) !important;
  border: 1px solid var(--border-strong) !important;
  border-radius: var(--radius-md) !important;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--shadow-modal) !important;
  padding: 0.5rem 0.75rem !important;
}
.leaflet-tooltip::before { display: none !important; }
/* stylelint-enable declaration-no-important */

/* Second line inside a player marker tooltip: the live speed under the
   name/vehicle. Tokens only -- our own element, no upstream conflict, so no
   !important needed (unlike the .leaflet-tooltip overrides above). */
.map-tip-speed {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Leaflet popup -- see note above .housing-popup for why these stay literal. */
/* stylelint-disable declaration-property-value-allowed-list */
.fire-popup__title  { font-weight: var(--fw-bold); color: color-mix(in srgb, var(--status-orange) 55%, white); margin-bottom: 0.25rem; font-size: 0.85rem; }
.fire-popup__row    { font-size: 0.78rem; color: var(--text-muted); line-height: 1.6; }
.fire-popup__status { font-size: 0.78rem; color: var(--text); margin-top: 0.25rem; }
/* stylelint-enable declaration-property-value-allowed-list */

/* ============================================================
   Responsive

   Canonical breakpoints. Keep new/edited rules on these three; CSS custom
   properties do not work inside @media conditions in a no-build setup, so this
   is a documented convention, not a token:
     <= 1024px  large tablets / small laptops -- burger nav + drawer, fewer
                columns, the asymmetric 2-pane layouts stack
     <=  768px  portrait tablet -- map sidebar -> overlay, chart rows stack
     <=  480px  phone -- single column, stacked, tighter gutters
   Touch sizing keys off (pointer: coarse) so it also covers large touch tablets,
   not only narrow screens. Pure column grids prefer repeat(auto-fit, minmax())
   over a breakpoint. The old one-off widths (560/600/680/720/820/880/900/1100/
   1180) were folded into this scale in 2026-06.

   Deliberately off this scale (documented exceptions):
     - cb-radio float widget: 40rem -- its own touch threshold, paired CSS<->JS
       (the @media below + cb-radio-float.js).
     - (pointer: coarse) / (prefers-reduced-motion) capability queries.

   JS mirrors that MUST move together with their breakpoint:
     - 1024 nav drawer  <-> nav-auth.js (min-width: 1025)
     - 768  map sidebar <-> map-app.js  (max-width: 768)
   ============================================================ */
/* Mobile navigation -- burger + off-canvas drawer. Replaces the old 860/680px
   "hide the links" behaviour, which left phones with NO way to navigate. At or
   below 1024px the burger shows and .nav__links becomes a left-side drawer
   toggled by .is-nav-open on .nav (a class, not [hidden], so the slide transform
   works and the links stay visible on desktop). The topic groups turn into
   inline, indented accordion sections -- the existing createDisclosure wiring
   still toggles them, just in static flow instead of as floating panels.
   Breakpoint is 1024 (not 768): the full bar (Map + four group menus + 41PD +
   Support us + logo + user chip) only fits above ~1024px; below that it
   overflowed. Kept in sync with nav-auth.js (min-width: 1025px). */
@media (max-width: 1024px) {
  .nav__burger { display: inline-flex; }

  .nav__links {
    /* Absolute (not fixed) so the drawer hangs from the nav's own bottom edge.
       The nav carries backdrop-filter, which makes a position:fixed descendant
       resolve against the 58px nav box instead of the viewport -- top:nav-h plus
       bottom:0 would then collapse it to zero height. Anchoring to the sticky
       nav with absolute + max-height sidesteps that containing-block trap. */
    position: absolute;
    top: 100%;
    left: 0;
    z-index: var(--z-nav);
    width: min(80vw, 320px);
    max-height: calc(100dvh - var(--nav-h));
    flex-direction: column;
    align-items: stretch;
    gap: 0.15rem;
    padding: 0.6rem;
    overflow-y: auto;
    background: var(--surface-overlay);
    backdrop-filter: blur(24px) saturate(180%);
    -webkit-backdrop-filter: blur(24px) saturate(180%);
    border-right: 1px solid var(--border-soft);
    border-bottom: 1px solid var(--border-soft);
    box-shadow: var(--shadow-popup);
    transform: translateX(-100%);
    transition: transform var(--motion-slow);
  }
  .nav.is-nav-open .nav__links { transform: translateX(0); }

  /* Each row fills the drawer width, left-aligned, with a comfortable tap
     height. Group triggers keep the label left / caret right. */
  .nav__links > li { width: 100%; }
  .nav__support { margin-inline-start: 0; }  /* not pushed right in the stacked drawer */
  .nav__link { width: 100%; justify-content: flex-start; padding: 0.6rem 0.75rem; }
  .nav__group { display: block; position: static; }
  .nav__group-trigger { justify-content: space-between; }

  /* Group dropdowns become static, indented accordion bodies -- no float, no
     separate glass surface (they live inside the drawer's own surface).
     Scoped to the drawer (.nav__links): the call-nav menu in the top bar
     also carries .nav__group-dropdown (for the shared wiring) but must KEEP
     its floating glass chrome -- unscoped, this reset made it transparent
     over the page content. */
  .nav__links .nav__group-dropdown {
    position: static;
    width: 100%;
    min-width: 0;
    padding: 0.1rem 0 0.2rem 0.6rem;
    background: transparent;
    border: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* Foundation -- tighter page gutters on phones. The desktop padding
   (1.5-2rem) wastes scarce width at <=480px where content needs it. */
@media (max-width: 480px) {
  .container     { padding: 0 1rem; }
  .page          { padding: 2rem 1rem; }
  .stats-page    { padding: 1.75rem 1rem 3rem; }
  .footer__inner { padding: 0 1rem; }
  .footer__legal { padding: 0 1rem; }
}

/* Foundation -- touch-friendly hit areas. On touch devices the shared
   action buttons and native form controls grow to the ~44px target the
   platform guidelines ask for (WCAG 2.5.5 / Apple HIG 44pt). Dense in-list
   icon buttons are sized per surface in their own sections, not globalised
   here; range/checkbox inputs are deliberately excluded (their own controls
   already have a usable hit area). */
@media (pointer: coarse) {
  .btn { min-height: 44px; }
  select,
  textarea,
  input[type="text"],
  input[type="search"],
  input[type="number"],
  input[type="email"],
  input[type="password"] { min-height: 44px; }
}

/* ============================================================
   Statistics page
   ============================================================ */

.stats-page { max-width: var(--max-w-dashboard); margin: 0 auto; padding: 2.5rem 2rem 4rem; display: flex; flex-direction: column; gap: 1.5rem; }
/* Narrower variant for table-only pages without charts (e.g. /staff/admins,
   /staff/police-roster). Same vertical rhythm as .stats-page, just a tighter
   content column so 4-column tables stay readable instead of stretched. */
.stats-page--narrow { max-width: var(--max-w-content); }

/* Header */
.stats-header        { display: flex; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
.stats-header__title {
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-xl), 4vw, var(--fs-2xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
  margin: 0;
}
.stats-header__sub   { margin: 0.25rem 0 0; color: var(--text-muted); font-size: var(--fs-base); }
.stats-header__meta  { display: flex; align-items: center; gap: 0.6rem; font-size: var(--fs-sm); color: var(--text-muted); }

.stats-live-dot {
  display: inline-block; width: var(--size-xs); height: var(--size-xs); border-radius: var(--radius-full);
  background: var(--status-green);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-green) 60%, transparent);
  animation: livePulse 2s ease-out infinite;
}
@keyframes livePulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-green) 60%, transparent); }
  70%  { box-shadow: 0 0 0 8px color-mix(in srgb, var(--status-green) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-green) 0%, transparent); }
}
.stats-last-update { color: var(--text); }
.stats-countdown   { color: var(--text-dim); font-size: var(--fs-xs); }
/* Housing KPI strip (card grid).
   auto-fit/minmax scales from 2 to N columns depending on viewport
   and number of cards, so adding or removing a card needs no CSS
   change. Each .kpi-card reads --accent from its inline style (set per
   card in the view); the optional .kpi-card--accented modifier tints
   the card background to match. Cards without --accent stay neutral.
   NOTE: named .housing-kpis, NOT .kpi-strip -- the garage KPI partial
   (views/partials/kpi-strip.eta) owns .kpi-strip as a flex column with a
   .kpi-strip__grid child. Sharing the bare name made the later garage
   rule's `display:flex` win the cascade and collapse this grid to a
   single column (the 2026-05-30 housing-layout bug). */
.housing-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
}
.kpi-card {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-top: 2px solid var(--accent, transparent);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  padding: 1.4rem 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.kpi-card--accented {
  background: color-mix(in srgb, var(--accent) 6%, var(--surface-1));
}
/* Global status accent. Set via data-status="red|orange|yellow|green|muted"
   on anything that should tint itself with the status colour (KPI cards,
   housing dots/markers, map popups, town-bonus chips). "muted" is for
   inactive/zero states (e.g. a town with no payment bonus). */
[data-status="muted"]  { --accent: var(--status-muted); }
[data-status="red"]    { --accent: var(--status-red); }
[data-status="orange"] { --accent: var(--status-orange); }
[data-status="yellow"] { --accent: var(--status-yellow); }
[data-status="green"]  { --accent: var(--status-green); }
.kpi-card__label--red    { color: var(--status-red); }
.kpi-card__label--orange { color: var(--status-orange); }
.kpi-card__label--yellow { color: var(--status-yellow); }
.kpi-card__label--green  { color: var(--status-green); }
.kpi-card__value {
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-xl), 3.5vw, var(--fs-2xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  color: var(--accent, var(--text));
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}
.kpi-card__sub   { font-size: var(--fs-xs); color: var(--text-muted); }

/* Card chrome shared across data-focused pages */
.econ-card { background: var(--surface-1); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); overflow: hidden; }

/* Map tab: full-width Leaflet map of delivery sites (markers reuse the shared
   .poi-marker__dot from the GPS map). The toolbar holds the map-only controls. */
.econ-map-pane {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.econ-map-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}
.econ-map {
  width: 100%;
  height: clamp(32rem, 72vh, 60rem);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
/* Hovered/pinned site on the economy map: enlarge its dot so the focus is clear
   while its supply-flow arrows are drawn. */
.poi-marker--active .poi-marker__dot {
  transform: scale(1.6);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--map-poi) 45%, transparent), var(--map-glow-box);
}

/* Pinned-site panel sits beside the map; the map flexes to fill the rest. */
.econ-map-split {
  display: flex;
  gap: 1rem;
  /* stretch (not flex-start) so that when this stacks into a column on narrow
     screens the children fill the full width -- otherwise the map collapses to
     ~0px on the cross axis. In the row layout both children have an explicit
     height, so stretch is a no-op there. */
  align-items: stretch;
}
.econ-map-wrap {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

/* Legend: a floating "?" at the map's bottom-right toggles a popover (zoom
   controls sit at bottom-left). Mirrors the GPS map's legend. */
.econ-map-legend-btn {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: var(--z-map-control);
  width: var(--size-4xl);
  height: var(--size-4xl);
  border-radius: var(--radius-full);
  border: 1px solid var(--border-emphasis);
  background: var(--surface-overlay);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--text);
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  cursor: pointer;
  transition: background var(--motion-base), border-color var(--motion-base);
}
.econ-map-legend-btn:hover { background: color-mix(in srgb, var(--surface-3) 50%, var(--surface-overlay)); }
.econ-map-legend-btn--open { background: color-mix(in srgb, var(--map-poi) 22%, transparent); border-color: var(--map-poi); }
.econ-map-legend {
  position: absolute;
  right: 12px;
  bottom: 52px;
  z-index: var(--z-map-control);
  width: 230px;
  max-height: min(70vh, 480px);
  overflow-y: auto;
  padding: 0.85rem 1rem;
  font-size: var(--fs-xs);
  color: var(--text);
  background: var(--surface-overlay);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.econ-map-legend__title { margin: 0 0 0.4rem; font-size: var(--fs-sm); }
.econ-map-legend__heading {
  margin-top: 0.6rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.econ-map-legend__row { display: flex; align-items: center; gap: 0.5rem; padding: 0.2rem 0; }
.econ-legend-tri {
  width: 0; height: 0; flex-shrink: 0;
  border-left: 10px solid currentColor;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
}
.econ-legend-tri--in  { color: var(--econ-flow-in); }
.econ-legend-tri--out { color: var(--econ-flow-out); }
.econ-legend-dot { width: 9px; height: 9px; border-radius: var(--radius-full); background: var(--accent, var(--text-muted)); flex-shrink: 0; }
.econ-legend-line { width: 22px; flex-shrink: 0; border-top: 3px dotted var(--text); }
.econ-legend-line--approx { border-top-width: 2px; opacity: 0.45; }
.econ-legend-site {
  width: var(--size-sm); height: var(--size-sm); flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: var(--map-poi);
  border: 2px solid var(--text);
}
.econ-map-detail {
  flex: 0 0 22rem;
  max-width: 22rem;
  height: clamp(32rem, 72vh, 60rem);
  overflow-y: auto;
  padding: 1rem;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
.econ-map-hint { margin-left: auto; color: var(--text-overlay-muted); }
/* Selected cargo in the pinned panel: its flow lines stay lit, the rest dim. */
.econ-map-detail .econ-ref-list__btn.is-selected {
  font-weight: var(--fw-bold);
  text-decoration: underline;
}

/* No nested scroll: inner lists render full-height; only the panel scrolls. */
.econ-map-detail .econ-ref-list { max-height: none; overflow: visible; }

/* Collapsible sections in the pinned panel so a hauler shows only what matters. */
.econ-map-sec { border-top: 1px solid var(--border-hairline); }
.econ-map-sec__head {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.6rem 0;
  font-weight: var(--fw-bold);
}
.econ-map-sec__head::-webkit-details-marker { display: none; }
.econ-map-sec__head::before {
  content: "";
  align-self: center;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform var(--motion-base);
}
.econ-map-sec[open] .econ-map-sec__head::before { transform: rotate(90deg); }
.econ-map-sec__body { padding-bottom: 0.7rem; }

/* Glow look: round dot ends + a soft halo so the lines read as luminous flow on
   the dark map. The dots are the FLOW_DASH pattern set in JS; the marching
   keyframe (-16 = one 16px period) moves them source -> dest, which reads as
   "leaving" for outbound flows and "arriving" for inbound. */
@keyframes econ-flow-march { to { stroke-dashoffset: -16; } }
.econ-flow {
  stroke-linecap: round;
  filter: drop-shadow(0 0 3px color-mix(in srgb, var(--text) 45%, transparent));
  animation: econ-flow-march 0.6s linear infinite;
}
.econ-flow--dim { animation: none; filter: none; }   /* faded cargo stays still */

/* Direction triangles riding the flow lines: rotate an inner span to the flow
   bearing (--angle via the DOM API; CSP-safe). Fill is per-cargo on the path. */
.econ-flow-tri { background: transparent; border: 0; pointer-events: none; will-change: transform; }
.econ-flow-tri__rot { display: block; transform: rotate(var(--angle, 0deg)); }
.econ-flow-tri svg { display: block; }

@media (prefers-reduced-motion: reduce) {
  .econ-flow { animation: none; }
}

@media (max-width: 768px) {
  .econ-map-split { flex-direction: column; }
  .econ-map-detail {
    flex-basis: auto;
    max-width: none;
    width: 100%;
    height: auto;
    max-height: 24rem;
  }
}
.econ-card__header    { display: flex; align-items: center; gap: 0.75rem; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-hairline); }
.econ-card__title     { font-size: var(--fs-xs); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: 0.1em; color: var(--text); opacity: 0.7; white-space: nowrap; flex-shrink: 0; }
.econ-card__meta      { flex: 1; display: flex; align-items: center; gap: 0.4rem; font-size: var(--fs-sm); color: var(--text-muted); overflow: hidden; white-space: nowrap; }
.econ-card__body-inner { padding: 1.25rem; display: flex; flex-direction: column; gap: 1rem; }

/* Bar primitive used by htmlBar() in utils.js. Width is set per-fill via
   --fill (DOM-API setProperty in applyBarWidths()); we cannot inline it
   because the value is unbounded continuous and CSP would block it. The
   data-pct="X.X" attribute on the fill marks elements for the JS pass. */
.stat-bar       { width: 100%; background: var(--surface-2); border-radius: var(--radius-sm); overflow: hidden; height: 4px; }
.stat-bar--sm   { height: 3px; }
/* Stat-bar fill uses a deliberately slow 0.6s so the bar visibly grows
   on first render, drawing the eye. Twice --motion-animate; longer
   than any other transition in the file. */
.stat-bar__fill { height: 100%; border-radius: var(--radius-sm); transition: width 0.6s ease; background: var(--accent, var(--red)); width: var(--fill, 0%); }

/* Sortable table headers used by initSortableTable().
   --fs-dense applies to the whole table so cell content stays compact in
   the Hybrid scale; cells can opt out by setting their own size. */
/* Horizontal-scroll wrapper for wide data tables on narrow screens: wrap
   a wide <table> in <div class="table-scroll"> so it scrolls sideways
   instead of overflowing the viewport (applied to the data tables in
   phase 2). Sits next to the shared .sortable-table base. */
.table-scroll { overflow-x: auto; }
.sortable-table { font-size: var(--fs-dense, var(--fs-sm)); }
.sortable-table th[data-key] { cursor: pointer; user-select: none; white-space: nowrap; }
.sortable-table th[data-key]:hover { color: var(--text); }
.sortable-table th.th-sort-active  { color: var(--text); }
.sortable-table th .sort-indicator { display: inline-block; width: 0.85em; height: 0.85em; vertical-align: -0.15em; margin-left: 0.3em; opacity: 0.3; }
.sortable-table th.th-sort-active .sort-indicator { opacity: 1; color: var(--red); }
.sortable-table thead th {
  text-align: left; padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border-soft);
}

/* --- Housing page --- */
/* The KPI strip uses the intrinsic .housing-kpis base (auto-fit minmax 180px):
   6 cards flow down 6->5->...->1 as width shrinks, no breakpoints needed. Only
   the card padding/font below are housing-specific. */
.housing-page .kpi-card {
  padding: 1rem 1.2rem;
}
.housing-page .kpi-card__value {
  font-size: clamp(var(--fs-lg), 2.5vw, var(--fs-xl));
}

/* --- Housing table --- */
.housing-table { width: 100%; }
.housing-name__plot  { font-weight: var(--fw-medium); }
.housing-name__depot {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 0.15rem;
}
.housing-owner__link {
  color: var(--text);
  text-decoration: none;
  font-weight: var(--fw-medium);
  border-bottom: 1px solid transparent;
  transition: color var(--motion-base), border-color var(--motion-base);
}
.housing-owner__link:hover {
  color: var(--red);
  border-bottom-color: var(--red);
}
.housing-owner__anon {
  color: var(--text-dim);
  font-style: italic;
}
.housing-owner__available {
  background: color-mix(in srgb, var(--status-green) 15%, transparent);
  color: var(--status-green);
}
.housing-rent   { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text); font-weight: var(--fw-medium); }
.housing-size   { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text-muted); }
.housing-expiry { white-space: nowrap; }
.cell--empty         { color: var(--text-dim); font-weight: normal; }
.cell--empty-italic  { font-style: italic; }

/* --- Housing split-view (table + map) --- */
.housing-split {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 1rem;
  margin-top: 1rem;
  align-items: start;
}
.housing-split__list { min-width: 0; }
.housing-split__map-wrap {
  position: sticky;
  top: calc(var(--nav-h) + 1rem);
  height: calc(100vh - var(--nav-h) - 2rem);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--border-hairline);
  box-shadow: var(--shadow-card);
}
.housing-map { width: 100%; height: 100%; }
.housing-split__hint {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--surface-overlay);
  color: var(--text-overlay-muted);
  font-size: var(--fs-xs);
  padding: 0.35rem 0.75rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-soft);
  pointer-events: none;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: opacity var(--motion-animate);
}
.housing-split__hint--hidden { opacity: 0; }

.housing-table tbody tr { cursor: pointer; transition: background var(--motion-base); }
.housing-table tbody tr:hover { background: var(--surface-1); }
.housing-table tbody tr.housing-tr--active {
  background: color-mix(in srgb, var(--red) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--red);
}
.housing-table tbody tr.housing-tr--available {
  box-shadow: inset 3px 0 0 var(--status-green);
}
.housing-table tbody tr.housing-tr--available.housing-tr--active {
  background: color-mix(in srgb, var(--red) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--red), inset 6px 0 0 var(--status-green);
}

/* Leaflet assigns z-index to markers dynamically by latitude (markers
   lower on the screen render above markers higher up). To make the
   active housing marker always sit above the rest, !important is the
   only reliable way to pin it. */
/* stylelint-disable-next-line declaration-no-important */
.housing-marker--active { z-index: var(--z-map-layer) !important; }
.housing-marker--active .housing-marker__dot {
  width: var(--size-md); height: var(--size-md);
  border-color: var(--text);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--red) 45%, transparent), 0 0 8px color-mix(in srgb, var(--red) 80%, transparent);
}

@media (max-width: 1024px) {
  .housing-split { grid-template-columns: 1fr; }
  .housing-split__map-wrap {
    position: relative;
    top: auto;
    height: 380px;
  }
}
.housing-date   { color: var(--text-muted); font-size: var(--fs-dense); white-space: nowrap; }
.housing-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  margin-right: 0.45em;
  vertical-align: middle;
  flex-shrink: 0;
  background: var(--accent);
}

@media (max-width: 480px) {
  /* Housing leases -> stacked cards (the 6-column table scrolls awkwardly on a
     phone). Each row becomes a card; every data cell carries its column name in
     data-label (set in housing-app.js) and renders it beside the value. The
     plot-name cell stays the card title; thead is hidden, so tap-to-sort is
     unavailable here -- the default "expires soonest" sort applies. The
     --active/--available status stripe (inset box-shadow) survives as the
     card's left edge. */
  .housing-page .table-scroll { overflow-x: visible; }
  .housing-table,
  .housing-table tbody,
  .housing-table tr,
  .housing-table td { display: block; }
  .housing-table thead { display: none; }

  .housing-table tbody tr {
    margin-bottom: 0.6rem;
    padding: 0.2rem 0.85rem 0.55rem;
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-md);
    background: var(--surface-1);
  }

  .housing-table .housing-name {
    margin-bottom: 0.35rem;
    padding: 0.55rem 0 0.5rem;
    border-bottom: 1px solid var(--border-hairline);
  }
  .housing-table .housing-name__plot { font-size: var(--fs-md); }

  .housing-table td[data-label] {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    padding: 0.32rem 0;
  }
  .housing-table td[data-label]::before {
    content: attr(data-label);
    flex-shrink: 0;
    color: var(--text-muted);
    font-size: var(--fs-dense);
  }
  .housing-table .housing-date { white-space: normal; text-align: right; }

  /* The expiry cell carries a progress bar -> stack label over value so the bar
     spans the full width instead of being squeezed to the right edge. */
  .housing-table .housing-expiry {
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
    white-space: normal;
  }
  .housing-table .housing-expiry .stat-bar { width: 100%; }
}

/* --- Statistics page --- */
.stats-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.75rem;
}
.stats-card {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 1.1rem 1.25rem;
  text-align: center;
}
.stats-card__val {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-xl);
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  color: var(--text);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  margin-bottom: 0.35rem;
}
.stats-card__lbl {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.stats-section {
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
  padding: 1.25rem 1.5rem 1.5rem;
}
.stats-section--half { flex: 1 1 0; min-width: 0; }
.stats-section__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.stats-section__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
  margin: 0;
  flex-shrink: 0;
}
.stats-section__sub { font-size: var(--fs-sm); color: var(--text-muted); font-weight: var(--fw-regular); }

.stats-row { display: flex; gap: 1rem; flex-wrap: wrap; }

.stats-chart-wrap          { position: relative; height: 280px; }
.stats-chart-wrap--sm      { height: 200px; }
.stats-chart-wrap--md      { height: 360px; }

@media (max-width: 768px) {
  /* Two-up chart rows stack to one column. The half sections otherwise only
     shrink (min-width:0) into two unreadably narrow charts side by side. */
  .stats-section--half { flex-basis: 100%; }
  /* Tighter section padding and a slightly shorter tall chart so a stacked
     column of charts does not turn into one long scroll. */
  .stats-section { padding: 1rem 1rem 1.25rem; }
  .stats-chart-wrap--md { height: 280px; }
}

/* Segmented control (action filter group). The shared container with its
   own border distinguishes interactive filters from passive status pills,
   which stay fully rounded. Each segment carries an accent via --accent so the
   active slot can light up in its own colour (police blue, admin red,
   skill-class colour, etc.). */
.stats-toggle-group {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.15rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.2rem;
}
.stats-toggle {
  font-size: var(--fs-xs);
  padding: 0.3rem 0.75rem;
  border-radius: var(--radius-sm);
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-family: inherit;
  font-weight: var(--fw-medium);
  transition: background var(--motion-base), color var(--motion-base);
}
.stats-toggle:hover { color: var(--text); background: var(--surface-1); }
.stats-toggle--on {
  background: color-mix(in srgb, var(--accent, var(--text-muted)) 22%, transparent);
  color: var(--accent, var(--text));
  font-weight: var(--fw-bold);
}
.stats-toggle--on:hover {
  background: color-mix(in srgb, var(--accent, var(--text-muted)) 28%, transparent);
  color: var(--accent, var(--text));
}
/* Disabled pill (e.g. residents "Nearby" sort while the viewer is not
   in-game): dimmed, no hover feedback, cursor signals why. The title
   attribute on the button carries the explanation. */
.stats-toggle[disabled] {
  color: var(--text-dim);
  cursor: not-allowed;
}
.stats-toggle[disabled]:hover {
  color: var(--text-dim);
  background: transparent;
}
/* Residents-page role filter buttons. "all" maps to status-green by
   UI choice (not a real role token). Other --accent users on .stats-toggle
   (statistics.eta dataset toggles) set --accent via DOM-API, see
   statistics-app.js. */
.stats-toggle[data-filter="all"]        { --accent: var(--status-green); }
.stats-toggle[data-filter="police"]     { --accent: var(--role-police); }
.stats-toggle[data-filter="admin"]      { --accent: var(--role-admin); }
.stats-toggle[data-filter="enthusiast"] { --accent: var(--role-enthusiast); }

.stats-section__controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.75rem;
  margin-left: auto;
  align-items: center;
}
.stats-range-group {
  display: inline-flex;
  gap: 0.15rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-sm);
  padding: 0.15rem;
}
.stats-range {
  font-size: var(--fs-xs);
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-sm);
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-family: inherit;
  transition: background var(--motion-base), color var(--motion-base);
}
.stats-range:hover { color: var(--text); background: var(--surface-1); }
.stats-range--on {
  background: color-mix(in srgb, var(--red) 18%, transparent);
  color: var(--text);
  font-weight: var(--fw-medium);
}

/* Activity heatmap */
.heatmap-wrap {
  min-height: 120px;
  display: flex;
  flex-direction: column;
}
.heatmap-grid {
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr: a bare 1fr resolves to minmax(auto, 1fr),
     and the auto floor (the hour labels' min-content width) keeps the 24
     columns from shrinking, so the grid overflows narrow screens. minmax(0,..)
     lets the columns shrink to fit. */
  grid-template-columns: 2rem repeat(24, minmax(0, 1fr));
  gap: 2px;
  margin-top: 0.5rem;
}
.hm-label {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}
.hm-label--wd { justify-content: flex-end; padding-right: 4px; }
/* Smaller than --fs-xs so 24 hour labels fit a narrow heatmap row. */
/* stylelint-disable-next-line declaration-property-value-allowed-list */
.hm-label--hour { font-size: 0.55rem; }
.hm-cell {
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  cursor: default;
}
.chart-spinner {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  gap: 0.6rem;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}
.chart-spinner::before {
  content: '';
  width: var(--size-xl);
  height: var(--size-xl);
  border: 2px solid var(--border-soft);
  border-top-color: var(--text-overlay-muted);
  border-radius: var(--radius-full);
  animation: spin 0.7s linear infinite;
  flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.stats-player-search {
  position: relative;
  margin-bottom: 0.75rem;
}
.stats-search-input {
  width: 100%;
  box-sizing: border-box;
  padding: 0.4rem 0.75rem;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-sm);
  outline: none;
}
.stats-search-input:focus {
  border-color: var(--border-emphasis);
}
.stats-search-results {
  position: absolute;
  bottom: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: var(--z-popover);
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 260px;
  overflow-y: auto;
}
.stats-search-results--down {
  top: calc(100% + 4px);
  bottom: auto;
}
.stats-search-result {
  padding: 0.45rem 0.75rem;
  font-size: var(--fs-sm);
  cursor: pointer;
  color: var(--text-overlay-strong);
}
.stats-search-result a {
  color: inherit;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.stats-search-result:hover { background: var(--surface-2); }
.stats-search-result__meta {
  color: var(--text-muted);
  font-size: var(--fs-xs);
  white-space: nowrap;
}
.stats-search-result--empty {
  color: var(--text-muted);
  font-style: italic;
  cursor: default;
}
.stats-search-result--added {
  opacity: 0.4;
  cursor: default;
}
.stats-search-highlight {
  background: none;
  color: var(--text);
  font-weight: var(--fw-medium);
}
/* Online marker in search-result rows: reuses the shared .stats-live-dot
   chrome (green + pulse), this class only adds the inline spacing. */
.stats-search-result__dot {
  margin-right: 0.4rem;
  vertical-align: middle;
  flex: none;
}
/* --- POI delivery popup --- */
.poi-info-popup .leaflet-popup-content-wrapper {
  background: var(--surface-overlay);
  color: var(--text-overlay-strong);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-modal);
  padding: 0;
}
.poi-info-popup .leaflet-popup-tip-container { display: none; }
.poi-info-popup .leaflet-popup-content       { margin: 0; line-height: 1.5; }

/* Leaflet popup -- see note above .housing-popup for why these stay literal. */
/* stylelint-disable declaration-property-value-allowed-list */
.poi-popup          { min-width: 220px; font-size: 12px; display: flex; flex-direction: column; max-height: 320px; }
.poi-popup__name    { padding: 7px 12px 5px; font-size: 13px; font-weight: var(--fw-bold); color: var(--text); border-bottom: 1px solid var(--border-soft); white-space: nowrap; }
.poi-popup__section { padding: 5px 12px; border-bottom: 1px solid var(--border-hairline); }
.poi-popup__section:last-child { border-bottom: none; }
.poi-popup__head    { font-size: 9px; font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-overlay-dim); margin-bottom: 3px; }
.poi-popup__inv-row,
.poi-popup__job-row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; padding: 1px 0; }
.poi-popup__inv-name,
.poi-popup__job-dest { color: var(--text-overlay-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.poi-popup__inv-stock       { color: var(--map-poi); font-weight: var(--fw-medium); font-variant-numeric: tabular-nums; white-space: nowrap; }
.poi-popup__inv-stock--empty { color: var(--red); }
.poi-popup__job-meta { color: var(--map-poi); font-variant-numeric: tabular-nums; white-space: nowrap; }
.poi-popup__more    { color: var(--text-overlay-dim); font-size: 11px; padding: 1px 0 2px; }
.poi-popup__empty   { color: var(--text-overlay-dim); font-style: italic; }
.poi-popup__sources { padding: 1px 0 4px 8px; display: flex; flex-direction: column; gap: 1px; }
.poi-popup__source  { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 11px; }
.poi-popup__source-name  { color: var(--text-overlay-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.poi-popup__source-stock { color: var(--map-poi); font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: var(--fw-medium); }

.poi-popup__body { flex: 1; min-height: 0; overflow: hidden; }
.poi-popup--pinned .poi-popup__body { overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--border-emphasis) transparent; }
.poi-popup__hint { flex-shrink: 0; padding: 5px 12px 6px; font-size: 11px; color: var(--text-overlay-dim); text-align: center; border-top: 1px solid var(--border-soft); cursor: pointer; user-select: none; }
/* Once pinned the hint has served its purpose (and on touch the popup pins
 * straight away, so "Click to pin" would be wrong from the first frame). */
.poi-popup--pinned .poi-popup__hint { display: none; }
/* stylelint-enable declaration-property-value-allowed-list */
.poi-popup__hint:hover { color: var(--text-overlay-muted); background: var(--surface-1); }

/* Residents overview + detail
 *
 * Shared building blocks live at the top, page-specific styles below.
 * Avatar sizes (sm/md) and role pills are reused by both pages. */

/* --- Shared: avatars and role pills --- */

.resident-avatar {
  border-radius: var(--radius-full);
  object-fit: cover;
  background: var(--surface-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: var(--fw-bold);
  color: var(--text-muted);
  flex-shrink: 0;
}
/* Avatar font-size is paired to its px width/height (initials sit
   centred inside the circle); not part of the document type scale. */
/* stylelint-disable declaration-property-value-allowed-list */
.resident-avatar--sm  { width: var(--size-3xl); height: var(--size-3xl); font-size: 0.85rem; }
.resident-avatar--md  { width: var(--size-5xl); height: var(--size-5xl); font-size: 1.05rem; }
/* stylelint-enable declaration-property-value-allowed-list */
.resident-avatar--placeholder { border: 1px solid var(--border-soft); }

/* Pills inherit the role-token palette; transparent background and
   border are derived in-place via color-mix so a token change at :root
   updates pills, dots, and animations together. */
.role-pill--police {
  background: color-mix(in srgb, var(--role-police) 22%, transparent);
  color:      var(--role-police);
  border:     1px solid color-mix(in srgb, var(--role-police) 45%, transparent);
}
.role-pill--admin {
  background: color-mix(in srgb, var(--role-admin) 22%, transparent);
  color:      var(--role-admin);
  border:     1px solid color-mix(in srgb, var(--role-admin) 50%, transparent);
}
.role-pill--enthusiast {
  background: color-mix(in srgb, var(--role-enthusiast) 22%, transparent);
  color:      var(--role-enthusiast);
  border:     1px solid color-mix(in srgb, var(--role-enthusiast) 50%, transparent);
}

/* ============================================================
   Residents (overview + detail)
   ============================================================ */

.residents-live-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.25rem 0.7rem 0.25rem 0.55rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--status-green) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--status-green) 40%, transparent);
  color: color-mix(in srgb, var(--status-green) 50%, white);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.residents-live-pill--offline {
  background: color-mix(in srgb, var(--status-orange) 14%, transparent);
  border-color: color-mix(in srgb, var(--status-orange) 45%, transparent);
  color: color-mix(in srgb, var(--status-orange) 55%, white);
}
.residents-live-pill--offline .stats-live-dot {
  background: var(--status-orange);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-orange) 55%, transparent);
  animation: none;
}

/* --- Residents overview --- */

.residents-page { display: flex; flex-direction: column; gap: 1.75rem; }

.residents-hero {
  display: grid;
  grid-template-columns: 1fr minmax(260px, 420px);
  gap: 1.25rem;
  align-items: end;
}
.residents-hero__title {
  margin: 0;
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-xl), 3vw, var(--fs-2xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
}
.residents-hero__sub   { margin: 0.35rem 0 0; color: var(--text-muted); font-size: var(--fs-base); }
.residents-hero__search { position: relative; margin-bottom: 0; }
@media (max-width: 768px) {
  .residents-hero { grid-template-columns: 1fr; }
}

.residents-block {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
/* Each section header carries a "tone" color via data-accent, used by the
   leading accent bar, the underline accent and the hint pill. Tone is
   set once at the header, inherited via the --accent custom property. */
.residents-block__header {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  flex-wrap: wrap;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--border-hairline);
  position: relative;
}
.residents-block__header::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 1px;
  width: 90px;
  background: linear-gradient(90deg, var(--accent), transparent);
}
.residents-block__header[data-accent="live"]      { --accent: var(--status-green);                                  --accent-soft: color-mix(in srgb, var(--status-green)   22%, transparent); }
.residents-block__header[data-accent="champions"] { --accent: var(--brand-motortown);                                --accent-soft: color-mix(in srgb, var(--brand-motortown) 22%, transparent); }
.residents-block__header[data-accent="fame"]      { --accent: color-mix(in srgb, var(--brand-motortown) 75%, white); --accent-soft: color-mix(in srgb, var(--brand-motortown) 24%, transparent); }

.residents-block__title {
  margin: 0;
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-lg), 2.1vw, var(--fs-xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--text);
  line-height: var(--lh-tight);
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
}
.residents-block__title::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 1.05em;
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 35%, transparent));
  box-shadow: 0 0 14px var(--accent-soft);
  flex: 0 0 auto;
}
.residents-block__header[data-accent="live"] .residents-block__title::before {
  animation: residents-block-marker-pulse 2.6s ease-in-out infinite;
}
@keyframes residents-block-marker-pulse {
  0%, 100% { box-shadow: 0 0 12px var(--accent-soft); }
  50%      { box-shadow: 0 0 22px var(--accent), 0 0 4px var(--accent); }
}

.residents-block__hint {
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  letter-spacing: 0.02em;
  padding: 0.2rem 0.6rem;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  color: color-mix(in srgb, var(--accent) 35%, var(--text));
}
.residents-block__empty { color: var(--text-muted); font-size: var(--fs-base); font-style: italic; }
.residents-online-filter { margin-left: auto; }

/* "Right now" tile strip */
.residents-online-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 0.5rem;
}
.residents-online-strip__empty {
  grid-column: 1 / -1;
}
.resident-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.15rem;
  padding: 0.55rem 0.45rem 0.5rem;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  text-decoration: none;
  color: var(--text);
  transition: border-color var(--motion-base), background var(--motion-base), transform var(--motion-base);
  min-width: 0;
}
.resident-tile:hover { border-color: var(--border-emphasis); background: var(--surface-2); transform: translateY(-1px); }
/* Stretched primary link: the whole card opens the profile. Sits below the
   tile content (which is non-interactive text) but above nothing, so the
   visible children still show; the map pin gets a higher z-index to stay
   clickable on top. Two sibling links (not nested) keep the markup valid. */
.resident-tile__link {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
/* Secondary action: jump to the player's live map position. A shared
   .icon-btn (look + hover/focus glow come from there); this only pins it to
   the tile's top-left corner above the stretched profile link and tightens
   the size for a dense strip. */
.resident-tile__map {
  position: absolute;
  top: 0.2rem;
  left: 0.2rem;
  z-index: 2;
  width: 1.6rem;
  height: 1.6rem;
}
.resident-tile__map .icon { width: 1rem; height: 1rem; }
.resident-tile__avatar { position: relative; margin-bottom: 0.2rem; }
.resident-tile__roles {
  position: absolute;
  top: -3px;
  right: -3px;
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
}
.resident-tile__role-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--bg);
  animation: resident-role-pulse 2.4s ease-in-out infinite;
}
/* `color` is consumed as the pulse-halo via `currentColor` in the
   @keyframes below; deriving it from the token keeps the halo in step. */
.resident-tile__role-dot--police {
  background: var(--role-police);
  color:      color-mix(in srgb, var(--role-police) 60%, transparent);
}
.resident-tile__role-dot--admin {
  background: var(--role-admin);
  color:      color-mix(in srgb, var(--role-admin) 60%, transparent);
}
.resident-tile__role-dot--enthusiast {
  background: var(--role-enthusiast);
  color:      color-mix(in srgb, var(--role-enthusiast) 60%, transparent);
}
@keyframes resident-role-pulse {
  0%   { box-shadow: 0 0 0 0   currentColor; }
  70%  { box-shadow: 0 0 0 5px transparent; }
  100% { box-shadow: 0 0 0 0   transparent; }
}
.resident-tile__name {
  font-weight: var(--fw-bold);
  color: var(--text);
  font-size: var(--fs-dense);
  line-height: var(--lh-tight);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.resident-tile__meta {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.resident-tile__since {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  margin-top: 0.1rem;
}
/* Skill accent slot. Set via data-skill="CL_X" on any element that needs to
   tint itself in the skill's colour (champion cards, char tabs, invalid
   pills). Values come from the --skill-* token family in :root; the JS
   side (src/public/skills.js) keeps its own copies of the hex literals
   for dimHex() colour math -- keep both in sync on retune. */
[data-skill="CL_Driver"]  { --accent: var(--skill-driver); }
[data-skill="CL_Taxi"]    { --accent: var(--skill-taxi); }
[data-skill="CL_Bus"]     { --accent: var(--skill-bus); }
[data-skill="CL_Truck"]   { --accent: var(--skill-truck); }
[data-skill="CL_Racer"]   { --accent: var(--skill-racer); }
[data-skill="CL_Wrecker"] { --accent: var(--skill-wrecker); }
[data-skill="CL_Police"]  { --accent: var(--skill-police); }

/* Skill champions: 7 colored cards, one per skill class */
.residents-champions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 0.75rem;
}
.resident-champion {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 0.9rem 1rem;
  border-radius: var(--radius-lg);
  text-decoration: none;
  color: var(--text);
  background:
    linear-gradient(140deg,
      color-mix(in srgb, var(--accent) 28%, transparent),
      color-mix(in srgb, var(--accent) 8%, transparent) 70%);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  transition: transform var(--motion-base), border-color var(--motion-base);
}
.resident-champion:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--accent) 75%, transparent); }
.resident-champion__skill {
  font-size: var(--fs-xs);
  font-weight: var(--fw-hero);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: color-mix(in srgb, var(--accent) 90%, white 25%);
}
.resident-champion__level {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-xl);
  font-weight: var(--fw-hero);
  color: var(--text);
  line-height: 1;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
}
.resident-champion__player {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: auto;
}
.resident-champion__name { font-weight: var(--fw-bold); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-sm); }

/* Hall of fame: six small leaderboards in a single row on desktop, with
   stepwise fallbacks to keep the rows readable as the viewport shrinks.
   Step boundaries are picked so each column never gets narrower than ~150px
   for the avatar+rank+name+value layout to stay legible. */
/* Flex-wrap instead of a fixed N-column grid so an odd table count (7 since the
   reputation column) wraps tidily: full rows fill edge-to-edge, and a partial
   last row CENTRES (justify-content) instead of leaving a lone left-aligned
   card. flex-basis sets the wrap point; max-width stops a lone card from
   stretching wide; align-items:flex-start keeps each card its natural height. */
.residents-hall {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 0.75rem;
}

.residents-hall__col {
  flex: 1 1 200px;
  max-width: 320px;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: 0.7rem 0.75rem;
  min-width: 0;
}
/* One full-width card per row on phones. */
@media (max-width: 480px) { .residents-hall__col { flex-basis: 100%; max-width: none; } }
.residents-hall__title {
  margin: 0 0 0.55rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-hero);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text);
  opacity: 0.85;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.25rem;
}
.residents-hall__list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.residents-hall__row {
  display: grid;
  /* Rank column wide enough for a two-digit "#10" so it never overlaps the
     avatar; single-digit ranks just get a little leading space. */
  grid-template-columns: 1.7rem 1fr auto;
  align-items: center;
  gap: 0.35rem;
  padding: 0.22rem 0;
  border-bottom: 1px dashed var(--border-hairline);
  font-size: var(--fs-dense);
  min-width: 0;
}
.residents-hall__row:last-child { border-bottom: none; }
.residents-hall__rank { color: var(--text-dim); font-weight: var(--fw-bold); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.residents-hall__link { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--text); text-decoration: none; min-width: 0; }
.residents-hall__link:hover .residents-hall__name { color: var(--red); }
.residents-hall__name { font-weight: var(--fw-medium); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-dense); min-width: 0; }
.residents-hall__value { font-weight: var(--fw-bold); color: var(--text); font-variant-numeric: tabular-nums; font-size: var(--fs-dense); text-align: right; justify-self: end; }
/* Smaller avatars so 10 rows fit compactly into a narrow 6-up column.
   font-size paired to 18px width -- see avatar note above. */
/* stylelint-disable-next-line declaration-property-value-allowed-list */
.residents-hall__link .resident-avatar--sm { width: var(--size-xl); height: var(--size-xl); font-size: 0.6rem; }

/* --- Resident detail page --- */

.resident-page { display: flex; flex-direction: column; gap: 1.5rem; }

.resident-toolbar { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.resident-toolbar__search { flex: 1; min-width: 240px; max-width: 480px; margin-bottom: 0; }
.resident-back-link {
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--fs-base);
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-soft);
  white-space: nowrap;
  transition: color var(--motion-base), border-color var(--motion-base);
}
.resident-back-link:hover { color: var(--text); border-color: var(--border-emphasis); }

/* Hero */
.resident-hero {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: 1rem 1.1rem;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--red) 8%, var(--surface-overlay)),
    var(--surface-overlay));
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
.resident-hero__top {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1rem;
  align-items: center;
}
.resident-hero__avatar { width: var(--size-6xl); height: var(--size-6xl); flex-shrink: 0; }
.resident-avatar-img,
.resident-avatar-fallback {
  width: var(--size-6xl);
  height: var(--size-6xl);
  border-radius: var(--radius-full);
  object-fit: cover;
  display: block;
  border: 2px solid var(--border-strong);
}
.resident-avatar-fallback {
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Paired to 64px avatar -- see avatar note above. */
  /* stylelint-disable-next-line declaration-property-value-allowed-list */
  font-size: 1.6rem;
  font-weight: var(--fw-hero);
  color: var(--text-muted);
}
.resident-hero__ident { min-width: 0; }
.resident-hero__name {
  margin: 0;
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-lg), 2.4vw, var(--fs-xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
}
/* align-items shifted from baseline to center now that the row holds the
   "Show on map" pill button next to the short status/vehicle labels. */
.resident-hero__status-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.35rem; font-size: var(--fs-base); }
.resident-hero__playing-as { margin-top: 0.2rem; font-size: var(--fs-sm); color: var(--text-muted); }

/* Name line: headline name + inline role badges sitting right next to it. */
.resident-hero__nameline { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
.resident-hero__roles { display: inline-flex; align-items: center; gap: 0.3rem; }

/* Reputation chip (account-scoped): star + point sum + rank, inline in the
   identity line. Compact -- no card, no breakdown. The star is gold; the rank
   reads as a quiet caps label after the number. */
.resident-reputation {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.55rem;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
}
.resident-reputation__star { width: var(--size-md); height: var(--size-md); color: var(--status-yellow); }
.resident-reputation__points {
  font-weight: var(--fw-hero);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.resident-reputation__rank {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--status-yellow) 70%, white 20%);
}
.resident-reputation__rank:empty { display: none; }
.resident-reputation__rank::before { content: "\00b7"; margin-right: 0.35rem; color: var(--text-dim); }

/* Police badge chip (account-scoped RP): shield + badge number + rank, inline in
   the identity line. Mirrors the reputation chip's compact inline-flex; themed
   with the police role token. The number span collapses when empty (PPO/trainee
   carry no number) so only the rank shows. */
.resident-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--fs-sm);
  color: var(--role-police);
}
.resident-badge__icon { width: 1.15em; height: 1.15em; color: var(--role-police); }
.resident-badge__number {
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.resident-badge__number:empty { display: none; }
.resident-badge__rank {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--role-police) 75%, white 20%);
}
.resident-badge__rank:empty { display: none; }
/* Staff-only privacy flag: same chip shape, muted tone -- it is an admin
   hint, not an RP decoration. */
.resident-badge--private,
.resident-badge--private .resident-badge__icon { color: var(--text-muted); }
.resident-badge__label {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
/* Separator dot before the rank, but only when a number precedes it. */
.resident-badge__number:not(:empty) + .resident-badge__rank::before {
  content: "\00b7"; margin-right: 0.35rem; color: var(--text-dim);
}

.resident-status--online  { color: var(--status-green); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: 0.08em; font-size: var(--fs-xs); }
.resident-status--offline { color: var(--text-muted); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: 0.08em; font-size: var(--fs-xs); }
.resident-vehicle { font-size: var(--fs-sm); color: var(--text); opacity: 0.85; }

/* The "show on map" button is a shared .icon-btn .icon-btn--lg (look + the
   disabled/offline treatment both come from there); no bespoke rule needed. */

/* Account-wide KPI strip */
.resident-kpis {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.5rem;
}
.resident-kpis li {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.5rem 0.7rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
}
.resident-kpi__value {
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-md), 1.8vw, var(--fs-lg));
  font-weight: var(--fw-hero);
  color: var(--text);
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-tight);
  letter-spacing: 0.01em;
}

/* Character picker + properties row */
.resident-hero__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.5rem;
}
/* "Characters" label + the tab list, kept together so the label always sits
   with its tabs when the row wraps. */
.resident-chars { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
.resident-chars__label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.resident-chars__label .icon { width: var(--size-md); height: var(--size-md); }
.resident-char-tabs {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
/* Chip chrome comes from the shared rule near the top of this file. */
/* Doubled selector wins specificity over `:hover`, so the active red stays
   visible while the cursor is still over the tab after a click. Without
   the explicit `:hover` partner, `.foo:hover` (0,2,0) beats `.foo--active`
   (0,1,0) and the active state only shows after the cursor moves away. */
.resident-char-tab--active,
.resident-char-tab--active:hover {
  background: color-mix(in srgb, var(--red) 18%, transparent);
  border-color: color-mix(in srgb, var(--red) 55%, transparent);
  cursor: default;
}
.resident-char-tab__name { font-weight: var(--fw-bold); font-size: var(--fs-sm); color: var(--text); }

.resident-properties {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--fs-sm);
}
.resident-properties__label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-right: 0.2rem;
}
.resident-properties__label .icon { width: var(--size-md); height: var(--size-md); }
/* Property links: shared chip chrome; each flies the map to the plot. */
.resident-property {
  font-weight: var(--fw-bold);
  font-size: var(--fs-sm);
}

/* Cards (shared) */
.resident-card {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 0.85rem 1rem;
}
.resident-card__title {
  margin: 0 0 0.85rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-hero);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text);
  opacity: 0.85;
}
.resident-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.85rem;
  flex-wrap: wrap;
}
.resident-card__header .resident-card__title { margin: 0; }
.resident-card__sub {
  margin: 1rem 0 0.45rem;
}
.resident-card__sub-note { margin-left: 0.4rem; }

/* Standalone section box (Community activity + Career). Its own surface, set
   apart from the account hero so the scopes read as distinct regions. The left
   accent bar marks it as a sub-region of the profile. */
.resident-section {
  background: var(--surface-overlay);
  border: 1px solid var(--border-soft);
  border-left: 3px solid color-mix(in srgb, var(--red) 55%, transparent);
  border-radius: var(--radius-lg);
  padding: 0.4rem 1.25rem 1.1rem;
}
.resident-section__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--text);
  letter-spacing: 0.04em;
}

/* Collapsible behaviour shared by the activity + career boxes. The header is
   a full-width button; the caret rotates and the body hides when collapsed.
   Collapse state persists in localStorage (resident-app.js initCollapsibles). */
.resident-collapsible__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.6rem 0;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
  font: inherit;
  text-align: left;
}
.resident-collapsible__chevron {
  width: var(--size-md);
  height: var(--size-md);
  color: var(--text-muted);
  flex-shrink: 0;
  transition: transform var(--motion-base);
}
.resident-collapsible--collapsed .resident-collapsible__chevron { transform: rotate(-90deg); }
.resident-collapsible__body {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.resident-collapsible--collapsed .resident-collapsible__body { display: none; }

.resident-section-title {
  margin: 0;
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--text);
  letter-spacing: 0.04em;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}
.resident-section-title span:first-of-type { color: var(--red); }

.resident-profile-tag--specialist  { background: color-mix(in srgb, var(--red)           22%, transparent); color: color-mix(in srgb, var(--red)           50%, white); border: 1px solid color-mix(in srgb, var(--red)           50%, transparent); }
.resident-profile-tag--hybrid      { background: color-mix(in srgb, var(--status-orange) 18%, transparent); color: color-mix(in srgb, var(--status-orange) 55%, white); border: 1px solid color-mix(in srgb, var(--status-orange) 45%, transparent); }
.resident-profile-tag--allrounder  { background: color-mix(in srgb, var(--status-green)  18%, transparent); color: color-mix(in srgb, var(--status-green)  50%, white); border: 1px solid color-mix(in srgb, var(--status-green)  40%, transparent); }

/* Company/corporation membership chips in the career title line. Chrome
   comes from the Tag-pill family (see "Tag-pill chrome" group); only the
   colour recipe lives here. Corp chips tint toward the brand red (the
   "official" groups), plain companies stay neutral. */
.resident-companies {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-left: 0.6rem;
  vertical-align: middle;
}
.resident-company-tag {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
}
.resident-company-tag--corp {
  background: color-mix(in srgb, var(--red) 14%, transparent);
  border-color: color-mix(in srgb, var(--red) 40%, transparent);
  color: color-mix(in srgb, var(--red) 55%, white);
}

.resident-aka {
  margin-left: 0.6rem;
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  font-style: italic;
  color: var(--text-muted);
  opacity: 0.75;
  letter-spacing: 0;
}

/* Activity heatmap (7 weekdays x 24 hours). The 24 hour columns share the full
   card width (1fr each) so the grid runs edge to edge instead of staying
   squashed -- there is nothing to gain from compressing it. */
.resident-heatmap {
  display: grid;
  grid-template-columns: 2rem repeat(24, 1fr);
  gap: 2px;
  width: 100%;
}
.resident-heatmap__corner { /* empty cell */ }
.resident-heatmap__hour {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  text-align: center;
  font-variant-numeric: tabular-nums;
  padding-bottom: 4px;
}
.resident-heatmap__day {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  align-self: center;
}
.resident-heatmap__cell {
  /* Cells stretch to full card width (1fr columns) but keep a fixed, compact
     height so the heatmap fills the row horizontally without growing tall and
     unbalancing the column against the skills treemap. */
  height: 14px;
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  transition: transform var(--motion-fast);
  cursor: default;
}
.resident-heatmap__cell:hover { transform: scale(1.15); outline: 1px solid var(--text-muted); }
.resident-heatmap__cell[data-l="0"] { background: var(--surface-1); }
.resident-heatmap__cell[data-l="1"] { background: color-mix(in srgb, var(--red) 20%, transparent); }
.resident-heatmap__cell[data-l="2"] { background: color-mix(in srgb, var(--red) 40%, transparent); }
.resident-heatmap__cell[data-l="3"] { background: color-mix(in srgb, var(--red) 65%, transparent); }
.resident-heatmap__cell[data-l="4"] { background: color-mix(in srgb, var(--red) 95%, transparent); }
.resident-heatmap__empty {
  color: var(--text-muted);
  font-style: italic;
  text-align: center;
  margin: 1rem 0 0;
}
.resident-heatmap__legend { display: inline-flex; align-items: center; gap: 0.4rem; font-size: var(--fs-xs); color: var(--text-dim); }
.resident-heatmap__legend-scale { display: inline-flex; gap: 2px; }
.resident-heatmap__legend-cell {
  width: var(--size-md);
  height: var(--size-md);
  border-radius: var(--radius-sm);
}
.resident-heatmap__legend-cell[data-l="0"] { background: var(--surface-1); }
.resident-heatmap__legend-cell[data-l="1"] { background: color-mix(in srgb, var(--red) 20%, transparent); }
.resident-heatmap__legend-cell[data-l="2"] { background: color-mix(in srgb, var(--red) 40%, transparent); }
.resident-heatmap__legend-cell[data-l="3"] { background: color-mix(in srgb, var(--red) 65%, transparent); }
.resident-heatmap__legend-cell[data-l="4"] { background: color-mix(in srgb, var(--red) 95%, transparent); }

/* Career grid: skills (treemap) + driving (stats + bars) side by side */
.resident-grid--career {
  display: grid;
  grid-template-columns: minmax(280px, 1.3fr) minmax(280px, 1fr);
  gap: 1.25rem;
  /* stretch so the skills card grows to match the taller right column
     (heatmap + driving stacked), filling the gap that 'start' left below it. */
  align-items: stretch;
}
@media (max-width: 1024px) {
  .resident-grid--career { grid-template-columns: 1fr; }
}
/* Right column of the career grid: heatmap card stacked above the driving
   card. */
.resident-career__right {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  min-width: 0;
}
/* Skills card is a flex column so its treemap fills the height the stretched
   grid gives it (matching the right column). */
.resident-card--skills { display: flex; flex-direction: column; }
.resident-treemap {
  position: relative;
  width: 100%;
  flex: 1;             /* take the leftover card height */
  min-height: 220px;   /* floor on short profiles / mobile single-column */
}
/* Chart.js paints the treemap canvas with inline width/height styles, which
   beat any non-!important rule in the cascade. With maintainAspectRatio:false
   the chart sizes to this container, so the canvas must fill it exactly. */
/* stylelint-disable-next-line declaration-no-important */
.resident-treemap canvas { display: block; width: 100% !important; height: 100% !important; }
.resident-radar__note { color: var(--text-muted); font-style: italic; text-align: center; margin: 1rem 0; }

.resident-skills-invalid {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0.85rem 0 0;
  padding: 0.55rem 0.7rem;
  background: var(--surface-1);
  border: 1px dashed var(--border-emphasis);
  border-radius: var(--radius-md);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.resident-skills-invalid__label {
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--fs-xs);
  color: var(--text);
  opacity: 0.75;
}
.resident-skills-invalid__list { display: inline-flex; flex-wrap: wrap; gap: 0.3rem; }
.resident-skills-invalid__pill {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--accent) 80%, white 25%);
}
.resident-skills-invalid__hint { margin-left: auto; }

/* Compact bought/sold counters in the Driving card header: a car icon with a
   small in/out arrow badge + the count. Sits inline in the card header next to
   the title (justify-content: space-between handles the placement). */
.resident-trade { display: inline-flex; align-items: center; gap: 0.9rem; }
.resident-trade__item {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.resident-trade__item .icon { width: var(--size-lg); height: var(--size-lg); }
.resident-trade__dir { width: var(--size-sm); height: var(--size-sm); }
.resident-trade__dir--in  { color: var(--status-green); }
.resident-trade__dir--out { color: var(--status-orange); }
.resident-trade__num {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-hero);
  color: var(--text);
}

.resident-skill-list { list-style: none; padding: 0; margin: 0.5rem 0 0; display: flex; flex-direction: column; gap: 0.5rem; }
.resident-skill {
  display: grid;
  grid-template-columns: 6.5rem 1fr auto;
  align-items: center;
  gap: 0.7rem;
  font-size: var(--fs-sm);
}
.resident-skill__name  { color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.resident-skill__bar   { display: block; }
.resident-skill__value { font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; min-width: 3rem; text-align: right; color: var(--text); }
.resident-skill .stat-bar       { height: 8px; border-radius: var(--radius-sm); background: var(--surface-1); }
.resident-skill .stat-bar__fill { background: linear-gradient(90deg, color-mix(in srgb, var(--red) 55%, white), var(--red)); border-radius: var(--radius-sm); }

/* Recent moves timeline */
.resident-card--timeline { padding-bottom: 1.25rem; }
.resident-timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
/* Five aligned columns: icon | when | skill/event | diff | level. The icon disc
   tints with --accent (skill colour for level-ups, kind colour for login /
   logout). Tabular numerals keep the diff/level columns aligned while paging. */
.resident-timeline__row {
  display: grid;
  grid-template-columns: 1.75rem 5.5rem 1fr 3.5rem 5.5rem;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  border-bottom: 1px dashed var(--border-hairline);
  font-size: var(--fs-base);
}
.resident-timeline__row:last-child { border-bottom: none; }
/* Row accent per event kind. Level-up rows carry a data-skill attribute that
   overrides --accent via the global skill-colour selector, so a level-up tints
   with the skill colour; login/logout use neutral kind tones. */
.resident-timeline__row[data-kind="login"]  { --accent: var(--status-green); }
.resident-timeline__row[data-kind="logout"] { --accent: var(--text-muted); }
.resident-timeline__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
}
.resident-timeline__icon svg { width: 0.95rem; height: 0.95rem; }
/* Login/logout markers: glowing white Lucide icons, no tinted disc -- they are
   navigation events, not skill colour, so they read as a neutral bright accent
   (same --glow-white treatment as the shared icon-button hover state). */
.resident-timeline__icon--event {
  background: none;
  color: var(--text);
  filter: drop-shadow(var(--glow-white));
}
.resident-timeline__when  { color: var(--text-muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; white-space: nowrap; }
.resident-timeline__skill { color: var(--text); font-weight: var(--fw-bold); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resident-timeline__skill--event { font-weight: var(--fw-regular); color: var(--text-muted); }
.resident-timeline__diff  { color: var(--accent); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; text-align: right; }
.resident-timeline__diff--empty,
.resident-timeline__level--empty { color: var(--border-hairline); }
.resident-timeline__level { color: var(--text-muted); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.resident-timeline__empty { color: var(--text-muted); font-style: italic; margin: 0.5rem 0 0; }

/* Pager: Newer / Page N / Older, mirrors the history-stream pager controls. */
.resident-timeline__total { color: var(--text-muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.resident-timeline__pager { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 0.75rem; }
.resident-timeline__page  { color: var(--text-muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; display: inline-flex; align-items: center; gap: 0.35rem; }
.resident-timeline__page-input {
  width: 3.5rem;
  text-align: center;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  padding: 0.2rem 0.3rem;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  /* Drop the native number-spinner: it stole ~1rem of width (leaving room
     for only two digits) and is redundant with the Newer/Older buttons. */
  appearance: textfield;
}
.resident-timeline__page-input::-webkit-inner-spin-button,
.resident-timeline__page-input::-webkit-outer-spin-button { appearance: none; margin: 0; }
.resident-timeline__page-total { color: var(--text); font-weight: var(--fw-medium); }
.resident-timeline__page-btn {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  padding: 0.35rem 0.9rem;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.resident-timeline__page-btn:hover:not(:disabled) { background: var(--surface-3); }
.resident-timeline__page-btn:disabled { opacity: 0.4; cursor: default; }

/* Character-switch animation: each block scoped to the current character
   gets a brief fade-up when the user clicks a different char-tab. The JS
   removes + re-adds the class to restart the animation per switch. */
@keyframes resident-character-switch {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.resident-character-block--switching {
  animation: resident-character-switch var(--motion-slow) ease;
}

/* Account activity (tabbed). A compact horizontal stat row, not a grid of
   fat cards -- numbers read first, labels below in small caps. The tab strip
   surfaces only when more than one context exists (police/convoy later);
   today the single Tow tab is hidden and the panel renders bare. */
.resident-activity {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.7rem 0.9rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
}
.resident-activity__tabs {
  display: flex;
  gap: 0.3rem;
  border-bottom: 1px solid var(--border-hairline);
  padding-bottom: 0.4rem;
}
.resident-activity__tab {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  padding: 0.25rem 0.7rem;
  border: none;
  background: transparent;
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
}
.resident-activity__tab:hover { color: var(--text); background: var(--surface-2); }
.resident-activity__tab--active {
  background: color-mix(in srgb, var(--red) 20%, transparent);
  color: var(--red);
  font-weight: var(--fw-bold);
}
.resident-activity__stats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 1.4rem 2rem;
}
.resident-activity__stats li { display: flex; flex-direction: column; gap: 0.1rem; }
.resident-activity__num {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-lg);
  font-weight: var(--fw-hero);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  line-height: var(--lh-tight);
}
/* Footer KPIs row (character-scoped detail metrics) */
.resident-footer-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 0.5rem;
}
.resident-footer-kpis > div {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.55rem 0.8rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
}
.resident-footer-kpis .resident-kpi__label { font-size: var(--fs-xs); }
.resident-footer-kpis .resident-kpi__value {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
}

/* --- Economy page ---------------------------------------------------- */

.econ-page {
  max-width: var(--max-w-dashboard);
  margin: 0 auto;
  padding: 2.5rem 2rem 4rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.econ-header        { display: flex; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
.econ-header__title {
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-xl), 4vw, var(--fs-2xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
  margin: 0;
}
.econ-header__sub   { margin: 0.25rem 0 0; color: var(--text-muted); font-size: var(--fs-base); }
.econ-header__meta  { display: flex; align-items: center; gap: 0.6rem; font-size: var(--fs-sm); color: var(--text-muted); }

.econ-live-dot {
  width: var(--size-xs); height: var(--size-xs); border-radius: var(--radius-full);
  background: var(--status-green);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-green) 50%, transparent);
  animation: econ-pulse 2s infinite;
}
.econ-last-update { color: var(--text); }
.econ-countdown   { color: var(--text-dim); font-size: var(--fs-xs); }

@keyframes econ-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-green) 50%, transparent); }
  70%  { box-shadow: 0 0 0 8px color-mix(in srgb, var(--status-green) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-green) 0%, transparent); }
}

.econ-toolbar {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 0.6rem 0.75rem;
}

.econ-tabs {
  display: inline-flex;
  gap: 0.15rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.2rem;
}
.econ-tab {
  font-size: var(--fs-sm);
  padding: 0.35rem 0.95rem;
  border-radius: var(--radius-sm);
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-family: inherit;
  font-weight: var(--fw-medium);
  transition: background var(--motion-base), color var(--motion-base);
}
.econ-tab:hover { color: var(--text); background: var(--surface-1); }
.econ-tab.is-active {
  background: color-mix(in srgb, var(--red) 22%, transparent);
  color: var(--red);
  font-weight: var(--fw-bold);
}

.econ-search {
  flex: 1;
  min-width: 220px;
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.45rem 1rem;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  transition: background var(--motion-base), border-color var(--motion-base);
}
.econ-search:focus-within {
  background: var(--surface-3);
  border-color: var(--border-emphasis);
}
.econ-search__icon  { width: var(--size-lg); height: var(--size-lg); color: var(--text-dim); flex-shrink: 0; }
.econ-search__input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  font-size: var(--fs-base);
}
.econ-search__input::placeholder { color: var(--text-dim); }
.econ-search__clear {
  background: transparent; border: none;
  color: var(--text-muted); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding: 0.2rem;
  transition: color var(--motion-base), transform var(--motion-fast);
}
.econ-search__clear:hover { color: var(--text); }
.econ-search__clear:active { transform: scale(0.92); }

.econ-layout {
  display: grid;
  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}
@media (max-width: 1024px) {
  .econ-layout { grid-template-columns: minmax(0, 1fr); }
}

.econ-list-pane {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 0.85rem;
  display: flex; flex-direction: column; gap: 0.6rem;
  max-height: calc(100vh - 220px);
  overflow: hidden;
  min-width: 0;
}
.econ-list-pane__count {
  padding: 0 0.25rem;
}
.econ-list {
  display: flex; flex-direction: column; gap: 0.3rem;
  overflow-y: auto;
  padding-right: 0.25rem;
}
.econ-list__item {
  display: grid;
  grid-template-columns: 10px 1fr;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-1);
  color: var(--text);
  cursor: pointer;
  text-align: left;
  font-size: var(--fs-sm);
}
.econ-list__item:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
}
.econ-list__item.is-selected {
  background: color-mix(in srgb, var(--red) 12%, transparent);
  border-color: color-mix(in srgb, var(--red) 55%, transparent);
}
.econ-list__dot {
  width: var(--size-xs); height: var(--size-xs); border-radius: var(--radius-full);
  background: var(--accent, var(--text-dim));
}
.econ-list__main {
  display: flex; flex-direction: column; gap: 0.1rem;
  min-width: 0;
}
.econ-list__title {
  font-weight: var(--fw-medium); color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.econ-list__sub {
  font-size: var(--fs-xs); color: var(--text-dim);
}
.econ-list__metrics {
  grid-column: 1 / -1;
  display: flex; gap: 0.75rem;
  margin-top: 0.25rem;
  padding-top: 0.4rem;
  border-top: 1px dashed var(--border-hairline);
  font-size: var(--fs-xs);
}
.econ-list__metric        { display: flex; align-items: baseline; gap: 0.3rem; }
.econ-list__metric-label  { color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; font-size: var(--fs-xs); }
.econ-list__metric-value  { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* Restock: haul-units value tinted by match precision -- green when the cargo
   is exact, orange (with a leading ~) when only the category is known. Same
   status tokens as .badge--online. The compound selector beats the plain
   .econ-list__metric-value colour. */
.econ-list__metric-value.econ-haul--exact  { color: color-mix(in srgb, var(--status-green) 70%, white); }
.econ-list__metric-value.econ-haul--approx { color: color-mix(in srgb, var(--status-orange) 75%, white); cursor: help; }
.econ-haul--exact  { color: color-mix(in srgb, var(--status-green) 70%, white); }
.econ-haul--approx { color: color-mix(in srgb, var(--status-orange) 75%, white); }
.econ-needs-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: baseline;
  padding-bottom: 0.5rem;
  font-size: var(--fs-xs);
  color: var(--text-dim);
}

/* Restock max-stock slider: label, range and live value on one row. */
.econ-needs-range { flex-direction: row; align-items: center; gap: 0.5rem; }
.econ-needs-range input[type="range"] { accent-color: var(--status-orange); }
.econ-needs-range__value {
  min-width: 1.5rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.econ-detail-pane {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  display: flex; flex-direction: column; gap: 1.25rem;
  min-height: 240px;
  min-width: 0;
}
.econ-detail-pane__placeholder {
  margin: auto; color: var(--text-dim); font-size: var(--fs-base);
  text-align: center;
}


.econ-detail__header {
  display: flex; gap: 0.85rem; align-items: flex-start;
}
.econ-detail__type-dot {
  width: var(--size-md); height: var(--size-md); border-radius: var(--radius-full);
  flex-shrink: 0; margin-top: 0.4rem;
  background: var(--accent, var(--text-dim));
}
.econ-detail__type-dot--site {
  background: var(--surface-3);
  border: 2px solid var(--border-emphasis);
}
.econ-detail__title {
  margin: 0;
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
}
.econ-detail__sub   { margin: 0.25rem 0 0; color: var(--text-muted); font-size: var(--fs-sm);
                      display: flex; flex-wrap: wrap; gap: 0.4rem; }
.econ-detail__sep   { color: var(--text-dim); }

.econ-detail__metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 0.5rem;
}
.econ-metric {
  display: flex; flex-direction: column; gap: 0.15rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.55rem 0.75rem;
}
.econ-metric__value {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.econ-detail__cols {
  display: grid;
  /* Two equal reference-list columns; auto-fit collapses to one when the
     column would drop below ~280px -- no breakpoint needed (was @720). */
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1rem;
}

.econ-col {
  display: flex; flex-direction: column; gap: 0.5rem;
  min-width: 0;
}
.econ-col__title {
  display: flex; gap: 0.4rem; align-items: baseline;
}
.econ-col__count {
  color: var(--text-dim); font-weight: var(--fw-medium);
}

.econ-ref-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.2rem;
  max-height: 320px;
  overflow-y: auto;
}
.econ-ref-list__item {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
}
.econ-ref-list__dot { width: var(--size-xs); height: var(--size-xs); border-radius: var(--radius-full); flex-shrink: 0; background: var(--accent, var(--text-dim)); }
.econ-ref-list__btn {
  background: transparent; border: none; padding: 0;
  color: var(--text); flex: 1; text-align: left;
  cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.econ-ref-list__btn:hover { color: var(--red); text-decoration: underline; }
.econ-ref-list__amount {
  display: inline-flex; align-items: baseline; gap: 0.3rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  flex-shrink: 0;
}
.econ-ref-list__amount-label {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.econ-jobs {
  display: flex; flex-direction: column; gap: 0.4rem;
}
.econ-jobs__wrap { overflow-x: auto; }
.econ-jobs__table {
  width: 100%; border-collapse: collapse; font-size: var(--fs-dense, var(--fs-sm));
}
.econ-jobs__table th {
  text-align: left;
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border-soft);
}
.econ-jobs__table td {
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border-hairline);
  color: var(--text);
  vertical-align: middle;
}
.econ-jobs__table tr:last-child td { border-bottom: none; }
.econ-jobs__table tr:hover td      { background: var(--surface-1); }
.econ-jobs__dot {
  display: inline-block;
  width: var(--size-xs); height: var(--size-xs); border-radius: var(--radius-full);
  margin-right: 0.4rem;
  vertical-align: middle;
  background: var(--accent, var(--text-dim));
}
.econ-jobs__qty    { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.econ-jobs__payout { font-weight: var(--fw-bold); color: var(--red); white-space: nowrap; text-align: right; }
.econ-jobs__table th.econ-jobs__th--num { text-align: right; }
.econ-jobs__timed  { font-size: var(--fs-xs); opacity: 0.7; padding: 0 0.3rem; border: 1px solid currentColor; border-radius: var(--radius-sm); color: var(--text-dim); }
.econ-jobs__ambig  { color: var(--text-muted); font-style: italic; cursor: help; border-bottom: 1px dotted var(--text-dim); }

.econ-link {
  background: transparent; border: none; padding: 0;
  color: var(--text); cursor: pointer;
  text-align: left; font: inherit;
}
.econ-link:hover { color: var(--red); text-decoration: underline; }

.econ-hint {
  margin: 0;
  padding: 0.6rem 0.85rem;
  background: var(--surface-1);
  border-left: 3px solid color-mix(in srgb, var(--red) 55%, transparent);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.econ-hint strong { color: var(--text); }

.econ-empty {
  margin: 0;
  padding: 0.8rem 0.5rem;
  font-size: var(--fs-sm);
  color: var(--text-dim);
  font-style: italic;
}

.econ-filters {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.75rem 0.85rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
}
.econ-filters__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
}
.econ-filters__row--sort { justify-content: flex-end; }
.econ-filters__field {
  display: flex; align-items: center; gap: 0.4rem;
  font-size: var(--fs-sm); color: var(--text-muted);
}
.econ-filters__clear {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.4rem 0.95rem;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  cursor: pointer;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
  border-radius: var(--radius-pill);
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  box-shadow: var(--shadow-button), var(--shadow-highlight-top);
  transition: background var(--motion-base), border-color var(--motion-base), color var(--motion-base), box-shadow var(--motion-base), transform var(--motion-fast);
  margin-left: auto;
}
.econ-filters__clear:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
  color: var(--text);
}
.econ-filters__clear:active { transform: scale(0.97); }

.econ-chips {
  display: flex; flex-wrap: wrap; gap: 0.3rem;
  flex: 1;
}
.econ-chip {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
}
.econ-chip:hover { color: var(--text); }
.econ-chip.is-active {
  background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
  color: var(--accent, var(--text));
  font-weight: var(--fw-bold);
}
.econ-chip.is-active:hover {
  background: color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
}
.econ-chip__dot {
  width: var(--size-xs); height: var(--size-xs); border-radius: var(--radius-full);
  flex-shrink: 0;
  background: var(--accent, var(--text-dim));
}

.econ-toggle {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
  cursor: pointer;
  user-select: none;
}
.econ-toggle input[type="checkbox"] {
  appearance: none;
  width: var(--size-4xl); height: var(--size-xl);
  background: var(--surface-3);
  border: 1px solid var(--border-emphasis);
  border-radius: var(--radius-pill);
  position: relative;
  cursor: pointer;
  transition: background var(--motion-base), border-color var(--motion-base);
}
.econ-toggle input[type="checkbox"]::after {
  content: '';
  position: absolute;
  top: 1px; left: 1px;
  width: var(--size-md); height: var(--size-md);
  background: var(--text-muted);
  border-radius: var(--radius-full);
  transition: transform var(--motion-base), background var(--motion-base);
}
.econ-toggle input[type="checkbox"]:checked {
  background: color-mix(in srgb, var(--red) 50%, transparent);
  border-color: color-mix(in srgb, var(--red) 70%, transparent);
}
.econ-toggle input[type="checkbox"]:checked::after {
  transform: translateX(14px);
  background: var(--text);
}

.econ-sort-dir {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-4xl); height: var(--size-4xl);
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
  border-radius: var(--radius-pill);
  cursor: pointer;
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  box-shadow: var(--shadow-button), var(--shadow-highlight-top);
  transition: background var(--motion-base), border-color var(--motion-base), color var(--motion-base), box-shadow var(--motion-base), transform var(--motion-fast);
}
.econ-sort-dir:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
  color: var(--text);
}
.econ-sort-dir:active { transform: scale(0.92); }
.econ-sort-dir .icon { width: var(--size-md); height: var(--size-md); }

/* ============================================================
   Garage
   ============================================================ */

.garage-page {
  max-width: var(--max-w-content);
  margin: 0 auto;
  padding: 2.5rem 2rem 4rem;
}
.garage-page__header { margin-bottom: 2rem; }
.garage-page__title {
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-xl), 4vw, var(--fs-2xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
  margin: 0;
}
.garage-page__sub { margin: 0.5rem 0 0; color: var(--text-muted); font-size: var(--fs-base); }
.garage-section { margin-top: 2.5rem; }
.garage-section:first-of-type { margin-top: 0; }
.garage-section__header { margin-bottom: 1rem; }
.garage-section__title {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  color: var(--text);
  margin: 0;
}
.garage-section__sub {
  margin: 0.25rem 0 0;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}
/* Hub cards (Staff + Garage) reuse the home .section-card component
   (see .section-card around the home section-cards block). */

/* --- Garage / Leaderboard page --- */

.garage-leaderboard {
  max-width: var(--max-w-content);
  margin: 0 auto;
  padding: 2.5rem 2rem 4rem;
}
.garage-leaderboard__header { margin-bottom: 1.5rem; }
.garage-leaderboard__title {
  font-family: var(--font-display, var(--font));
  font-size: clamp(var(--fs-xl), 4vw, var(--fs-2xl));
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  line-height: var(--lh-tight);
  margin: 0;
}
.garage-leaderboard__sub  { margin: 0.5rem 0 0; color: var(--text-muted); font-size: var(--fs-base); }
.garage-leaderboard__error {
  --accent: var(--red);
  margin-bottom: 1rem;
}

/* Sub-classed table -- shares the thead typography idiom with .sortable-
   table (capitalised, muted, narrow) but doesn't carry the click-to-sort
   interaction. Standalone class keeps the leaderboard simple to evolve
   without touching the dense statistics tables elsewhere. */
.leaderboard-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.leaderboard-table thead th {
  text-align: left;
  padding: 0.5rem 0.8rem;
  border-bottom: 1px solid var(--border-soft);
}
.leaderboard-table tbody td {
  padding: 0.55rem 0.8rem;
  border-bottom: 1px solid var(--border-hairline);
  color: var(--text);
}
.leaderboard-table tbody tr:last-child td { border-bottom: none; }
.leaderboard-table__rank { width: 3rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* .prod-demand__num joined 2026-07-18 (production-event demand table) --
   same right-aligned tabular number cell, one source. */
.leaderboard-table__num,
.prod-demand__num  { text-align: right; font-variant-numeric: tabular-nums; }
/* Combined with .sortable-table the sortable base forces thead th to
   text-align:left + tighter padding at higher specificity -- same
   header/cell mismatch documented at .staff-table.sortable-table. Pin the
   number headers right and match the header padding to the cells. */
.sortable-table thead th.leaderboard-table__num { text-align: right; }
.leaderboard-table.sortable-table thead th { padding: 0.5rem 0.8rem; }
.leaderboard-name {
  color: var(--text);
  font-weight: var(--fw-medium);
}
.leaderboard-name:hover { color: var(--red); }
.leaderboard-name--unlinked { color: var(--text-muted); font-weight: var(--fw-regular); }

/* --- Garage / Tow page --- */

.assist-page {
  max-width: 720px;
  margin: 0 auto;
  padding: 1.5rem 2rem 3rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.assist-page__panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}
.assist-page__header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
.assist-page__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-xl);
  font-weight: var(--fw-hero);
  letter-spacing: 0.01em;
  margin: 0;
}
.assist-page__banner {
  --accent: var(--status-orange);
}
.assist-page__login-hint {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
}
.assist-page__login-hint a { color: color-mix(in srgb, var(--brand-steam) 60%, white); }

.assist-page__list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  max-height: 70vh;
  overflow-y: auto;
  /* Reserve scrollbar gutter on the inner scroll container so the list
     doesn't reflow when items appear or disappear. */
  scrollbar-gutter: stable;
}

.assist-card {
  display: flex;
  flex-direction: column;
  /* The list caps at 70vh; without this the card (a flex item whose
     overflow:hidden zeroes its automatic minimum size) gets compressed to
     the cap instead of overflowing it, clipping the expanded drawer and
     leaving the list with nothing to scroll. */
  flex-shrink: 0;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--accent, var(--text-dim));
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: background var(--motion-base);
}
.assist-card--mine { background: color-mix(in srgb, var(--accent, var(--text-dim)) 4%, var(--surface-1)); }
.assist-card--accepted { border-left-color: color-mix(in srgb, var(--accent, var(--text-dim)) 60%, var(--text)); }
.assist-card--disconnected { opacity: 0.7; }

/* Header is the clickable area that expands the drawer. Styled like
   the rest of the card (no chrome) so the click affordance comes from
   the caret + hover background, not button visuals. */
.assist-card__header {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.8rem 1rem;
  background: transparent;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
  transition: background var(--motion-base);
}
.assist-card__header:hover { background: var(--surface-2); }
.assist-card__caret {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform var(--motion-base);
}
.assist-card--expanded .assist-card__caret { transform: rotate(180deg); }

.assist-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--size-4xl);
  height: var(--size-4xl);
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--text);
  flex-shrink: 0;
}
.assist-card__icon .icon { width: var(--size-lg); height: var(--size-lg); }
.assist-card__body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.assist-card__head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: var(--fs-sm);
}
.assist-card__name { color: var(--text); font-weight: var(--fw-bold); }
/* In-game join code ("#17"), the id a helper types as "!join17". */
.assist-card__code { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.assist-card__vehicle { color: var(--text-muted); }
/* Service chip on merged dispatch-center rows (Tow/Fuel/...). Chrome from
   the Tag-pill family; one neutral recipe for every service -- the assist
   family shares one tone by design (--tow-marker), so the LABEL is the
   differentiator, not a colour. */
.assist-card__service {
  background: color-mix(in srgb, var(--tow-marker) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--tow-marker) 40%, transparent);
  color: color-mix(in srgb, var(--tow-marker) 60%, white);
}
/* Center-page chrome: the shared service filter row above the list, the
   type tabs inside the create panel, and the police cross-link section. */
.assist-page__service-filter { margin-bottom: 0.75rem; }
.assist-create-panel__services { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.assist-page__police { margin-top: 1.5rem; }
/* Both rows render ICON pills (service glyphs instead of text; the label
   lives on aria-label/title). Larger padding + icon than the text ranges so
   the glyphs breathe and stay comfortable touch targets. */
.assist-page__service-filter .stats-range,
.assist-create-panel__services .stats-range {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.4rem 0.7rem;
}
.assist-page__service-filter .stats-range .icon,
.assist-create-panel__services .stats-range .icon {
  width: 1.2rem;
  height: 1.2rem;
}
.assist-card__age {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.assist-card__status {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--accent);
}
.assist-card__note {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* Drawer: the expanded detail panel below the header. Renders the
   full note, timestamps, helper info, and the action buttons.
   Accordion -- only one drawer open at a time, controlled by JS. */
.assist-card__drawer {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding: 0.2rem 1rem 0.9rem;
  border-top: 1px solid var(--border-hairline);
}
.assist-card__drawer-meta {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.assist-card__drawer-row {
  display: flex;
  gap: 0.6rem;
  font-size: var(--fs-sm);
  color: var(--text);
}
.assist-card__drawer-label {
  min-width: 4.5rem;
  color: var(--text-muted);
  font-weight: var(--fw-medium);
}
.assist-card__drawer-value { color: var(--text); }
.assist-card__drawer-row--note {
  display: block;
  color: var(--text-muted);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.assist-card__drawer-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  align-items: stretch;
}
.assist-card__btn {
  padding: 0.45rem 0.8rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  border: 1px solid var(--border-soft);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
  min-height: 32px;
  transition: background var(--motion-base), border-color var(--motion-base);
}
.assist-card__btn:hover { background: var(--surface-3); border-color: var(--border-strong); }
.assist-card__btn[disabled] { opacity: 0.5; cursor: not-allowed; filter: grayscale(0.4); }
.assist-card__btn--accept {
  --accent: var(--status-green);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.assist-card__btn--done {
  --accent: var(--status-green);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.assist-card__btn--cancel {
  --accent: var(--red);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.assist-card__error {
  --accent: var(--red);
  padding: 0.4rem 1rem 0.7rem;
  font-size: var(--fs-xs);
  color: var(--accent);
}

/* "Show on map" button is an anchor; layout same as the action
   buttons but with an inline icon. */
.assist-card__btn--map {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-decoration: none;
}

/* "Who helped?" picker -- inline panel revealed in the drawer when the
   requester presses Done (assist-app.js builds it imperatively). Lets them
   credit helpers who never joined: a locked-checked roster group on top, then
   a scrollable list of online players (ranked by distance) with a search box. */
.assist-done {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  background: var(--surface-1);
}
.assist-done__heading {
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--text);
}
.assist-done__group-label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
}
.assist-done__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.assist-done__candidates {
  max-height: 12rem;
  overflow-y: auto;
}
.assist-done__item label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 36px;
  padding: 0.4rem 0.45rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text);
  cursor: pointer;
}
.assist-done__item label:hover { background: var(--surface-2); }
.assist-done__item input { accent-color: var(--status-green); }
/* Tint a row the requester picked so several selections stay obvious at a
   glance -- the native checkbox tick alone is easy to miss in a long list. */
.assist-done__item:has(.assist-done__pick:checked) label {
  background: color-mix(in srgb, var(--status-green) 14%, transparent);
}
.assist-done__item--locked label { color: var(--text-muted); cursor: default; }
.assist-done__item--locked label:hover { background: transparent; }
.assist-done__search { width: 100%; }
.assist-done__loading,
.assist-done__empty {
  padding: 0.3rem 0;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.assist-done__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-top: 0.2rem;
}
.assist-done__error {
  --accent: var(--red);
  font-size: var(--fs-xs);
  color: var(--accent);
}

/* --- Modal (native <dialog>) --- */

/* Inline create-panel that opens directly under the "Send distress
   call" CTA. Replaces the previous centred dialog overlay. Same
   internal layout as the modal had -- the surface chrome is the only
   thing that changed (no backdrop, no centring, integrates with the
   page flow). */
.assist-create-panel {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: 1rem 1.2rem;
}
.assist-create-panel__form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.assist-create-panel__reasons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}
.assist-reason-radio {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-soft);
  background: var(--surface-1);
  cursor: pointer;
  min-height: 44px;
  transition: background var(--motion-base), border-color var(--motion-base);
}
/* Per-reason accent. Set via data-reason="<reason>" on anything that should
   tint with the reason colour (radio buttons in the create panel,
   assist-card list items, /map sidebar activity rows). EVERY reason across
   all assist services needs an entry here -- .assist-card__icon paints
   background: var(--accent), and a reason without a rule leaves
   var(--accent) unset (no circle at all).
   Tow keeps the :root --tow-reason-* aliases; the indirection stays so a
   future diversification is a token-only change. */
[data-reason="stuck"]        { --accent: var(--tow-reason-stuck); }
[data-reason="flipped"]      { --accent: var(--tow-reason-flipped); }
[data-reason="out_of_fuel"]  { --accent: var(--tow-reason-out-of-fuel); }
/* Fuel-service reasons (gasoline/diesel). Reuse existing status tokens as a
   green/amber mnemonic so the swatch reads correctly without new :root hex. */
[data-reason="gasoline"]     { --accent: var(--status-green); }
[data-reason="diesel"]       { --accent: var(--status-orange); }
/* Fire reasons burn in the map's fire tone; water borrows the cool
   "extinguished" blue (same family, opposite mnemonic); escort reads as
   construction-sign gold. */
[data-reason="road_hazard"]  { --accent: var(--fire); }
[data-reason="wildfire"]     { --accent: var(--fire); }
[data-reason="water"]        { --accent: var(--fire-extinguished); }
[data-reason="oversize"]     { --accent: var(--status-yellow); }
[data-reason="slow_vehicle"] { --accent: var(--status-yellow); }
/* Taxi rides in the route blue -- the "travel from A to B" colour the map
   already speaks -- keeping it apart from escort's construction gold. */
[data-reason="ride"]         { --accent: var(--route); }
.assist-reason-radio:hover { background: var(--surface-2); border-color: var(--border-strong); }
.assist-reason-radio input { accent-color: var(--accent); }
.assist-reason-radio--checked {
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-1));
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.assist-reason-radio__swatch {
  display: inline-block;
  width: var(--size-sm);
  height: var(--size-sm);
  border-radius: var(--radius-full);
  background: var(--accent);
  flex-shrink: 0;
}
.assist-reason-radio__label { font-size: var(--fs-sm); color: var(--text); }
.assist-create-panel__field { display: flex; flex-direction: column; gap: 0.35rem; }
.assist-create-panel__textarea {
  width: 100%;
  min-height: 80px;
  resize: vertical;
}
.assist-create-panel__counter {
  align-self: flex-end;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.assist-create-panel__counter--warn { --accent: var(--status-orange); }
/* Destination picker (taxi): embedded base map + helper hint. Fixed height --
   the Leaflet container needs a real box before invalidateSize can size it. */
.assist-create-panel__dest-map {
  height: 320px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  overflow: hidden;
}
.assist-create-panel__hint {
  font-size: var(--fs-xs);
  color: var(--text-dim);
}
.assist-create-panel__error {
  --accent: var(--red);
  font-size: var(--fs-sm);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-radius: var(--radius-md);
  padding: 0.5rem 0.7rem;
}
.assist-create-panel__buttons { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 0.5rem; }

/* Pending-ratings strip above the tow list. One card per ratable tow; the
   three smiley choices map to happy/neutral/sad (green/neutral/red). Colour
   comes from a data-rate-value selector family, never inline (CSP). */
.assist-ratings {
  margin-bottom: 1.2rem;
}
.assist-ratings__title {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin: 0 0 0.6rem;
}
.assist-ratings__list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.assist-rating {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
  padding: 0.6rem 0.9rem;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
}
.assist-rating__who { color: var(--text); }
.assist-rating__choices {
  display: flex;
  gap: 0.4rem;
}
.assist-rating__choice {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  cursor: pointer;
  /* Each choice carries its own semantic colour via --smiley (set per value
     below). The icon shows it at rest so happy/neutral/sad read at a glance;
     hover just tints the background + border with the same colour. */
  color: var(--smiley);
}
/* The choice is a standalone tap target, not an inline glyph, so the smiley
   overrides the 1em .icon default -- at 1em the face detail is unreadable
   (only the outer circle shows). 1.75rem makes happy/neutral/sad distinct. */
.assist-rating__choice .icon {
  width: 1.75rem;
  height: 1.75rem;
}
.assist-rating__choice:hover {
  border-color: var(--smiley);
  background: color-mix(in srgb, var(--smiley) 14%, var(--surface-2));
}
.assist-rating__choice:disabled {
  opacity: 0.5;
  cursor: default;
}
/* Semantic colour per rating value (green / amber / red), shown at rest. */
.assist-rating__choice[data-rate-value="happy"]   { --smiley: var(--status-green);  }
.assist-rating__choice[data-rate-value="neutral"] { --smiley: var(--status-orange); }
.assist-rating__choice[data-rate-value="sad"]     { --smiley: var(--red);           }

/* ----- KPI strip + history stream (shared by /dispatch/tow + /dispatch/police) -----
   Markup lives in views/partials/kpi-strip.eta + history-stream.eta; the
   classes here are shared so the two garage surfaces never drift. The KPI
   strip is a three-card grid driven by ONE range pill group, reusing the
   statistics-page .stats-card + .stats-range-group tokens. */
.kpi-strip {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.kpi-strip__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
}
.kpi-strip__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
  margin: 0;
}
.kpi-strip__sub {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  font-weight: var(--fw-regular);
}
.kpi-strip__head .stats-range-group { margin-left: auto; }
/* Override the statistics-page card auto-fit minmax so three KPIs stay
   on one row at the garage content width (720 px max). Auto-fit with a
   150-px minmax would let a fourth empty column hint appear. */
.kpi-strip__grid { grid-template-columns: repeat(3, 1fr); }

/* ----- History stream -----
   Event log shown below the active list, with a compact row design
   optimized for chronological scanning rather than action. */
.history-stream {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 1.5rem;
}
.history-stream__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.history-stream__title {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: 0.01em;
  margin: 0;
}
.history-stream__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-1);
  overflow: hidden;
}
.stream-event {
  display: flex;
  gap: 0.75rem;
  padding: 0.55rem 0.9rem;
  border-bottom: 1px solid var(--border-hairline);
  font-size: var(--fs-sm);
  align-items: baseline;
}
.stream-event:last-child { border-bottom: none; }
.stream-event__time {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  min-width: 11ch;
}
.stream-event__body {
  flex: 1 1 auto;
  color: var(--text);
  min-width: 0;
}
.stream-event__actor,
.stream-event__admin {
  font-weight: var(--fw-bold);
  color: var(--text);
}
/* When the actor/admin name resolves to a publicId, it becomes a link.
   Use the same subdued underline as other inline residents-links. */
a.stream-event__actor:hover,
a.stream-event__admin:hover {
  color: color-mix(in srgb, var(--accent, var(--brand-steam)) 70%, white);
  text-decoration: underline;
}
/* In-game join code ("#17") on a live "created" feed row, the id a helper types
   as "!join17". Mirrors .assist-card__code (muted, tabular figures); only the
   live CB-radio feed sets the showJoinCode opt, never the /radio history. */
.stream-event__code {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.stream-event--system {
  color: var(--text-muted);
  font-style: italic;
}
.stream-event--system .stream-event__body { color: var(--text-muted); }

/* Category icon (left column). Colour comes from --accent, set per row
   via [data-tone] -- direction at a glance. The tone set is the union of
   the surfaces (tow: good/bad/alert/system; police: good/bad/police/
   neutral; ratings: rating); each surface uses only its subset. Same
   mechanism as the resident-timeline icon (currentColor <- --accent). */
.stream-event__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  align-self: center;
  color: var(--accent, var(--text-muted));
}
.stream-event__icon svg { width: 1.05rem; height: 1.05rem; }
.stream-event__icon[data-tone="good"]    { --accent: var(--status-green); }
.stream-event__icon[data-tone="bad"]     { --accent: var(--status-red); }
.stream-event__icon[data-tone="alert"]   { --accent: var(--status-yellow); }
.stream-event__icon[data-tone="system"]  { --accent: var(--text-muted); }
.stream-event__icon[data-tone="police"]  { --accent: var(--role-police); }
.stream-event__icon[data-tone="neutral"] { --accent: var(--text-muted); }
/* Ratings reuse the profile reputation-star colour (--status-yellow) so the
   star reads the same in the feed as on /residents. */
.stream-event__icon[data-tone="rating"]  { --accent: var(--status-yellow); }

/* Trailing "jump to" links (map + action). Body is flex:1 so the group sits
   at the row's right edge. Look + hover/focus glow come from the shared
   .icon-btn; this only groups the links and tightens them for the dense row
   (same approach as .resident-tile__map). */
.stream-event__links {
  flex: 0 0 auto;
  align-self: center;
  display: inline-flex;
  gap: 0.1rem;
}
.stream-event__link { width: 1.6rem; height: 1.6rem; }
.stream-event__link .icon { width: 1rem; height: 1rem; }

.history-stream__filters { align-self: flex-start; }

/* CB History page (/radio): the full paginated archive. Reuses the shared
   .history-stream__list / .stream-event row styling; only page layout lives
   here. The "View full history" link sits on the live surfaces (nav widget +
   popout) and points here. */
.radio-history__filters,
.radio-history__list,
.radio-history__foot { max-width: 760px; }
.radio-history__filters { margin-bottom: 1rem; }
.radio-history__foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1rem;
}
/* Footer action on the live CB-radio surfaces (nav widget + popout): the
   standard .btn, centred and separated from the feed by a hairline. Points at
   the full CB History page (/radio). */
.cb-radio__foot {
  display: flex;
  justify-content: center;
  padding: 0.6rem;
  border-top: 1px solid var(--border-hairline);
}

.history-stream__pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.history-stream__page-info {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* Caret rotation for prev/next buttons. The shared #icon-caret-down
   asset points down; rotation makes it left/right. CSS classes (not
   inline style) so the strict CSP holds (no style-src 'unsafe-inline'). */
.history-stream__caret--prev { transform: rotate(90deg); }
.history-stream__caret--next { transform: rotate(-90deg); }

/* --- Tinted-text utility ---
   Single source of truth for the "text on tinted surface" formula
   (35% mix of the source tone with white, for legible foreground over a
   coloured-12-to-18% background). Consumers set --accent on their own class;
   the formula stays here so the colour math never drifts again.
   New components with the same idiom get added to this selector list,
   they do NOT re-declare the color rule. Pattern grew from one badge
   variant pre-step-6 to a family of seven status surfaces -- past the
   pattern-check 3+ threshold it earns one canonical declaration. */
.badge--discord:hover,
.assist-card__btn--accept,
.assist-card__btn--done,
.assist-card__btn--cancel,
.assist-card__error,
.assist-create-panel__counter--warn,
.assist-create-panel__error {
  color: color-mix(in srgb, var(--accent) 35%, white);
}


/* ============================================================
   Shared modal chrome.
   Two modals (staff edit, off-duty confirm) share the same dark
   surface + scrim backdrop layout. The tow and police-shift create
   flows used to be in this group too, but have moved to inline panels;
   the Discord seed wizard was removed with the roster sync redesign.
   ============================================================ */

.staff-roster-modal,
.police-offduty-modal {
  border: none;
  padding: 0;
  background: transparent;
  color: var(--text);
  max-width: 480px;
  width: calc(100% - 2rem);
}
.staff-roster-modal::backdrop,
.police-offduty-modal::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.staff-roster-modal__form,
.police-offduty-modal__form {
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: 1.5rem 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  box-shadow: var(--shadow-modal);
}
.staff-roster-modal__field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.staff-roster-modal__title,
.police-offduty-modal__form h2 {
  font-family: var(--font-display, var(--font));
  font-size: var(--fs-lg);
  font-weight: var(--fw-hero);
  margin: 0;
}
.staff-roster-modal__error,
.garage-police__own-shift-error {
  color: var(--red);
  font-size: var(--fs-sm);
}
.staff-roster-modal__buttons,
.police-offduty-modal__buttons {
  display: flex;
  justify-content: flex-end;
  gap: 0.6rem;
  margin-top: 0.5rem;
}

/* Inline start-shift panel: opens under the "Start a shift" button
   when the officer clicks it. Same chrome as the tow create-panel
   (consistency across the two CTAs); the form layout mirrors what
   the dialog used to render. */
.police-start-panel {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: 1rem 1.2rem;
  margin-top: 0.6rem;
}
.police-start-panel__form {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}
.police-start-panel__field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.police-start-panel__error {
  color: var(--red);
  font-size: var(--fs-sm);
}
.police-start-panel__buttons {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.6rem;
  margin-top: 0.2rem;
}

/* /staff/police-roster page layout. */
.staff-roster {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding: 1rem 0;
}
.staff-roster__header {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.staff-roster__title {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: var(--fw-hero);
}
.staff-roster__section-title,
.staff-roster__queue-title {
  font-size: var(--fs-lg);
  margin: 0 0 0.5rem;
}
.staff-roster__hint {
  margin: 0 0 0.6rem;
}
.staff-roster__intake-list,
.staff-roster__rank-list,
.staff-roster__audit-list,
.staff-roster__queue-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.staff-roster__intake-tile,
.staff-roster__row,
.staff-roster__queue-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
}
.staff-roster__intake-tile:hover,
.staff-roster__row:hover {
  background: var(--surface-3);
}
.staff-roster__badge {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  min-width: 3ch;
}
.staff-roster__name {
  flex: 1;
  font-weight: var(--fw-bold);
  text-decoration: none;
  color: var(--text);
}
.staff-roster__name:hover { color: var(--role-police); }
.staff-roster__rank {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.staff-roster__date {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-family: var(--font-mono);
}
.staff-roster__online-dot {
  font-size: var(--fs-xs);
  color: var(--status-green);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
/* /staff/admins and /staff/police-roster tables. Builds on
   .sortable-table (font-size baseline + sort-indicator chrome) and
   the existing .resident-avatar pattern for the identity cell. */
.staff-table { width: 100%; border-collapse: collapse; }
.staff-table th,
.staff-table td {
  padding: 0.55rem 0.75rem;
  vertical-align: top;
  border-bottom: 1px solid var(--border-hairline);
}
.staff-table tbody tr:last-child td { border-bottom: none; }
.staff-table thead th {
  text-align: left;
}
.staff-table__actions-col   { width: 1px; white-space: nowrap; }
.staff-table__actions-cell  { white-space: nowrap; text-align: right; }
/* Identity cell: avatar floats left, nickname + steamid stack right.
   The flex layout lives on the inner wrapper (.staff-table__identity),
   NOT on the <td> -- setting display:flex directly on a <td> opts the
   cell out of table-row height sync, which leaves the row's
   border-bottom hairline a sub-pixel out of step with its siblings and
   shows up as a broken/offset divider. Other tables in the codebase
   (housing, economy) follow the same wrapper-div pattern.
   The --no-avatar variant drops the avatar gap (used on
   /staff/police-roster where character-name + steamid is shown
   without an avatar -- player identity is intentionally character-
   centric there). */
.staff-table__identity {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-width: 0;
}
.staff-table__identity--no-avatar { gap: 0; }
/* Rank pill: small token next to the rank column on the main roster
   table. Reuses --radius-pill + muted surface like other section pills. */
.staff-table__rank-pill {
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.staff-table__badge-cell {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  white-space: nowrap;
}
.staff-table__identity-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.2;
}
.staff-table__name {
  font-weight: var(--fw-bold);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.staff-table__name--unknown { font-weight: var(--fw-regular); font-style: italic; color: var(--text-muted); }
.staff-table__steamid {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-family: var(--font-mono);
}
.staff-table__added-date  { color: var(--text); }
.staff-table__added-by    { font-size: var(--fs-xs); color: var(--text-muted); }
.staff-table__note-cell   { color: var(--text-muted); }
.staff-table__when-cell   { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; }
/* Two-line action cell on /staff/police-roster audit log: a readable
   label on top, an optional plain-English description ("Officer -> PPO;
   Badge: 042 -> 007") below in muted small text. Built from the
   action + before/after JSON in staff-police-roster-app.js. */
.staff-table__action-cell        { font-size: var(--fs-sm); color: var(--text); }
.staff-roster__audit-label       { font-weight: var(--fw-medium); }
.staff-roster__audit-desc {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 0.15rem;
}
.staff-table__sentinel {
  /* Ghost variant on purpose: the "no value" placeholder must not
     read as a real badge, so it stays unfilled. */
  background: transparent;
  font-family: var(--font-mono);
}

/* Add-form panel -- inline section inside the Active section's head,
   appears in flow above the table when toggled. */
.staff-admins-add-panel { margin-bottom: 1rem; padding: 1rem 1.25rem; background: var(--surface-2); border-radius: var(--radius-md); border: 1px solid var(--border-hairline); }
.staff-admins-add-form { display: flex; flex-direction: column; gap: 0.75rem; }
.staff-admins-add-form__field        { display: flex; flex-direction: column; gap: 0.25rem; font-size: var(--fs-sm); color: var(--text-muted); }
.staff-admins-add-form__preview      { padding: 0.5rem 0.75rem; background: var(--surface-1); border-radius: var(--radius-md); font-size: var(--fs-sm); }
.staff-admins-add-form__error        { padding: 0.5rem 0.75rem; background: var(--surface-1); border-left: 3px solid var(--status-red); color: var(--text); font-size: var(--fs-sm); }
.staff-admins-add-form__actions      { display: flex; gap: 0.5rem; justify-content: flex-end; }

/* Manual-tag panel + result list on /staff/cheaters. Mirrors the staff-admins
   add-panel surface treatment (same tokens, kept parallel for an easy later
   merge if a shared inline-add panel is ever extracted). */
.staff-cheaters-add { margin-bottom: 1rem; padding: 1rem 1.25rem; background: var(--surface-2); border-radius: var(--radius-md); border: 1px solid var(--border-hairline); }
.staff-cheaters-add__field        { display: flex; flex-direction: column; gap: 0.25rem; font-size: var(--fs-sm); color: var(--text-muted); }
.staff-cheaters-add__results      { list-style: none; margin: 0.75rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.staff-cheaters-add__result       { display: flex; align-items: center; gap: 0.75rem; padding: 0.4rem 0.6rem; background: var(--surface-1); border-radius: var(--radius-md); }
.staff-cheaters-add__result-name  { font-weight: var(--fw-medium); color: var(--text); margin-right: auto; }
.staff-cheaters-add__result-hours { font-size: var(--fs-xs); color: var(--text-muted); }
.staff-cheaters-add__empty        { color: var(--text-muted); font-size: var(--fs-sm); font-style: italic; }
.staff-cheaters-add__error        { margin-top: 0.5rem; padding: 0.5rem 0.75rem; background: var(--surface-1); border-left: 3px solid var(--status-red); color: var(--text); font-size: var(--fs-sm); }
.staff-table__num                 { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Right-align the numeric header too -- .sortable-table thead th forces left at
   higher specificity, so override it for the number column. */
.sortable-table thead th.staff-table__num { text-align: right; }
/* ...and give the header the SAME horizontal padding as the cells: combining
   .staff-table with .sortable-table otherwise leaves the header on the sortable
   base's tighter padding (0.6rem) while cells keep 0.75rem, so a right-aligned
   number column ends up offset by that 0.15rem. Match them here. */
.staff-table.sortable-table thead th { padding: 0.55rem 0.75rem; }
.staff-table__check-col           { width: 1%; white-space: nowrap; text-align: center; }
.staff-cheaters-bulk              { display: flex; align-items: center; justify-content: flex-end; gap: 0.75rem; margin-top: 0.75rem; }
.staff-cheaters-bulk__count       { font-size: var(--fs-sm); color: var(--text-muted); }
.staff-cheaters-cleared > summary { cursor: pointer; }

.staff-roster__rank-block { margin-bottom: 1rem; }
.staff-roster__rank-title {
  font-size: var(--fs-base);
  margin: 0 0 0.4rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.staff-roster__audit-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.6rem;
  font-size: var(--fs-sm);
  border-bottom: 1px solid var(--border-soft);
}
.staff-roster__actor {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  text-decoration: none;
  color: var(--text);
}
.staff-roster__actor--admin:hover { color: var(--role-police); }
.staff-roster__actor--system {
  color: var(--text-muted);
  font-style: italic;
}
.staff-roster__audit-action,
.staff-roster__audit-ts {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

/* Section-header icon: sits left of the section title, muted-police
   tint so it reads as decorative chrome rather than an action. The
   title wrapper is the .stats-section__title h2 (inline-flex via the
   icon's own alignment -- the .icon base class is vertical-aligned). */
.staff-roster__section-icon {
  width: 1.05em;
  height: 1.05em;
  color: var(--role-police);
  margin-right: 0.35rem;
  vertical-align: -0.18em;
}

/* Inline "lost the police role" chip, shown next to the name in the
   identity cell. Two states: un-acked (red, needs attention) and
   acked (muted, operator already saw it). */
.staff-roster__flag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin-left: 0.5rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  vertical-align: middle;
}
.staff-roster__flag .icon { width: 0.9em; height: 0.9em; }
.staff-roster__flag--lost {
  color: color-mix(in srgb, var(--red) 75%, white);
  background: color-mix(in srgb, var(--red) 14%, transparent);
}
.staff-roster__flag--acked {
  color: var(--text-muted);
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
}

/* Flagged row: a faint red left border + tint so the whole row reads
   as "needs attention" even before the eye reaches the chip. */
.staff-table__row--flagged > td:first-child {
  box-shadow: inset 3px 0 0 0 color-mix(in srgb, var(--red) 60%, transparent);
}
.staff-table__row--flagged {
  background: color-mix(in srgb, var(--red) 5%, transparent);
}

/* Rank filter bar above the active-officers table. Reuses the project-
   wide .stats-toggle-group / .stats-toggle pattern (residents-page,
   statistics datasets) so all filter pills look the same across the
   app. Only the per-pill --accent token is set here -- everything else
   (layout, hover, on-state) inherits from the shared base. "All" maps
   to status-green by UI choice (mirrors the residents filter). */
.staff-roster-active-filter { margin-bottom: 0.75rem; }
.staff-roster-active-filter .stats-toggle[data-rank-filter="*"] {
  --accent: var(--status-green);
}
.staff-roster-active-filter .stats-toggle[data-rank-filter="chief"],
.staff-roster-active-filter .stats-toggle[data-rank-filter="asst_chief"],
.staff-roster-active-filter .stats-toggle[data-rank-filter="instructor"],
.staff-roster-active-filter .stats-toggle[data-rank-filter="sergeant"],
.staff-roster-active-filter .stats-toggle[data-rank-filter="officer"] {
  --accent: var(--role-police);
}

/* Both roster tables share these column widths (via <colgroup>) so
   Rank / Player / Badge / Actions line up vertically across the
   Active and Candidates sections. Without fixed widths the browser's
   auto-layout would size each table independently and the columns
   would drift apart. */
.staff-roster-table__col--rank    { width: 9rem;  }
.staff-roster-table__col--player  { width: auto;  }   /* flex */
.staff-roster-table__col--badge   { width: 6rem;  }
.staff-roster-table__col--actions { width: 16rem; }

/* Candidates is a native <details>: the <summary> carries the section
   heading. We swap the default disclosure triangle for a chevron that
   rotates on open, and align the summary content like other section
   heads. The whole strip is clickable. */
.staff-roster-collapsible > summary {
  list-style: none;       /* drop the default marker (Firefox/Chromium) */
  cursor: pointer;
}
.staff-roster-collapsible > summary::-webkit-details-marker {
  display: none;          /* drop the Safari/Chromium native marker */
}
.staff-roster-collapsible__summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.staff-roster-collapsible__summary::after {
  content: '';
  width: 0.65em;
  height: 0.65em;
  margin-left: auto;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg);   /* points right when collapsed */
  transition: transform var(--motion-base);
}
.staff-roster-collapsible[open] > .staff-roster-collapsible__summary::after {
  transform: rotate(45deg);    /* points down when open */
}

/* Inline edit panel: an expandable row beneath a roster entry, opened
   by the row's "Edit" button. Replaces the old centred <dialog> for the
   staff roster (user-requested). The .staff-roster-edit__row is the
   <tr> wrapper; .staff-roster-edit is the flex strip inside its single
   full-width <td>. */
.staff-roster-edit__row > td {
  padding: 0;
  border-bottom: 1px solid var(--border-soft);
}
.staff-roster-edit {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.9rem;
  padding: 0.8rem 1rem;
  background: var(--surface-1);
}
.staff-roster-edit__field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.staff-roster-edit__rank,
.staff-roster-edit__badge {
  /* inherits the global dark form-element reset; keep widths sane */
  min-width: 8rem;
}
.staff-roster-edit__badge { min-width: 6rem; }
.staff-roster-edit__badge-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.staff-roster-edit__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;   /* push Save/Cancel/Revoke to the right edge */
}
/* Revoke sits visually apart (destructive) + has an "armed" confirm
   state after the first click. */
.staff-roster-edit__revoke {
  margin-left: 0.6rem;
  color: color-mix(in srgb, var(--red) 60%, white);
  border-color: color-mix(in srgb, var(--red) 30%, transparent);
}
.staff-roster-edit__revoke--armed {
  background:    color-mix(in srgb, var(--red) 28%, transparent);
  border-color: color-mix(in srgb, var(--red) 60%, transparent);
  color:        color-mix(in srgb, var(--red) 85%, white);
}
.staff-roster-edit__error {
  flex: 1 0 100%;
  color: color-mix(in srgb, var(--red) 75%, white);
  font-size: var(--fs-sm);
}

/* /dispatch/police page layout. */
.garage-police {
  max-width: 720px;
  margin: 0 auto;
  padding: 1.5rem 2rem 3rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.garage-police__panel {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.garage-police__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.garage-police__title {
  margin: 0;
  font-size: var(--fs-xl);
}
.garage-police__banner {
  --accent: var(--status-orange);
}
.garage-police__own-shift {
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--role-police) 30%, var(--border-soft));
  border-radius: var(--radius-md);
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
/* No-shift wrapper: no surface chrome, only stacks the CTA + the inline
   start panel that opens underneath. Mirrors the tow create surface
   where the CTA sits bare in the section. */
.garage-police__no-shift {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.garage-police__own-shift-title {
  font-size: var(--fs-base);
  margin: 0;
}
.garage-police__own-shift-meta {
  display: flex;
  gap: 0.75rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.garage-police__own-shift-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  font-size: var(--fs-sm);
}
.garage-police__own-shift-controls label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
/* Dispatch sits in the same row as the code + region selects (clear
   "pick here, send right next to it" grouping). margin-left:auto pushes
   it to the row's trailing edge so it reads as the row's action, not a
   third field. */
.garage-police__own-shift-controls .btn {
  margin-left: auto;
}
/* In-game broadcast indicator (officer console + start panel). The radio /
   radio-off icon carries the signal; the --off modifier dims the text when the
   picked code will NOT be radioed (admin per-code toggle off). */
.police-notify {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.5rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.police-notify--off {
  color: var(--text-dim);
}
/* In the start panel the indicator shares the button row: pushed to the left
   (margin-right:auto) with the buttons trailing right, vertically centred. */
.police-start-panel__buttons .police-notify {
  margin-top: 0;
  margin-right: auto;
}
.garage-police__own-shift-events { font-size: var(--fs-sm); }
.garage-police__own-events-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  max-height: 200px;
  overflow-y: auto;
}
.garage-police__own-events-list li {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.garage-police__own-events-list time {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.garage-police__list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.garage-police__rows {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.garage-police__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  font-size: var(--fs-sm);
}
.garage-police__row:hover {
  background: var(--surface-3);
  border-color: color-mix(in srgb, var(--role-police) 40%, var(--border-soft));
}
.garage-police__badge {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  min-width: 3ch;
}
.garage-police__name {
  flex: 1;
  font-weight: var(--fw-bold);
  text-decoration: none;
  color: var(--text);
}
.garage-police__name:hover { color: var(--role-police); }
.garage-police__rank,
.garage-police__code,
.garage-police__region,
.garage-police__since {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.garage-police__since { font-family: var(--font-mono); }
/* "View on map" deep-link in the own-shift meta row. Inline pill,
   subtle until hovered. */
.garage-police__own-shift-map-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--role-police);
  text-decoration: none;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  transition: color var(--motion-fast);
}
.garage-police__own-shift-map-link:hover {
  color: color-mix(in srgb, var(--role-police) 70%, white);
}

/* ===========================================================================
   Community Events (/events)
   Reuses the shared .history-stream / .stream-event rows + the .btn family.
   Per-type colour comes from the [data-event-type] accent family at the end.
   =========================================================================== */
.garage-events {
  max-width: 720px;
  margin: 0 auto;
  padding: 1.5rem 2rem 3rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.garage-events__panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.garage-events__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}
.garage-events__title { font-size: var(--fs-xl); font-weight: var(--fw-bold); }
.garage-events__banner {
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
}

/* --- live + upcoming lists --- */
.events-live__title,
.events-upcoming__title {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  margin-bottom: 0.6rem;
}
.events-live__list,
.events-upcoming__list {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}

/* --- event card --- */
.event-card {
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-left: 3px solid var(--accent, var(--border-strong));
  border-radius: var(--radius-md);
  padding: 0.8rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.event-card--flash { border-color: var(--accent); background: var(--surface-2); }
.event-card__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.event-card__icon { color: var(--accent, var(--text-muted)); display: inline-flex; }
.event-card__type {
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent, var(--text-muted));
}
.event-card__title { font-size: var(--fs-md); font-weight: var(--fw-medium); }
.event-card__badge {
  margin-left: auto;
}
.event-card__badge[data-approval="approved"] { color: var(--status-green); }
.event-card__badge[data-approval="rejected"] { color: var(--status-red); }
.event-card__badge[data-approval="pending"]  { color: var(--status-yellow); }
/* Second badge on one head (e.g. closure + overdue): only the first pushes
   right via margin-left auto, followers sit next to it. */
.event-card__badge ~ .event-card__badge { margin-left: 0.4rem; }
/* Planned event whose start passed without the host going live: dimmed like
   the moderation list's hidden cards, badge in muted tone. Host controls
   inside stay usable -- going live late is what the grace window is for. */
.event-card--overdue { opacity: 0.75; }
.event-card__badge--overdue { color: var(--text-muted); }
.event-card__when { font-size: var(--fs-sm); }
.event-card__when-berlin { color: var(--text-muted); }
.event-card__route { font-size: var(--fs-sm); color: var(--text-muted); }
.event-card__desc { font-size: var(--fs-sm); color: var(--text-muted); }
.event-card__meta {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.event-card__error {
  font-size: var(--fs-xs);
  color: var(--status-red);
}
.event-card__controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.3rem;
  align-items: center;
}
.event-card__hint {
  font-size: var(--fs-xs);
  color: var(--status-yellow);
}

/* --- participant roster (click the "X going, Y maybe" count to reveal names) --- */
.event-card__rsvp-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  cursor: pointer;
}
.event-card__rsvp-toggle:hover { color: var(--text); }
.event-card__rsvp-caret { transition: transform var(--motion-base); }
.event-card__rsvp-toggle[aria-expanded="true"] .event-card__rsvp-caret {
  transform: rotate(90deg);
}
.event-card__roster {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.event-card__roster-group {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: var(--fs-sm);
}
.event-card__roster-label {
  flex: 0 0 3rem;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.event-card__roster-name { color: var(--text); }
a.event-card__roster-name:hover { text-decoration: underline; }

/* --- per-event detail map (shared, on-demand "Show on map") --- */
.event-card__map-row { margin-top: 0.3rem; }
.event-detail-map {
  height: 18rem;
  margin: 0.2rem 0 0.6rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  overflow: hidden;
}
/* Leaflet renders the route as an SVG path; stroke comes from CSS (CSP: no
   inline style). Mirrors the events marker colour. */
.event-detail-map__route {
  stroke: var(--event-marker);
  stroke-width: 3;
  stroke-dasharray: 6 6;
  fill: none;
}
/* Role-coloured detail markers: start (green), destination (red), closure
   (orange). Colour via [data-role] on the disc (CSP: class + data-*, no inline
   style). Icon + border stay white for contrast on the coloured disc. */
.event-detail-marker { background: transparent; border: none; }
.event-detail-marker__disc {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-full);
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
  color: var(--text);
}
.event-detail-marker__disc[data-role="start"]   { background: var(--status-green); }
.event-detail-marker__disc[data-role="dest"]     { background: var(--status-red); }
.event-detail-marker__disc[data-role="closure"]  { background: var(--status-orange); }

/* --- staff event moderation (/staff/events) --- */
.staff-events__list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.staff-events__note-field { margin-top: 0.3rem; }
/* A hidden event still shows in the moderation list, dimmed. */
.event-card--hidden { opacity: 0.55; }
.event-card__badge[data-hidden] { color: var(--status-red); }
.event-card__badge[data-status="live"]    { color: var(--status-green); }
.event-card__badge[data-status="planned"] { color: var(--text-muted); }

/* --- create panel --- */
.event-create-panel {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: 1rem 1.2rem;
}
.event-create-panel__form { display: flex; flex-direction: column; gap: 1rem; }
.event-create-panel__field { display: flex; flex-direction: column; gap: 0.4rem; }
.event-create-panel__label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
}
.event-create-panel__counter {
  align-self: flex-end;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.event-create-panel__error {
  font-size: var(--fs-sm);
  color: var(--status-red);
}
.event-create-panel__buttons {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

/* type radios: a row of selectable chips, accent per type */
.event-type-radios { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.event-type-radio {
  font-size: var(--fs-sm);
}
.event-type-radio svg { color: var(--accent, var(--text-muted)); }
.event-type-radio input { position: absolute; opacity: 0; pointer-events: none; }
.event-type-radio:has(input:checked) {
  border-color: var(--accent, var(--border-emphasis));
  background: var(--surface-2);
}

/* time hint (local + Berlin) */
.event-time-hint {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.7rem;
}
.event-time-hint__note { font-size: var(--fs-xs); color: var(--text-muted); }

/* site picker (search combobox) */
.event-site-picker { position: relative; }
.event-site-picker__results {
  list-style: none;
  position: absolute;
  /* Above the Leaflet preview map below it. Leaflet panes/controls reach
     z-index ~800 and its container sits at --z-map-layer (1000); --z-popover
     (20) would put the dropdown UNDER the map. --z-map-control (1010) clears
     the whole Leaflet stack so the results overlay the map. */
  z-index: var(--z-map-control);
  left: 0;
  right: 0;
  margin-top: 0.2rem;
  max-height: 14rem;
  overflow-y: auto;
}
.event-site-picker__option {
  padding: 0.4rem 0.7rem;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.event-site-picker__option:hover { background: var(--surface-2); }
.event-site-picker__empty {
  padding: 0.4rem 0.7rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.event-site-picker__chosen {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 0.2rem;
}

/* Start + Destination pickers side by side. Collapses to one column on a
   narrow panel so the comboboxes stay usable. */
.event-site-pickers {
  display: grid;
  /* Two equal picker columns; auto-fit collapses to one on a narrow panel.
     Reacts to the real panel width, not the viewport (was @32rem). */
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
}
.event-site-pickers__col { min-width: 0; }

/* Destination disabled for non-route types (carmeet/free): greyed + no input,
   but still visible so the layout doesn't jump on a type change (user request:
   grey out, not remove). The app sets .event-site-pickers__col--disabled. */
.event-site-pickers__col--disabled {
  opacity: 0.45;
  pointer-events: none;
}

/* preview map -- the main "is this the right place?" surface, so give it room.
   Taller now that the pickers share one row and the show-all toggle is gone. */
.event-map-preview {
  height: 26rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  overflow: hidden;
}
/* While a "Pick on map" mode is armed, the map invites a click. */
.event-map-preview.is-placing { cursor: crosshair; }

/* "Pick on map" button: the armed state (aria-pressed=true) reads as active. */
.event-site-picker__pick[aria-pressed="true"] {
  border-color: var(--border-emphasis);
  color: var(--text);
}

/* Optional closure-point picker row (shown only when road-closure is on). */
.event-closure-pick {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* road-closure toggle */
.event-closure-toggle {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.event-closure-note {
  font-size: var(--fs-xs);
  color: var(--status-yellow);
  margin-top: -0.5rem;
}

/* --- production events: vehicle checkboxes (wizard + dashboard) --- */
/* Shared by the create-wizard field and the dashboard's "Your order" form,
   so the two surfaces cannot drift apart. The container is a 6-column grid
   (checkbox | name | Required-money | level gate | level gate | count) and
   each row inherits those tracks via subgrid, so the "Required:" money and
   the level gates line up as columns across rows (user request 2026-07-18).
   The two level-gate tracks mirror REQUIREMENT_COLUMNS in
   vehicle-requirements.js -- change both together. */
.event-vehicle-checks {
  display: grid;
  grid-template-columns: max-content max-content max-content max-content max-content 1fr;
  row-gap: 0.4rem;
}
.event-vehicle-check {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  column-gap: 0.9rem;
  font-size: var(--fs-sm);
  cursor: pointer;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-1);
}
.event-vehicle-check:has(input:checked) {
  border-color: var(--border-emphasis);
}
.event-vehicle-check:has(input:disabled) {
  cursor: default;
  color: var(--text-muted);
}
.event-vehicle-check__req {
  color: var(--text-dim);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Fulfilled order marker on the dashboard form (checked + disabled row). */
.event-vehicle-check__bought {
  color: var(--status-green);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
}
.event-vehicle-check__count {
  justify-self: end;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Narrow screens: the 6 columns cannot fit -- fall back to a wrapping row
   (name + count first line, requirement cells flowing below). */
@media (max-width: 480px) {
  .event-vehicle-checks { display: flex; flex-direction: column; }
  .event-vehicle-check { display: flex; flex-wrap: wrap; gap: 0.25rem 0.6rem; }
  .event-vehicle-check__count { margin-left: auto; }
}

/* --- production-event dashboard page (/events/:id) --- */
.prod-event {
  max-width: 720px;
  margin: 0 auto;
  padding: 1.2rem 1rem 3rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.prod-event__back {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
  align-self: flex-start;
}
.prod-event__back:hover { color: var(--text); }
.prod-event__head-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.prod-event__title {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  margin: 0;
}
.prod-event__status {
  border-radius: var(--radius-pill);
  padding: 0.1rem 0.7rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
}
.prod-event__status[data-status="live"] {
  border-color: color-mix(in srgb, var(--status-green) 45%, transparent);
  color: var(--status-green);
}
.prod-event__status[data-status="planned"] {
  border-color: color-mix(in srgb, var(--status-yellow) 45%, transparent);
  color: var(--status-yellow);
}
.prod-event__meta {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  margin: 0.35rem 0 0;
}
.prod-event__desc {
  color: var(--text);
  font-size: var(--fs-base);
  margin: 0.5rem 0 0;
  max-width: 65ch;
}
.prod-event__error {
  border: 1px solid color-mix(in srgb, var(--red) 45%, transparent);
  border-radius: var(--radius-md);
  color: var(--red);
  font-size: var(--fs-sm);
  padding: 0.5rem 0.8rem;
}
.prod-event__panel {
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.9rem 1rem;
}
.prod-event__panel-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  margin: 0 0 0.5rem;
}
.prod-event__hint {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  margin: 0 0 0.7rem;
}
.prod-event__order-actions {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: 0.7rem;
}
.prod-event__order-note {
  color: var(--text-muted);
  font-size: var(--fs-xs);
}
.prod-event__orders {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.prod-event__order-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0;
  border-top: 1px solid var(--border-hairline);
  font-size: var(--fs-sm);
}
.prod-event__order-row:first-child { border-top: 0; }
.prod-event__order-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prod-event__order-tags {
  margin-left: auto;
  display: flex;
  gap: 0.3rem;
  flex-shrink: 0;
}
.prod-order-tag {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 0 0.35rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
}
.prod-order-tag--done {
  border-color: transparent;
  background: color-mix(in srgb, var(--status-green) 16%, transparent);
  color: var(--status-green);
}
/* Buy-feed timestamp column (stage C). */
.prod-event__buy-when {
  color: var(--text-dim);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex: none;
}

/* --- staff announcement panel (stage D) --- */
.prod-event__panel--staff {
  border-style: dashed;
  border-color: var(--border-strong);
}
/* Label chrome from the shared micro-label family; colour is the deliberate
   staff-only accent, layout local. */
.prod-event__staff-label {
  display: block;
  color: var(--status-yellow);
  margin-bottom: 0.3rem;
}
.prod-announce__form {
  display: flex;
  gap: 0.5rem;
}
.prod-announce__form .event-create-panel__input { flex: 1; }
/* Per-row megaphone (staff): own grid track (aligned column), quiet until
   hover. The empty .prod-node__mic cell keeps non-staff rows on the same
   template. */
.prod-node__announce {
  color: var(--text-dim);
}
.prod-node__announce:hover,
.prod-node__announce:focus-visible { color: var(--text); }
/* Preview-then-send strip under the pipeline (stage D). */
.prod-announce-confirm {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: 0.7rem;
  padding: 0.5rem 0.8rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
}
.prod-announce-confirm__text {
  flex: 1 1 16rem;
  font-size: var(--fs-sm);
  color: var(--text);
}

/* --- production-event dashboard: KPI row --- */
.prod-event__kpis {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.6rem;
}
@media (max-width: 768px) {
  .prod-event__kpis { grid-template-columns: repeat(2, 1fr); }
}
.prod-kpi {
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.6rem 0.8rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
/* .prod-kpi__label chrome comes from the shared micro-label family. */
.prod-kpi__value {
  font-size: var(--fs-lg);
  font-weight: var(--fw-hero);
  font-variant-numeric: tabular-nums;
}
.prod-kpi__sub {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.prod-event__stale {
  border: 1px solid color-mix(in srgb, var(--status-yellow) 40%, transparent);
  border-radius: var(--radius-md);
  color: var(--status-yellow);
  font-size: var(--fs-sm);
  padding: 0.5rem 0.8rem;
  margin: 0;
}

/* --- production-event dashboard: pipeline stages --- */
.prod-stage { padding: 0.55rem 0; border-top: 1px solid var(--border-hairline); }
.prod-stage:first-child { border-top: 0; padding-top: 0; }
/* Chrome from the shared micro-label family; only layout stays local. */
.prod-stage__title { margin: 0 0 0.4rem; }
/* One node row: dot | name | mic | bar | stock | warn chips. The state
   colour rides on --accent (finite set -> data-state selector family),
   which the shared .stat-bar__fill and the dot both inherit. The mic cell
   is its own track so the staff megaphones line up as a column instead of
   trailing the ragged name ends (user request 2026-07-19); for non-staff
   the cell is empty and the track collapses to zero. */
.prod-node {
  display: grid;
  grid-template-columns: var(--size-xs) minmax(8rem, 14rem) max-content minmax(4rem, 1fr) max-content;
  grid-auto-flow: column;
  align-items: center;
  gap: 0.6rem;
  padding: 0.22rem 0;
  font-size: var(--fs-sm);
}
.prod-node[data-state="ok"]      { --accent: var(--status-green); }
.prod-node[data-state="low"]     { --accent: var(--status-yellow); }
.prod-node[data-state="slow"]    { --accent: var(--status-orange); }
.prod-node[data-state="stalled"] { --accent: var(--status-red); }
.prod-node__dot {
  width: var(--size-xs);
  height: var(--size-xs);
  border-radius: var(--radius-full);
  background: var(--accent, var(--status-muted));
  flex: none;
}
.prod-node__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prod-node__bar { min-width: 4rem; }
.prod-node__stock {
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-align: right;
}
.prod-node__chips { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.prod-chip {
  border-radius: var(--radius-pill);
  padding: 0 0.55rem;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  background: var(--surface-2);
  color: var(--text-muted);
  white-space: nowrap;
}
.prod-chip--warn {
  background: color-mix(in srgb, var(--status-yellow) 16%, transparent);
  color: var(--status-yellow);
}
.prod-chip--crit {
  background: color-mix(in srgb, var(--status-red) 16%, transparent);
  color: var(--status-red);
}
@media (max-width: 480px) {
  .prod-node { grid-template-columns: var(--size-xs) 1fr max-content max-content; }
  .prod-node__bar { display: none; }
}

/* --- production-event dashboard: demand table --- */
.prod-demand {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-dense);
}
/* Header chrome from the shared micro-label family; layout stays local. */
.prod-demand th {
  text-align: left;
  padding: 0.3rem 0.6rem 0.4rem;
  border-bottom: 1px solid var(--border-soft);
}
/* .prod-demand th outranks the shared num rule -- pin number headers right
   (same header/cell mismatch documented at .leaderboard-table__num). */
.prod-demand th.prod-demand__num { text-align: right; }
.prod-demand td {
  padding: 0.35rem 0.6rem;
  border-bottom: 1px solid var(--border-hairline);
  font-variant-numeric: tabular-nums;
}
.prod-demand tr:last-child td { border-bottom: 0; }
/* numeric cells: shared .prod-demand__num rule next to .leaderboard-table__num */
.prod-demand__missing { color: var(--status-yellow); font-weight: var(--fw-bold); }
.prod-demand__cargo { display: flex; align-items: center; gap: 0.45rem; font-weight: var(--fw-medium); }
.prod-demand__swatch {
  width: var(--size-sm);
  height: var(--size-sm);
  border-radius: var(--radius-sm);
  background: var(--accent, var(--status-muted));
  flex: none;
}

/* --- calendar (event-calendar.js) --- */
.events-calendar-section { margin-top: 0.5rem; }
.evcal {
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.8rem;
}
.evcal__header {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}
.evcal__title {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  flex: 1;
  text-align: center;
}
.evcal__today { margin-left: auto; }
.evcal__weekdays,
.evcal__grid {
  display: grid;
  /* minmax(0, 1fr), not bare 1fr: a 1fr track's auto minimum lets one long
     nowrap chip widen its whole column and defeat the chip's ellipsis. */
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 2px;
}
.evcal__weekday {
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  padding: 0.2rem 0;
}
.evcal__cell {
  min-height: 4.5rem;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-sm);
  padding: 0.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.evcal__cell--empty { background: transparent; border-color: transparent; }
.evcal__cell--today { border-color: var(--border-emphasis); }
.evcal__cell--clickable { cursor: pointer; }
.evcal__cell--clickable:hover { background: var(--surface-2); }
.evcal__daynum { font-size: var(--fs-xs); color: var(--text-muted); }
.evcal__cell--today .evcal__daynum { color: var(--text); font-weight: var(--fw-medium); }
.evcal__chips { display: flex; flex-direction: column; gap: 0.1rem; overflow: hidden; }
.evcal__chip {
  font-size: var(--fs-xs);
  line-height: 1.3;
  padding: 0.05rem 0.3rem;
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--accent, var(--border-strong));
  background: var(--surface-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.evcal__chip--more {
  border-left-color: transparent;
  background: transparent;
  color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
   Event-type accent family. Sets --accent for any element tagged with
   data-event-type, mirroring the [data-skill] / [data-tone] families.
   Finite set, so a CSS family -- never inline styles (CSP, project_design_tokens).
   --------------------------------------------------------------------------- */
[data-event-type="convoy"]  { --accent: var(--event-convoy); }
[data-event-type="race"]    { --accent: var(--event-race); }
[data-event-type="carmeet"] { --accent: var(--event-carmeet); }
[data-event-type="free"]    { --accent: var(--event-free); }
[data-event-type="production"] { --accent: var(--event-production); }

/* Live-event map marker (map-app.js eventMarkerIcon). Same disc shape as the
   tow layer marker; fill comes from the [data-event-type] accent family on the
   disc element so each event type keeps its colour. CSP-safe (class + data-*). */
.event-layer-marker { background: transparent; border: none; }
.event-layer-marker__disc {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-full);
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
  color: var(--text);
  background: var(--accent, var(--event-free));
}

/* Privacy settings page: the account opt-out plus the two profile opt-in
   toggles. Layout-only spacing (rem); colours come from surface/border/text
   tokens so stylelint's no-raw-colour rule stays satisfied. */
.privacy-page { max-width: 44rem; }

.privacy-form__group {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 0 0 1.5rem;
  padding: 0;
  border: 0;
}

.privacy-toggle {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 1rem;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.privacy-toggle input {
  flex: none;
  margin-top: 0.2rem;
}

.privacy-toggle__text {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.privacy-toggle__title { font-weight: var(--fw-medium); }

.privacy-toggle__desc {
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.privacy-form__actions {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.privacy-form__status {
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.privacy-form__status--error {
  color: var(--text);
  font-weight: var(--fw-medium);
}

.privacy-page__policy {
  margin-top: 1.5rem;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.privacy-form__group:disabled .privacy-toggle { opacity: 0.6; cursor: default; }

/* Opt-in teaser on the profile: the granular cards show a blurred FAUX preview
   with an enable button (owner) instead of the real data until opted in. The
   server sends NO real data in this state -- the blurred layer is fake. */
.resident-card__teaser-wrap { position: relative; }

.resident-card.is-teaser .resident-heatmap,
.resident-card.is-teaser .resident-timeline {
  filter: blur(7px);
  opacity: 0.5;
  pointer-events: none;
  user-select: none;
}

.privacy-teaser {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 1rem;
  text-align: center;
}

.privacy-teaser__msg {
  margin: 0;
  max-width: 28rem;
  color: var(--text);
  font-weight: var(--fw-medium);
}

.resident-timeline__skeleton span {
  display: block;
  width: 70%;
  height: 0.85rem;
  background: var(--surface-3);
  border-radius: var(--radius-sm);
}

/* ============================================================
   Fuel-station editor (/fuel-stations): lean map + tanker roster
   ============================================================ */
.fuel-editor {
  display: flex;
  height: calc(100vh - var(--nav-h));
}
.fuel-editor__map {
  flex: 1 1 auto;
  min-width: 0;
}
.fuel-editor__sidebar {
  flex: 0 0 22rem;
  max-width: 85vw;
  overflow-y: auto;
  padding: 1rem;
  background: var(--surface-1);
  border-left: 1px solid var(--border-hairline);
}
.fuel-editor__title { font-size: var(--fs-lg); color: var(--text); }
.fuel-editor__hint {
  margin-top: 0.35rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.fuel-editor__status {
  margin: 0.6rem 0;
  padding: 0.5rem 0.7rem;
  font-size: var(--fs-sm);
  color: var(--text);
  background: var(--surface-2);
  border-radius: var(--radius-md);
}
.fuel-editor__status--error { color: color-mix(in srgb, var(--red) 70%, white); }

.fuel-roster__group { margin-top: 1rem; }
.fuel-roster__group-title {
  margin-bottom: 0.4rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.fuel-roster__list { list-style: none; display: flex; flex-direction: column; gap: 0.4rem; }
.fuel-roster__loading { font-size: var(--fs-sm); color: var(--text-muted); }

.fuel-row {
  padding: 0.55rem 0.65rem;
  background: var(--surface-2);
  border-radius: var(--radius-md);
}
/* A placed tanker's row is clickable to locate its pin on the map. */
.fuel-row--placed { cursor: pointer; }
.fuel-row--placed:hover { background: var(--surface-3); }
.fuel-row__head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.fuel-row__label { font-weight: var(--fw-medium); color: var(--text); }
.fuel-row__status { font-size: var(--fs-xs); color: var(--text-muted); }
.fuel-row__status[data-state="published"] { color: color-mix(in srgb, var(--fuel-fill-full) 70%, white); }
.fuel-row__status[data-state="draft"]     { color: color-mix(in srgb, var(--fuel-fill-low) 70%, white); }
.fuel-row__fills { display: flex; gap: 0.25rem; margin: 0.45rem 0; }
.fuel-fill-btn {
  flex: 1;
  padding: 0.25rem 0.2rem;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.fuel-fill-btn--on { color: var(--text); border-color: var(--text-muted); background: var(--surface-3); }
.fuel-fill-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.fuel-row__actions { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.fuel-row__meta { margin-top: 0.4rem; font-size: var(--fs-xs); color: var(--text-muted); }

/* Pin markers (editor + GPS layer): a fuel-pump icon coloured by fuel type.
   Fill level lives in the tooltip, not the marker (a ring read badly). Draft
   pins (editor only) are translucent so editors see what is not yet public. */
.fuel-pin-svg { display: block; transition: transform 0.15s ease; }
.fuel-pin-svg__body {
  fill: var(--fuel-diesel);
  stroke: white;
  stroke-width: 0.5;
}
.fuel-pin-svg[data-fuel="gasoline"] .fuel-pin-svg__body { fill: var(--fuel-gasoline); }
.fuel-pin-svg__icon { color: white; }          /* fuel-pump strokes white inside the coloured pin */
.fuel-marker--draft { opacity: 0.5; }
/* Brief emphasis when a roster row is clicked (locate): scale from the tip. */
.fuel-marker--active .fuel-pin-svg { transform: scale(1.45); transform-origin: 50% 92%; }

/* Members-only page (logged-in non-member). */
.fuel-locked-page { display: flex; justify-content: center; padding: 3rem 1rem; }
.fuel-locked-card {
  max-width: 30rem;
  padding: 2rem;
  text-align: center;
  background: var(--surface-1);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
}
.fuel-locked-card__icon { width: 2.5rem; height: 2.5rem; color: var(--text-muted); }
.fuel-locked-card__title { margin: 0.6rem 0; font-size: var(--fs-xl); }
.fuel-locked-card__text { margin-bottom: 1.2rem; color: var(--text-muted); }

@media (max-width: 768px) {
  .fuel-editor { flex-direction: column; height: auto; }
  .fuel-editor__map { height: 55vh; }
  .fuel-editor__sidebar {
    flex-basis: auto;
    max-width: none;
    border-top: 1px solid var(--border-hairline);
    border-left: 0;
  }
}


/* === Production chains (/chains) ============================================
   Flow board: tier columns of node cards joined by SVG connectors, styled
   after the economy family (shares .econ-page shell + header). States map to
   the status tokens; cargo dot colours arrive inline from the API payload
   (single source: data/cargo.js on the server). */

.chains-picker { display: flex; flex-direction: column; gap: 0.5rem; }
.chains-picker__row { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.chains-picker__group {
  flex: 0 0 8.5rem;
  font-size: var(--fs-xs, 0.72rem);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-dim);
}
.chains-picker__chips { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.chains-chip {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill);
  padding: 0.3rem 0.8rem;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.chains-chip:hover { background: var(--surface-2); color: var(--text); }
.chains-chip.is-active { background: var(--surface-3); border-color: var(--border-strong); color: var(--text); }

.chains-summary { margin: 0; color: var(--text-muted); font-size: var(--fs-sm); }
.chains-summary__stalled { color: var(--status-red); font-weight: var(--fw-medium); }

.chains-board-scroll {
  overflow-x: auto;
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-1);
}
/* extra bottom padding = the lane zone where loop edges swing back; the wide
   left padding is the gutter where those loops rise into column-1 targets */
.chains-board { position: relative; display: grid; gap: 2.4rem; padding: 1.1rem 1.2rem 4rem 3rem; min-width: 940px; }
.chains-wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
/* Flow edges are SOLID and calm; only boosters dash (and march, motion
   permitting) -- the legend promises exactly that distinction. */
@media (prefers-reduced-motion: no-preference) {
  .chains-wires .is-boost path { animation: chains-flow 1.4s linear infinite; }
  @keyframes chains-flow { to { stroke-dashoffset: -12; } }
}
/* Spotlight: hovering (or tapping) a card lights up its edges and fades the
   rest of the web plus unrelated cards. */
.chains-wires g { transition: opacity 0.15s; }
.chains-board.has-focus .chains-wires g { opacity: 0.06; }
/* same specificity as the fade rule above; source order gives it the win */
.chains-board.has-focus g.is-lit { opacity: 1; }
.chains-node { transition: opacity 0.15s; }
.chains-board.has-focus .chains-node:not(.is-lit) { opacity: 0.35; }
.chains-empty { grid-column: 1 / -1; padding: 2.5rem 1rem; text-align: center; color: var(--text-dim); }

.chains-tier { display: flex; flex-direction: column; gap: 0.8rem; min-width: 0; }
.chains-tier__head {
  font-size: var(--fs-xs, 0.72rem);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border-hairline);
  padding-bottom: 0.3rem;
}
.chains-tier__cards { display: flex; flex-direction: column; gap: 0.8rem; justify-content: center; flex: 1; }

.chains-node {
  /* solid (non-translucent) surface: connector lines pass UNDER the cards
     and must actually disappear there, not shimmer through */
  background: color-mix(in srgb, var(--text) 7%, var(--bg));
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: 0.55rem 0.65rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  position: relative;
  z-index: 1;
  font-size: var(--fs-sm);
}
.chains-node.is-stalled { border-color: color-mix(in srgb, var(--status-red) 45%, transparent); }
.chains-node.is-slow { border-color: color-mix(in srgb, var(--status-orange) 35%, transparent); }
/* Incomplete (a curated site missing from the game data, review B6-#3): a
   neutral/muted marker, not an alarm colour -- the stock reading cannot be
   trusted, so it must not read as a genuine stall. */
.chains-node.is-incomplete { border-color: color-mix(in srgb, var(--status-muted) 45%, transparent); }
.chains-node--sink { background: color-mix(in srgb, var(--text) 4%, var(--bg)); border-style: dashed; }

.chains-node__site { font-weight: var(--fw-medium); display: flex; justify-content: space-between; gap: 0.5rem; align-items: baseline; }
.chains-node__flag { font-size: var(--fs-xs); font-weight: var(--fw-bold); letter-spacing: 0.06em; white-space: nowrap; }
.chains-node__flag.is-stalled { color: var(--status-red); }
.chains-node__flag.is-slow, .chains-node__flag.is-low { color: var(--status-orange); }
.chains-node__flag.is-incomplete { color: var(--status-muted); }

.chains-node__cargo { display: flex; align-items: center; gap: 0.45rem; color: var(--text-muted); flex-wrap: wrap; }
.chains-node__cargo > span { display: inline-flex; align-items: center; gap: 0.3rem; }

.chains-dot { width: 8px; height: 8px; border-radius: var(--radius-full); flex: none; display: inline-block; }
.chains-dot--ok { background: var(--status-green); }
.chains-dot--low { background: var(--status-orange); }
.chains-dot--crit { background: var(--status-red); }
.chains-dot--slow { background: var(--status-muted); }

.chains-node__stock { display: flex; align-items: center; gap: 0.5rem; }
.chains-node__stockbar { flex: 1; height: 5px; border-radius: var(--radius-sm); background: var(--surface-3); overflow: hidden; }
.chains-node__stockbar i { display: block; height: 100%; border-radius: var(--radius-sm); }
.chains-node__stockbar i.is-ok { background: var(--status-green); }
.chains-node__stockbar i.is-low { background: var(--status-orange); }
.chains-node__stockbar i.is-crit { background: var(--status-red); }
.chains-node__stocknum { font-size: var(--fs-xs, 0.72rem); color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

.chains-node__rate { font-size: var(--fs-xs, 0.72rem); color: var(--text-dim); font-variant-numeric: tabular-nums; }

.chains-node__inputs { display: flex; flex-direction: column; gap: 0.15rem; border-top: 1px solid var(--border-hairline); padding-top: 0.35rem; }
.chains-node__input {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-xs, 0.72rem);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.chains-node__input.is-low { color: var(--status-orange); }
.chains-node__input.is-out { color: var(--status-red); font-weight: var(--fw-medium); }
.chains-node__have { margin-left: auto; }

.chains-node__boosters { display: flex; flex-direction: column; gap: 0.1rem; }
.chains-node__booster { display: flex; align-items: center; gap: 0.4rem; font-size: var(--fs-xs, 0.72rem); color: var(--status-yellow); }
.chains-node__booster.is-out { color: var(--text-dim); }
.chains-node__note { font-size: var(--fs-xs, 0.72rem); color: var(--text-dim); font-style: italic; }
.chains-node__note--warn { color: var(--status-orange); font-style: normal; }

.chains-legend { display: flex; gap: 1.1rem; flex-wrap: wrap; font-size: var(--fs-xs, 0.72rem); color: var(--text-dim); }
.chains-legend span { display: inline-flex; align-items: center; gap: 0.4rem; }

.chains-panels { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.chains-panel {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: 0.9rem 1rem;
}
.chains-panel h2 {
  margin: 0 0 0.5rem;
  font-size: var(--fs-xs, 0.72rem);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-dim);
  font-weight: var(--fw-medium);
}
.chains-panel__hint { margin: 0 0 0.6rem; font-size: var(--fs-xs); color: var(--text-dim); max-width: 60ch; }
.chains-hauls { display: flex; flex-direction: column; gap: 0.55rem; }
.chains-haul { display: flex; gap: 0.6rem; align-items: baseline; font-size: var(--fs-sm); }
.chains-haul small { color: var(--text-dim); }
.chains-haul.is-crit strong { color: var(--status-red); }
.chains-haul.is-warn strong { color: var(--status-orange); }
.chains-hauls__empty { margin: 0; color: var(--text-dim); font-size: var(--fs-sm); }
.chains-about { margin: 0; color: var(--text-muted); font-size: var(--fs-sm); max-width: 60ch; }

@media (max-width: 768px) {
  .chains-panels { grid-template-columns: 1fr; }
  .chains-picker__group { flex-basis: 100%; }
}

/* --- chains map view (second rendering behind the Flow | Island map toggle) -- */

.chains-controls { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; flex-wrap: wrap; }
.chains-seg { display: inline-flex; border: 1px solid var(--border-soft); border-radius: var(--radius-pill); overflow: hidden; flex: none; }
.chains-seg__btn {
  background: none;
  border: none;
  color: var(--text-dim);
  font: inherit;
  font-size: var(--fs-sm);
  padding: 0.35rem 0.95rem;
  cursor: pointer;
}
.chains-seg__btn:hover { color: var(--text-muted); }
.chains-seg__btn.is-active { background: var(--surface-3); color: var(--text); }

.chains-map-wrap { border: 1px solid var(--border-hairline); border-radius: var(--radius-md); overflow: hidden; }
/* Square container: the island texture is square, so cover-zoom == the WHOLE
   map -- no panning needed to see every pin (user requirement). */
.chains-map { width: min(100%, 78vh); aspect-ratio: 1; margin-inline: auto; background: var(--bg); }
.chains-map__note { margin: 0; padding: 0.5rem 0.9rem; font-size: var(--fs-xs); color: var(--text-dim); border-top: 1px solid var(--border-hairline); }

.chains-map-pin { display: flex; align-items: center; gap: 0.35rem; }
.chains-map-pin__ring {
  width: 18px; height: 18px; border-radius: var(--radius-full);
  border: 2px solid var(--status-green);
  background: var(--bg);
  display: grid; place-items: center;
  box-shadow: var(--shadow-popup);
  flex: none;
}
.chains-map-pin__ring.is-sink { border-style: dashed; }
.chains-map-pin__ring i { width: 7px; height: 7px; border-radius: var(--radius-full); display: block; }
.chains-map-pin__tag {
  white-space: nowrap;
  font-size: var(--fs-xs);
  line-height: 1.3;
  color: var(--text-overlay-strong);
  background: var(--surface-overlay);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  padding: 0.05rem 0.4rem;
}

/* Route direction triangle. Centred ON the line: the translate(-50%,-50%)
   runs INSIDE the same transform as the rotation (set via CSSOM), because a
   margin offset would shift in unrotated screen space and drift off the line
   at any angle other than horizontal. */
.chains-map-tri span {
  display: block;
  position: absolute;
  width: 0; height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 9px solid currentColor;
}

/* pin popups on the chains map */
.chains-popup { font-size: var(--fs-sm); line-height: 1.55; }
.chains-popup__title { font-weight: var(--fw-medium); margin-bottom: 0.25rem; }
.chains-popup__site { color: var(--text-dim); font-size: var(--fs-xs); margin-bottom: 0.2rem; }
.chains-popup__dim { color: var(--text-dim); font-size: var(--fs-xs); }
.chains-popup__input { font-size: var(--fs-xs); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.chains-popup__input.is-low { color: var(--status-orange); }
.chains-popup__input.is-out { color: var(--status-red); font-weight: var(--fw-medium); }
.chains-popup__flag { font-size: var(--fs-xs); font-weight: var(--fw-bold); letter-spacing: 0.06em; }
.chains-popup__flag.is-stalled { color: var(--status-red); }
.chains-popup__flag.is-slow, .chains-popup__flag.is-low { color: var(--status-orange); }
.chains-popup__flag.is-incomplete { color: var(--status-muted); }
.chains-map-pin { cursor: pointer; }

/* ============ Shared time-of-day picker (hour + 5-min selects) ============
   Used wherever a time is picked next to a native date input (event form,
   staff movement replay). Own selects instead of datetime-local's time
   half: minutes must move in 5-minute steps site-wide, and Chrome ignores
   a step attribute inside the datetime-local popup. Select chrome comes
   from the global form-element reset. */
.time-pick {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
}
.time-pick__colon { color: var(--text-dim); }
/* The events create-panel stacks fields; date + time sit on one line. */
.event-when-fields {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* ============ Staff movement replay (/staff/replay) ============
   Layout mirrors the fuel editor (map left, sidebar right, full viewport
   height under the nav); the transport bar reuses the shared .replay-bar
   chrome from the public map player, re-anchored inside the map wrap
   because this page has no .map-sidebar to offset against. */
.staff-replay-page {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--nav-h));
}
.staff-replay__toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  padding: 0.6rem 1rem;
}
.staff-replay__title { font-size: var(--fs-lg); color: var(--text); }
.staff-replay__field {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}
.staff-replay__field-label {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.staff-replay__status {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.staff-replay__status--error { color: color-mix(in srgb, var(--red) 70%, white); }
.staff-replay__hint {
  padding: 0 1rem 0.6rem;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.staff-replay__body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}
.staff-replay__map-wrap {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}
.staff-replay__map { height: 100%; }
/* Base .replay-bar chrome/position fits as-is (bottom 2.5rem like the GPS
   player); only the right edge differs -- this page has no overlay sidebar
   to offset against, the player list is a separate flex column. */
.staff-replay__bar { right: 12px; }
/* Lift the (bottom-right) zoom control clear of the transport bar. */
.staff-replay__map .leaflet-bottom .leaflet-control-zoom { margin-bottom: 6rem; }
.staff-replay__pos { min-width: 9.5em; }
.staff-replay__sidebar {
  display: flex;
  flex-direction: column;
  flex: 0 0 18rem;
  max-width: 85vw;
  min-height: 0;
  padding: 0.75rem;
  gap: 0.6rem;
  background: var(--surface-1);
  border-left: 1px solid var(--border-hairline);
}
.staff-replay__list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.staff-replay__empty {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
/* One row per tracked player. The --sel custom property carries the
   assigned highlight colour (stamped by staff-replay-app.js); unselected
   rows show a hollow dot. Rows are <button>s for keyboard access. */
.staff-replay__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.5rem;
  font-size: var(--fs-sm);
  color: var(--text);
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.staff-replay__row:hover { background: var(--surface-2); }
.staff-replay__row--selected { background: var(--surface-2); }
/* Camera-follow target: colour bar on the row's left edge (in the player's
   highlight colour). At most one row carries this at a time. */
.staff-replay__row--followed { box-shadow: inset 3px 0 0 var(--sel); }
.staff-replay__row-dot {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-full);
  border: 1px solid var(--text-dim);
  background: var(--sel, transparent);
}
.staff-replay__row--selected .staff-replay__row-dot { border-color: var(--sel); }
.staff-replay__row-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.staff-replay__row-veh {
  flex: none;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-xs);
  color: var(--text-dim);
}

/* Replay player marker: red dot like the live map's player marker; the
   name label appears on hover and stays visible while selected. A selected
   dot takes its player's highlight colour (--sel, stamped on the divIcon
   element). Stale = held position with no fresh sample (stationary/indoors
   or briefly disconnected) -- greyed, not hidden. */
.replay-player { background: transparent; border: none; }
.replay-player__dot {
  display: block;
  width: var(--size-sm);
  height: var(--size-sm);
  border-radius: var(--radius-full);
  background: var(--sel, var(--red));
  border: 2px solid var(--text);
  box-shadow: var(--map-glow-box);
}
.replay-player--stale .replay-player__dot {
  background: color-mix(in srgb, var(--sel, var(--red)) 30%, var(--status-muted));
  border-color: var(--text-dim);
}
.replay-player__label {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 3px;
  padding: 0 0.3rem;
  font-size: var(--fs-xs);
  line-height: 1.4;
  white-space: nowrap;
  color: var(--text-overlay-strong);
  background: var(--surface-overlay);
  border-radius: var(--radius-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-fast);
}
/* Vehicle line under the name (stamped by stampVehicle); hidden while the
   player has no stamped vehicle yet, so the label stays a single line. */
.replay-player__label-veh { color: var(--text-dim); }
.replay-player__label-veh:empty { display: none; }
.replay-player:hover .replay-player__label,
.replay-player--selected .replay-player__label,
.staff-replay__map--labels .replay-player__label { opacity: 1; }

/* Stack the sidebar under the map on phones -- same 768px family as the
   other map pages (project_breakpoint_consolidation). */
@media (max-width: 768px) {
  .staff-replay__body { flex-direction: column; }
  .staff-replay__sidebar {
    flex: 0 0 30vh;
    max-width: none;
    border-left: none;
    border-top: 1px solid var(--border-hairline);
  }
}


