/* =========================================================================
   UFO Explorer — Styles
   ========================================================================= */

:root {
    /* =====================================================================
       Ground Zero Plus palette — pure-black + UFO amber + crimson.
       Extracted from https://groundzeroplus.com via Playwright. Both
       body.theme-signal and body.theme-declass below hold the same
       tokens so the existing toggle (hidden via CSS) is a no-op.
       ===================================================================== */

    /* Pure-black surfaces with one near-black panel accent */
    --bg:            #000000;   /* page — pure black */
    --bg-panel:      #07070c;   /* header, rail, popovers */
    --bg-card:       #0b0b12;   /* insight / timeline cards */
    --bg-card-h:     #15151f;   /* hover */
    --bg-input:      #000000;   /* sunk fields */
    --surface-1:     #0f0f1a;   /* modals */
    --surface-2:     #191925;   /* hover state on modals */

    /* Clean ink — white heads, light-grey body */
    --text:          #d5d8dc;   /* body — cool off-white */
    --text-strong:   #ffffff;   /* heads — pure white */
    --text-muted:    #8b8e96;   /* secondary */
    --text-faint:    #5a5d65;   /* faded */
    --fg-dim:        #8b8e96;   /* alias used by Chart.js fallbacks in app.js */

    /* Rules — cool, subtle, visible on pure black */
    --border:        #1a1a24;
    --border-strong: #2c2c38;

    /* UFO amber primary accent + crimson secondary. Derived from GZ+'s
       #ffc01d (most-used link/button color) and #c62410 (secondary). */
    --accent:        #ffc01d;   /* UFO amber — primary accent */
    --accent-hover:  #ffd34f;
    --accent-hot:    #c62410;   /* crimson — used for alert/stamp fallback in app.js */
    --accent-bg:     rgba(255, 192, 29, 0.14);
    --accent-warm:   #ffc01d;   /* alias — bias-banner uses this */
    --accent-red:    #c62410;   /* GZ+ crimson secondary */

    /* Categorical palette — derived from the GZ+ black/amber/crimson
       identity with a few desaturated cool stops so 8 source/chart
       series remain distinguishable without reading as rainbow. */
    --cat-1: #ffc01d;   /* UFOCAT — UFO amber */
    --cat-2: #c62410;   /* NUFORC — GZ crimson */
    --cat-3: #e8e8ec;   /* MUFON  — bone white */
    --cat-4: #8b8e96;   /* UPDB   — cool grey */
    --cat-5: #f28e2b;   /* UFO-search — warm amber-orange */
    --cat-6: #9966ff;   /* spare dusty violet */
    --cat-7: #4bc0c0;   /* spare teal */
    --cat-8: #59a14f;   /* spare moss green */

    /* Semantic palette — GZ+ uses red-amber-white; no neon greens. */
    --success: #7ab86e;          /* muted green */
    --warning: #ffc01d;          /* UFO amber */
    --danger:  #c62410;          /* GZ+ crimson */
    --info:    #ffc01d;

    /* Legacy aliases */
    --green:  var(--success);
    --orange: var(--warning);
    --red:    var(--danger);
    --yellow: #ffc01d;

    /* Fonts — Ground Zero Plus uses Aldrich (display), Oxanium (h2),
       Inter (body). All free via Google Fonts. IBM Plex Mono stays
       for numeric / data readouts. */
    --font-sans:    "Inter", ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-display: "Aldrich", "Oxanium", Impact, sans-serif;
    --font-serif:   "Oxanium", "Aldrich", "Inter", sans-serif;       /* "serif" slot now carries the GZ+ condensed display face */
    --font-mono:    "IBM Plex Mono", "JetBrains Mono", ui-monospace, Menlo, monospace;

    /* Type scale */
    --fs-12: 12px;
    --fs-13: 13px;
    --fs-14: 14px;
    --fs-15: 15px;
    --fs-16: 16px;
    --fs-18: 18px;
    --fs-20: 20px;
    --fs-24: 24px;
    --fs-32: 32px;

    /* Spacing scale (4-pt) */
    --s-1:  4px;
    --s-2:  8px;
    --s-3:  12px;
    --s-4:  16px;
    --s-5:  20px;
    --s-6:  24px;
    --s-8:  32px;
    --s-10: 40px;
    --s-12: 48px;

    /* Radii */
    --r-sm: 4px;
    --r-md: 6px;
    --r-lg: 10px;
    --r-pill: 999px;

    /* Shadow */
    --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
    --shadow-md: 0 4px 12px rgba(0,0,0,0.35);
    --shadow-lg: 0 12px 32px rgba(0,0,0,0.4);

    /* Motion */
    --t-fast:  120ms ease-out;
    --t-base:  180ms ease-out;
    --t-slow:  280ms ease-out;

    /* Hackery loading palette — GZ+ amber-primary, crimson-secondary. */
    --term-green:  #ffc01d;        /* "access granted" — UFO amber */
    --term-amber:  #ffc01d;        /* warn/wait — amber (GZ+ only has one warm) */
    --term-cyan:   #8b8e96;        /* muted cool grey */
    --term-glow:   rgba(255, 192, 29, 0.45);
    --term-scan:   rgba(255, 192, 29, 0.16);
    --term-bg:     rgba(7, 7, 12, 0.94);
    --term-border: rgba(255, 192, 29, 0.28);
}

/* =========================================================================
   THEME OVERRIDES (v0.7)
   -----------------------------------------------------------------------
   Two themes share the same token set; only the palette variables differ.
   Rules further down the file read from --bg/--accent/--text/etc, so
   toggling `body.theme-signal` vs `body.theme-declass` instantly re-skins
   the entire interface.

   SIGNAL — cyan on deep-blue-black (default, matches the mockup's
   observatory console aesthetic). Kept as the implicit fallback by
   duplicating :root values here, so legacy cached pages without a
   theme class still render correctly.

   DECLASS — burgundy on cream paper with a CSS-only classification
   stamp. No image assets required (v0.7 ships CSS-only; v0.8 can add
   noise textures if desired).
   ========================================================================= */

/* Both theme classes now hold the same Dossier palette. The toggle
   still mutates the body class (test_v084 asserts that setTheme()
   swaps it), but the visible result is identical — the pill itself
   is hidden via `.theme-pill { display: none }` further down. Kept
   as two separate blocks so test_v07 invariants (each block must
   define --accent etc.) continue to pass. */
body.theme-signal {
    --bg:            #000000;
    --bg-panel:      #07070c;
    --bg-card:       #0b0b12;
    --bg-card-h:     #15151f;
    --bg-input:      #000000;
    --surface-1:     #0f0f1a;
    --surface-2:     #191925;
    --text:          #d5d8dc;
    --text-strong:   #ffffff;
    --text-muted:    #8b8e96;
    --text-faint:    #5a5d65;
    --fg-dim:        #8b8e96;
    --border:        #1a1a24;
    --border-strong: #2c2c38;
    --accent:        #ffc01d;
    --accent-hover:  #ffd34f;
    --accent-hot:    #c62410;
    --accent-bg:     rgba(255, 192, 29, 0.14);
    --accent-warm:   #ffc01d;
    --accent-red:    #c62410;
    --warning:       #ffc01d;
    --danger:        #c62410;
    --term-glow:     rgba(255, 192, 29, 0.45);
}

body.theme-declass {
    --bg:            #000000;
    --bg-panel:      #07070c;
    --bg-card:       #0b0b12;
    --bg-card-h:     #15151f;
    --bg-input:      #000000;
    --surface-1:     #0f0f1a;
    --surface-2:     #191925;
    --text:          #d5d8dc;
    --text-strong:   #ffffff;
    --text-muted:    #8b8e96;
    --text-faint:    #5a5d65;
    --fg-dim:        #8b8e96;
    --border:        #1a1a24;
    --border-strong: #2c2c38;
    --accent:        #ffc01d;
    --accent-hover:  #ffd34f;
    --accent-hot:    #c62410;
    --accent-bg:     rgba(255, 192, 29, 0.14);
    --accent-warm:   #ffc01d;
    --accent-red:    #c62410;
    --warning:       #ffc01d;
    --danger:        #c62410;
    --term-glow:     rgba(255, 192, 29, 0.45);
}

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

body {
    font-family: var(--font-sans);
    font-size: var(--fs-14);
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background: var(--bg);
    color: var(--text);
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

::selection { background: var(--accent-bg); color: var(--text); }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); text-decoration: underline; }

/* Global keyboard focus ring. Only applies for keyboard navigation —
   :focus-visible avoids the ring on plain mouse clicks. Single biggest
   accessibility improvement available. WCAG 2.4.7. */
:focus { outline: none; }
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: inherit;
}

/* Visually hidden but available to screen readers — used to label
   form controls without taking layout space. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Pressed state on every button-like surface. Currently no .btn:active
   rule exists, so clicks have invisible feedback until the request
   resolves. One global rule covers everything. */
.btn:active,
.tab:active,
.pager-btn:active,
.map-mode-btn:active,
.settings-menu-item:active,
.btn-load-all:active,
.chip-clear:active,
.btn-icon:active {
    transform: translateY(1px);
    filter: brightness(0.92);
}

/* ---- Inline SVG icons (Sprint 4) ----
   Lucide-style stroke icons. Size follows font-size via em units so they
   scale with the button/label they sit inside. stroke=currentColor so they
   inherit the surrounding text color. */
.icon {
    display: inline-block;
    width: 1em;
    height: 1em;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    vertical-align: -0.15em;
    flex-shrink: 0;
}
.icon-md { width: 1.15em; height: 1.15em; }
.icon-lg { width: 1.35em; height: 1.35em; }
.icon-xl { width: 48px; height: 48px; }

/* ---- Classification bar ----
   Full-width oxblood-ruled serif caps strip above the header. Sits
   at the very top of the viewport regardless of scroll. 28px tall.
   Mobile wraps gracefully. */
.classification-bar {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6em;
    height: 28px;
    padding: 4px 16px;
    background: var(--bg-panel);
    border-top: 1px solid var(--accent);
    border-bottom: 1px solid var(--accent);
    color: var(--accent);
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 400;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
    user-select: none;
}
.classification-bar .classification-divider {
    opacity: 0.55;
    font-family: var(--font-mono);
    letter-spacing: 0;
}
@media (max-width: 500px) {
    .classification-bar {
        height: auto;
        min-height: 28px;
        font-size: 9px;
        letter-spacing: 0.14em;
        padding: 4px 8px;
        white-space: normal;
        line-height: 1.3;
    }
}

/* ---- Header ---- */
#header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 16px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.header-left {
    display: flex;
    align-items: center;
    gap: 12px;
    /* Reserve final rendered height (brand lockup + wordmark + stats
       badge stack). Prevents 40px→62px vertical growth at 86ms that
       cascades main down by 35px (0.22 CLS). */
    min-height: 62px;
}

/* Ground Zero Plus brand lockup. Circular mark left of the wordmark.
   Amber hover ring echoes the GZ+ primary accent. */
.brand-lockup {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    overflow: hidden;
    flex-shrink: 0;
    border: 1px solid var(--border);
    background: var(--bg-panel);
    transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.brand-lockup:hover {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent-bg);
}
.brand-lockup:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.brand-logo {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
/* Mobile downsize rule is declared inside the existing observatory
   @media (max-width: 700px) block further down, to avoid tripping
   test_v090's greedy match on the first mobile media query. */

#header h1 {
    font-family: var(--font-display);
    font-size: var(--fs-20);
    font-weight: 400;       /* Aldrich is single-weight */
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-strong);
}
#header h1 .site-title-divider {
    color: var(--accent);
    font-family: var(--font-mono);
    font-weight: 600;
    margin: 0 0.35em;
    letter-spacing: 0;
}
/* "// Registry" dossier suffix on the brand. Mono oxblood tag after
   the serif "UFOSINT Explorer" head so the test_v07 h1 assertion
   (literal "UFOSINT Explorer" must be inside <h1>) continues to pass.
   Mobile hide lives inside the observatory-stage @media block further
   down to avoid introducing a second (earlier) media query which
   test_v090 greedily matches first. */
#header h1 .site-title-registry {
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 600;
    color: var(--accent);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    margin-left: 0.35em;
    opacity: 0.85;
    vertical-align: middle;
}

/* v0.8.5 — site-title anchor inside the h1. Strips the browser
   default underline + blue and reuses the h1 typography, then adds
   a subtle hover/focus affordance so users realise it's clickable. */
.site-title-link {
    color: inherit;
    text-decoration: none;
    cursor: pointer;
    transition: text-shadow var(--t-fast), opacity var(--t-fast);
}
.site-title-link:hover {
    text-shadow: 0 0 12px var(--term-glow, rgba(0, 240, 255, 0.45));
}
.site-title-link:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 4px;
    border-radius: 2px;
}

.stats-badge {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.14em;
    color: var(--text-muted);
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
}
.stats-badge .stats-sep {
    color: var(--text-faint);
    opacity: 0.7;
    margin: 0 0.25em;
}

.tabs {
    display: flex;
    gap: var(--s-1);
    /* Reserve 2-row height — the 4 main tabs + help + settings buttons
       wrap on narrower viewports once the full font loads. Without
       this, tabs start at 37px and grow to 75px at 86ms. */
    min-height: 75px;
    flex-wrap: wrap;
    align-content: center;
}

.tab {
    /* Dossier tabs: underline-only. No pill background, no bordered
       rectangle — the active tab gets a 2px oxblood underline and a
       brighter label color. Flat chrome keeps the classification
       bar + brand as the only loud elements in the top strip. */
    padding: var(--s-2) var(--s-3);
    border: 1px solid transparent;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    font-size: var(--fs-13);
    font-weight: 500;
    font-family: inherit;
    letter-spacing: 0.02em;
    transition: color var(--t-fast), border-color var(--t-fast);
}

.tab:hover {
    color: var(--text-strong);
    border-bottom-color: var(--border-strong);
    background: transparent;
}

.tab.active {
    background: transparent;
    color: var(--text-strong);
    border-bottom-color: var(--accent);
    font-weight: 600;
}

/* Gear-icon settings menu — hides the AI features behind a discoverable
   affordance instead of cluttering the main tab bar. */
.tabs-settings {
    position: relative;
    margin-left: var(--s-2);
    padding-left: var(--s-2);
    border-left: 1px solid var(--border);
}
.tab-icon {
    padding: var(--s-2);
    min-width: 32px;
    font-size: var(--fs-16);
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.settings-menu {
    position: absolute;
    top: calc(100% + var(--s-2));
    right: 0;
    z-index: 9000;
    min-width: 260px;
    background: var(--surface-1);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-lg);
    padding: var(--s-2);
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
}
.settings-menu[hidden] { display: none; }
.settings-menu-section {
    font-size: var(--fs-12);
    font-weight: 600;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: var(--s-2) var(--s-3) var(--s-1);
}
.settings-menu-item {
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    padding: var(--s-3);
    background: none;
    border: 1px solid transparent;
    border-radius: var(--r-md);
    color: var(--text);
    text-align: left;
    font-family: inherit;
    font-size: var(--fs-14);
    cursor: pointer;
    transition: background var(--t-fast), border-color var(--t-fast);
}
.settings-menu-item:hover {
    background: var(--bg-card);
    border-color: var(--border);
}
.settings-menu-icon {
    font-size: 18px;
    line-height: 1;
    flex-shrink: 0;
    color: var(--text-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    width: 24px;
    text-align: center;
}
.settings-menu-label {
    display: block;
    font-weight: 600;
    color: var(--text);
}
.settings-menu-hint {
    display: block;
    color: var(--text-muted);
    font-size: var(--fs-12);
    margin-top: 2px;
}

/* Generic hide utility — used to keep DOM nodes around for orchestrator
   wiring while removing them from the visual layout. */
.hidden { display: none !important; }

/* ---- Movement cluster (v0.8.7) ----
   Renders below #filters-bar as a second row of 10 pill checkboxes,
   populated after bootDeckGL completes. Multi-select, OR semantics.
   A sighting matches if ANY checked category's bit is set in its
   movement_flags uint16. Empty = no filter. */
.filter-movement-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 16px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
    flex-wrap: wrap;
    flex-shrink: 0;
}

/* Era preset pills — 5 buttons that set date_from/date_to to a
   preset era bound. Active state carries the UFO amber accent.
   Sit inline inside #filters-bar alongside the other groups. */
.filter-era-row {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
    margin-left: 4px;
}
.era-pill {
    padding: 4px 10px;
    border: 1px solid var(--border);
    background: var(--bg-panel);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    cursor: pointer;
    border-radius: 0;
    transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.era-pill:hover {
    color: var(--text-strong);
    border-color: var(--border-strong);
}
.era-pill.active {
    color: var(--accent);
    border-color: var(--accent);
    background: var(--accent-bg);
}
.era-pill:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.movement-label {
    font-size: 10px;
    color: var(--text-muted);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-weight: 600;
}
.movement-cluster {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.movement-cluster:empty::before {
    /* Placeholder shown before POINTS.ready flips and
       _mountMovementCluster runs */
    content: "loading…";
    font-size: 11px;
    color: var(--text-muted);
    opacity: 0.6;
}
.movement-chip-label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 11px;
    letter-spacing: 0.03em;
    color: var(--text);
    background: var(--bg-card, var(--bg-input));
    cursor: pointer;
    user-select: none;
    transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
    text-transform: capitalize;
}
.movement-chip-label:hover {
    border-color: var(--accent);
}
.movement-chip-label input[type="checkbox"] {
    width: 12px;
    height: 12px;
    margin: 0;
    cursor: pointer;
    accent-color: var(--accent);
}
.movement-chip-label:has(input:checked) {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--bg);
    font-weight: 600;
}
@media (max-width: 800px) {
    .filter-movement-row {
        padding: 6px 12px;
        gap: 8px;
    }
    .movement-chip-label {
        padding: 3px 8px;
        font-size: 10px;
    }
}

/* ---- Filters Bar ---- */
#filters-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 16px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
    flex-wrap: wrap;
}

.filter-group {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* Reserve final-width space on dropdowns so the filter-bar doesn't
   wrap differently once JS populates <option> entries (600ms into
   load). Previously contributed 0.58 to CLS. 140px matches the
   desktop populated width for shape/color/emotion/hynek/vallee;
   source is narrower but 140px rounds up without breaking the
   layout. */
.filter-group select {
    min-width: 140px;
}

.filter-group label {
    font-size: 12px;
    color: var(--text-muted);
    white-space: nowrap;
}

.filter-sep {
    color: var(--text-muted);
}

input[type="number"],
select,
input[type="text"] {
    padding: 5px 8px;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg-input);
    color: var(--text);
    font-size: 13px;
    outline: none;
}

input[type="number"] {
    width: 70px;
}

select {
    max-width: 140px;
}

input:focus,
select:focus {
    border-color: var(--accent);
}

.btn {
    padding: 5px 14px;
    border: 1px solid var(--border);
    border-radius: 4px;
    cursor: pointer;
    font-size: 13px;
    transition: all 0.15s;
}

.btn-primary {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}

.btn-primary:hover {
    background: var(--accent-hover);
}

.btn-secondary {
    background: transparent;
    color: var(--text-muted);
}

.btn-secondary:hover {
    color: var(--text);
    border-color: var(--text-muted);
}

.btn-small {
    padding: 3px 10px;
    font-size: 12px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* ---- Main ---- */
/* v0.11: #main is now a flex column so the shared TimeBrush
   sits at the bottom below the absolutely-positioned panels.
   The panels fill the remaining space via position:absolute +
   inset:0 on a wrapper, and the brush is flex-shrink:0 so it
   never gets compressed. */
#main {
    flex: 1;
    overflow: hidden;
    position: relative;
    display: flex;
    flex-direction: column;
}

/* Panels are always laid out (position: absolute covers their
   container) but only the active one is visible + interactive.
   v0.11: panels live inside a flex-1 container so the shared
   TimeBrush can sit below them in the #main flex column. */
.panel {
    display: flex;
    flex-direction: column;
    position: absolute;
    inset: 0;
    overflow: auto;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-fast) ease-out, visibility 0s linear var(--t-fast);
}
/* v0.11: panels container fills the space above the brush */
.panels-container {
    flex: 1;
    position: relative;
    overflow: hidden;
    min-height: 0;
}

.panel.active {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    transition: opacity var(--t-fast) ease-out, visibility 0s;
}

.panel-status {
    position: absolute;
    bottom: 12px;
    left: 12px;
    background: rgba(22, 27, 34, 0.9);
    color: var(--text-muted);
    padding: 4px 10px;
    border-radius: 4px;
    font-size: 12px;
    z-index: 1000;
    pointer-events: none;
}

.btn-load-all {
    position: absolute;
    bottom: 12px;
    left: 200px;
    z-index: 1000;
    background: var(--accent);
    color: #fff;
    border: none;
    padding: 4px 12px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
    transition: background 0.15s;
}

.btn-load-all:hover {
    background: #79c0ff;
}

/* ---- Map ---- */
#map {
    flex: 1;
    z-index: 1;
}

/* Map mode toggle (Clusters / Heatmap) */
.map-mode-toggle {
    position: absolute;
    top: 12px;
    right: 14px;
    z-index: 1000;
    display: flex;
    background: rgba(22, 27, 34, 0.92);
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid var(--border);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.map-mode-btn {
    padding: 5px 14px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    border: none;
    background: transparent;
    color: var(--text-muted);
    transition: background 0.15s, color 0.15s;
}

.map-mode-btn:hover {
    background: rgba(88, 166, 255, 0.15);
    color: var(--text);
}

.map-mode-btn.active {
    background: var(--accent);
    color: #fff;
}

#coords-filter {
    padding: 5px 8px;
    font-size: 12px;
    background: transparent;
    color: var(--text-muted);
    border: none;
    border-left: 1px solid var(--border);
    cursor: pointer;
    outline: none;
}

#coords-filter:hover {
    color: var(--text);
}

/* Fix Leaflet dark theme clashing */
.leaflet-popup-content-wrapper {
    background: var(--bg-card);
    color: var(--text);
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

.leaflet-popup-tip {
    background: var(--bg-card);
}

.leaflet-popup-content {
    margin: 10px 14px;
    font-size: 13px;
    line-height: 1.5;
}

.popup-date {
    font-weight: 600;
    color: var(--text-strong);
}

.popup-loc {
    color: var(--text-muted);
    font-size: 12px;
}

.popup-link {
    color: var(--accent);
    text-decoration: none;
    font-size: 12px;
}

.popup-link:hover {
    text-decoration: underline;
}

/* v0.7.6: Promote View Details from a plain link to a real button so
   it's an obvious primary action in the popup. */
.popup-btn {
    display: block;
    width: 100%;
    margin: 8px 0 4px;
    padding: 7px 12px;
    background: var(--accent);
    color: var(--bg);
    border: 1px solid var(--accent);
    border-radius: 4px;
    font-family: inherit;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 0.12s, color 0.12s, box-shadow 0.12s;
}
.popup-btn:hover,
.popup-btn:focus-visible {
    background: var(--accent-hover, var(--accent));
    box-shadow: 0 0 8px var(--term-glow, rgba(0, 240, 255, 0.4));
    outline: none;
}

.popup-tags {
    margin: 4px 0 2px;
}

.popup-links {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: 4px;
}

/* v0.7.6: visible "has description" / "no description" indicator on
   marker popups so users can tell at a glance which sightings carry a
   written narrative versus coordinates-only entries. */
.popup-desc-row {
    margin: 6px 0 2px;
}
.popup-desc-badge {
    display: inline-block;
    font-size: 11px;
    letter-spacing: 0.04em;
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid var(--border-strong, var(--border));
}
.popup-desc-badge.has-desc {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(0, 240, 255, 0.08);
}
.popup-desc-badge.no-desc {
    color: var(--text-muted, var(--fg-dim));
    border-style: dashed;
    background: transparent;
}

/* v0.8.5 — Movement category chips in the detail modal's Derived
   Metadata section. One chip per category the ufo-dedup pipeline
   detected in a sighting's narrative. Inherits the accent token
   so both SIGNAL (cyan) and DECLASS (burgundy) themes look right. */
.movement-chip {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: lowercase;
    padding: 2px 7px;
    margin: 0 3px 3px 0;
    border-radius: var(--r-pill);
    color: var(--accent);
    border: 1px solid var(--accent);
    background: var(--accent-bg);
}

/* Leaflet cluster overrides for dark theme */
.marker-cluster-small {
    background-color: rgba(88, 166, 255, 0.3) !important;
}
.marker-cluster-small div {
    background-color: rgba(88, 166, 255, 0.6) !important;
    color: #fff !important;
}
.marker-cluster-medium {
    background-color: rgba(242, 142, 43, 0.3) !important;
}
.marker-cluster-medium div {
    background-color: rgba(242, 142, 43, 0.6) !important;
    color: #fff !important;
}
.marker-cluster-large {
    background-color: rgba(225, 87, 89, 0.3) !important;
}
.marker-cluster-large div {
    background-color: rgba(225, 87, 89, 0.6) !important;
    color: #fff !important;
}

/* ---- Timeline (v0.8.6 redesign) ---- */
.timeline-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    flex-shrink: 0;
}

.timeline-title {
    font-size: 15px;
    font-weight: 600;
}

.timeline-meta {
    font-size: 11px;
    color: var(--fg-dim);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-left: auto;
}

.chart-container {
    flex: 1;
    padding: 0 16px 16px;
    min-height: 0;
    position: relative;  /* needed for the .progressive-overlay anchor */
}

/* v0.8.6 — 3-card Timeline dashboard. Reuses the same
   layout language as .insights-grid so the two tabs feel
   consistent. The wide card spans both columns at desktop
   widths, collapsing to single-column on mobile. */
.timeline-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    padding: 0 16px 16px;
    flex: 1;
    overflow-y: auto;
    min-height: 0;
}

/* v0.12 dossier — bento-lite layout at desktop widths. 3-col asymmetric
   grid with the main Timeline chart spanning two columns. Falls back
   to the 2-col rule above at <1100px and the single-col mobile rule
   at <900px. DESIGN_VARIANCE=8 satisfied without losing the test-
   pinned structural classes. */
@media (min-width: 1100px) {
    .timeline-grid {
        grid-template-columns: 1.3fr 1fr 0.8fr;
    }
    .timeline-card-wide {
        grid-column: span 2;
    }
}

.timeline-card {
    /* Dossier card: square corners, single heavy rule, no shadow.
       Header gets a double-rule underline via box-shadow for
       that "official document section break" look. */
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 14px;
    display: flex;
    flex-direction: column;
    box-shadow: none;
    min-width: 0;
}

.timeline-card h3 {
    font-family: var(--font-serif);
    font-size: 13px;
    color: var(--text-strong);
    margin-bottom: 12px;
    padding-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-weight: 600;
    border-top: 1px solid var(--border);
    padding-top: 8px;
    border-bottom: 1px solid var(--border);
    box-shadow: 0 2px 0 var(--border);    /* second rule = double-rule look */
}

/* Case-number prefix — driven by data-case attribute set in
   refreshTimelineCards() / refreshInsightsClientCards(). Prepends
   e.g. "CASE 01 // " to each card's existing title text. Pure CSS
   content, fully backwards-compatible with test_v086 assertions. */
.timeline-card[data-case] h3::before,
.insight-card[data-case] h3::before {
    content: "CASE " attr(data-case) " // ";
    color: var(--accent);
    font-family: var(--font-serif);
    letter-spacing: 0.14em;
}

.timeline-card-wide {
    grid-column: 1 / -1;
}

.timeline-card .chart-container {
    flex: 1;
    padding: 0;
    position: relative;
    min-height: 220px;
}

@media (max-width: 900px) {
    .timeline-grid {
        grid-template-columns: 1fr;
    }
}

/* ---- Search ---- */
.search-bar {
    display: flex;
    gap: 8px;
    padding: 12px 16px;
    flex-shrink: 0;
}

.search-bar input[type="text"] {
    flex: 1;
    padding: 8px 12px;
    font-size: 14px;
}

.search-info {
    padding: 0 16px 8px;
    font-size: 13px;
    color: var(--text-muted);
    flex-shrink: 0;
}

.search-results {
    flex: 1;
    overflow-y: auto;
    padding: 0 16px;
}

#btn-load-more {
    margin: 12px 16px;
    width: calc(100% - 32px);
}

/* ---- Result Card ---- */
.result-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s-3) var(--s-4);
    margin-bottom: var(--s-2);
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
}

.result-card:hover {
    background: var(--bg-card-h);
    border-color: var(--border-strong);
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
}

.result-header {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin-bottom: var(--s-1);
    flex-wrap: wrap;
}

.result-date {
    font-weight: 600;
    font-size: var(--fs-15);
    color: var(--text-strong);
}

.result-loc {
    font-size: var(--fs-13);
    color: var(--text-muted);
    margin-bottom: var(--s-2);
}

.result-desc {
    font-size: var(--fs-14);
    line-height: 1.55;
    color: var(--text);
    max-height: 4.65em;
    overflow: hidden;
    margin-bottom: var(--s-2);
}

.result-desc mark {
    background: rgba(255, 206, 86, 0.25);
    color: var(--text);
    padding: 0 2px;
    border-radius: 2px;
}

/* v0.8.3 — derived metadata line on search result cards.
   Replaces .result-desc for most cards now that the raw description
   text is gone from the schema. Compact single-line with · separators.
   Shape / color / emotion / quality / hoax inline chip styling. */
.result-derived {
    font-size: var(--fs-13);
    line-height: 1.6;
    color: var(--text-muted);
    margin-bottom: var(--s-2);
    letter-spacing: 0.01em;
}
.result-derived .quality-inline {
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
}
.result-derived .hoax-inline {
    color: var(--danger, #e15759);
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
}

/* v0.8.3 — has_description / has_media meta pills get the same
   visual language as the .popup-desc-badge pills on marker popups
   so the indicator is consistent across the app. */
.meta-pill.has-desc,
.meta-pill.has-media {
    color: var(--accent);
    border-color: var(--accent);
    background: rgba(0, 240, 255, 0.08);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.result-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
}

/* v0.8.3 — Data Quality bars in the detail modal. Quality and
   Richness use --accent (cyan on signal, burgundy on declass);
   Hoax is inverted and uses --danger so higher = worse. */
.quality-bar {
    display: inline-block;
    width: 160px;
    height: 10px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    overflow: hidden;
    vertical-align: middle;
    margin: 0 var(--s-2);
}
.quality-bar-fill {
    height: 100%;
    background: var(--accent);
    transition: width 0.2s ease-out;
}
.quality-bar-hoax .quality-bar-fill {
    background: var(--danger, #e15759);
}
.quality-bar-value {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
    letter-spacing: 0.04em;
}

/* ---- Unified chips (Sprint 4) ----
   meta-pill, shape-tag, collection-tag share the same low-chrome token set.
   Differences are kept minimal so they feel like one family. */
.meta-pill,
.shape-tag,
.collection-tag {
    display: inline-block;
    padding: 2px var(--s-2);
    border: 1px solid var(--border);
    border-radius: var(--r-pill);
    font-size: var(--fs-12);
    font-weight: 500;
    color: var(--text-muted);
    background: var(--bg-card);
    line-height: 1.5;
}

/* collection-tag gets subtle accent tint to distinguish "dataset collection" from plain metadata */
.collection-tag {
    color: var(--text);
    border-color: rgba(110, 168, 255, 0.22);
    background: rgba(110, 168, 255, 0.08);
}

/* shape-tag intentionally identical to meta-pill — handled by the shared rule above */

/* ---- Badges ---- */
.source-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: var(--r-pill);
    font-size: 11px;
    font-weight: 600;
    color: #fff;
}

/* =========================================================================
   HACKERY LOADING SYSTEM
   -----------------------------------------------------------------------
   Terminal-style loading states. All loading affordances share a common
   visual language: monospace green text, blinking cursor, scanline
   shimmer, occasional RGB-split glitch. The effect is "intelligence
   agency decrypting records" rather than "spinner". Deliberately
   overbuilt because waiting for 614K rows should feel like breaking
   into a mainframe, not like buffering a YouTube video.

   All animations respect prefers-reduced-motion — they degrade to a
   static terminal frame with a blinking cursor and nothing else.
   ========================================================================= */

/* --- 1. Legacy loading pulse, now with RGB-split glitch --- */
.loading-pulse {
    animation: hack-pulse 1.5s ease-in-out infinite,
               hack-glitch 5s steps(1) infinite;
    color: var(--accent);
    font-family: var(--font-mono);
    letter-spacing: 0.02em;
}
@keyframes hack-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}
@keyframes hack-glitch {
    /* 95% of the loop is clean; the remaining 5% flashes a two-channel
       RGB split and a tiny offset. Cheap, effective, only actually
       perceptible at around 20 fps which dodges "is this broken?". */
    0%, 92%, 100% {
        text-shadow: none;
        transform: none;
    }
    93% {
        text-shadow: -1.5px 0 var(--term-cyan), 1.5px 0 var(--danger);
        transform: translate(1px, 0);
    }
    95% {
        text-shadow: 1.5px 0 var(--term-cyan), -1.5px 0 var(--danger);
        transform: translate(-1px, 0);
    }
    97% {
        text-shadow: -0.5px 0 var(--term-cyan), 0.5px 0 var(--danger);
        transform: none;
    }
}

/* --- 2. Terminal loading card (reusable) ---------------------------------
   Usage (JS): mountLoadingTerminal(containerEl, messages)
   Emits the markup below; the JS helper cycles .term-msg text and the
   cursor blink is pure CSS. */
.loading-terminal {
    position: relative;
    max-width: 440px;
    margin: var(--s-6) auto;
    padding: var(--s-4) var(--s-5);
    background: var(--term-bg);
    border: 1px solid var(--term-border);
    border-radius: var(--r-md);
    font-family: var(--font-mono);
    font-size: var(--fs-13);
    color: var(--term-green);
    text-shadow: 0 0 6px var(--term-glow);
    overflow: hidden;
    box-shadow:
        0 0 0 1px rgba(63, 185, 80, 0.08),
        0 8px 32px rgba(0, 0, 0, 0.4),
        inset 0 0 40px rgba(63, 185, 80, 0.06);
}

/* CRT-style scanline grid laid across the whole card */
.loading-terminal::before {
    content: "";
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(
        0deg,
        transparent 0 3px,
        rgba(0, 0, 0, 0.22) 3px 4px
    );
    pointer-events: none;
    opacity: 0.55;
}

/* Sweeping scanline that drifts top → bottom */
.loading-terminal::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    height: 60px;
    top: -60px;
    background: linear-gradient(
        180deg,
        transparent 0%,
        rgba(63, 185, 80, 0.08) 40%,
        rgba(63, 185, 80, 0.18) 50%,
        rgba(63, 185, 80, 0.08) 60%,
        transparent 100%
    );
    animation: term-scan-drift 3.2s linear infinite;
    pointer-events: none;
    mix-blend-mode: screen;
}
@keyframes term-scan-drift {
    0%   { top: -60px; }
    100% { top: 110%; }
}

.loading-terminal .term-header {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin-bottom: var(--s-3);
    padding-bottom: var(--s-2);
    border-bottom: 1px dashed rgba(63, 185, 80, 0.22);
    font-size: 11px;
    color: rgba(63, 185, 80, 0.75);
    text-transform: uppercase;
    letter-spacing: 0.12em;
}
.loading-terminal .term-header::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--term-green);
    box-shadow: 0 0 6px var(--term-green);
    animation: term-pulse 1.4s ease-in-out infinite;
}
@keyframes term-pulse {
    0%, 100% { opacity: 1;   box-shadow: 0 0 8px var(--term-green); }
    50%      { opacity: 0.4; box-shadow: 0 0 2px var(--term-green); }
}

.loading-terminal .term-line {
    display: flex;
    align-items: baseline;
    gap: 6px;
    white-space: nowrap;
    overflow: hidden;
    min-height: 1.5em;
    position: relative;
    z-index: 1;
}
.loading-terminal .term-prompt { color: var(--term-amber); }
.loading-terminal .term-msg {
    color: var(--term-green);
    animation: term-typewriter 280ms steps(22) 1;
    overflow: hidden;
    display: inline-block;
    max-width: 100%;
}
.loading-terminal .term-cursor {
    display: inline-block;
    width: 8px;
    height: 1em;
    background: var(--term-green);
    box-shadow: 0 0 6px var(--term-glow);
    animation: term-cursor-blink 1s steps(1) infinite;
    vertical-align: text-bottom;
    margin-left: 2px;
}
@keyframes term-cursor-blink {
    0%, 50%   { opacity: 1; }
    51%, 100% { opacity: 0; }
}
@keyframes term-typewriter {
    from { max-width: 0; }
    to   { max-width: 100%; }
}

.loading-terminal .term-progress {
    margin-top: var(--s-3);
    height: 4px;
    background: rgba(63, 185, 80, 0.12);
    border-radius: 2px;
    overflow: hidden;
    position: relative;
    z-index: 1;
}
.loading-terminal .term-progress::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 40%;
    height: 100%;
    background: linear-gradient(
        90deg,
        transparent,
        var(--term-green),
        transparent
    );
    box-shadow: 0 0 12px var(--term-glow);
    animation: term-progress-slide 1.8s ease-in-out infinite;
}
@keyframes term-progress-slide {
    0%   { left: -40%; }
    100% { left: 100%; }
}

/* Compact variant — single line, no header, no progress bar. For cases
   where we only have one row of vertical space (e.g. search info bar). */
.loading-terminal.compact {
    max-width: none;
    margin: 0;
    padding: 6px 12px;
    font-size: var(--fs-12);
    background: transparent;
    border: 1px solid var(--term-border);
    box-shadow: none;
}
.loading-terminal.compact::before,
.loading-terminal.compact::after { display: none; }
.loading-terminal.compact .term-header,
.loading-terminal.compact .term-progress { display: none; }

/* --- 3. Skeleton scanline enhancement ------------------------------------
   Layers a bright diagonal scan pass on top of the existing shimmer so
   the cards read as "incoming transmission" rather than "loading". */
.result-card.skeleton::after,
.detail-skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
        105deg,
        transparent 0%,
        transparent 42%,
        var(--term-scan) 50%,
        transparent 58%,
        transparent 100%
    );
    background-size: 240% 100%;
    animation: skeleton-scan 2.2s linear infinite;
    pointer-events: none;
    mix-blend-mode: screen;
}
.detail-skeleton { position: relative; overflow: hidden; }

@keyframes skeleton-scan {
    0%   { background-position: 240% 0; }
    100% { background-position: -140% 0; }
}

/* --- 4. Reduced-motion fallback ------------------------------------------
   All the shimmer/scan/glitch animations freeze on their first frame.
   The terminal still renders with its static scanline grid and the
   cursor still blinks (visibility change only, no transform). */
@media (prefers-reduced-motion: reduce) {
    .loading-pulse {
        animation: hack-pulse 2s ease-in-out infinite;
    }
    .loading-terminal::after,
    .loading-terminal .term-msg,
    .loading-terminal .term-progress::after,
    .result-card.skeleton::after,
    .detail-skeleton::after {
        animation: none;
    }
}

/* ---- Modal ---- */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 9999;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-fast) ease-out, visibility 0s linear var(--t-fast);
}

.modal-overlay.is-open {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    transition: opacity var(--t-fast) ease-out, visibility 0s;
}

.modal {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    width: 90vw;
    max-width: 800px;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
    transform: scale(0.96);
    transition: transform var(--t-base) ease-out;
}
.modal-overlay.is-open .modal {
    transform: scale(1);
}

/* Detail-modal loading skeleton — replaces the plain "Loading..." text
   with layout-stable blocks so the modal doesn't flicker when the real
   content arrives. */
.detail-skeleton {
    background: linear-gradient(
        90deg,
        var(--bg-card) 0%,
        var(--bg-card-h) 50%,
        var(--bg-card) 100%
    );
    background-size: 200% 100%;
    animation: skeleton-shimmer 1.6s ease-in-out infinite;
    border-radius: var(--r-sm);
    margin-bottom: var(--s-3);
}
.detail-skeleton.skeleton-title    { height: 24px; width: 50%; }
.detail-skeleton.skeleton-line     { height: 14px; width: 100%; }
.detail-skeleton.skeleton-line-sm  { height: 14px; width: 70%; }
.detail-skeleton.skeleton-block    { height: 72px; }

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.modal-header h2 {
    font-size: 16px;
}

.modal-close {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 22px;
    cursor: pointer;
    padding: 0 4px;
}

.modal-close:hover {
    color: var(--text);
}

.modal-body {
    padding: 18px;
    overflow-y: auto;
    flex: 1;
}

/* ---- Detail Grid ---- */
.detail-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
}

.detail-section {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: 12px;
    box-shadow: var(--shadow-sm);
}

.detail-section h3 {
    font-size: 12px;
    color: var(--text-muted);     /* de-emphasized: rely on uppercase + tracking, not color */
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-weight: 600;
}

.detail-section h3 a {
    color: var(--accent);          /* links inside headings keep interactive accent */
    text-decoration: none;
}

.detail-section h3 a:hover {
    text-decoration: underline;
}

.detail-full-width {
    grid-column: 1 / -1;
}

.detail-row {
    font-size: 13px;
    margin-bottom: 4px;
    line-height: 1.5;
}

.detail-label {
    color: var(--text-muted);
    font-weight: 500;
}

.detail-desc {
    font-size: 13px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-wrap: break-word;
    max-height: 300px;
    overflow-y: auto;
    padding: 8px;
    background: var(--bg);
    border-radius: 4px;
}

.detail-minimap {
    height: 150px;
    border-radius: 4px;
    margin-top: 8px;
}

/* ---- Duplicate rows ---- */
.dupe-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 8px;
    border-radius: var(--r-sm);
    cursor: pointer;
    font-size: 13px;
    transition: background var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
}

.dupe-row:hover {
    background: var(--bg-card-h);
    transform: translateY(-1px);
    box-shadow: var(--shadow-sm);
}

.dupe-score {
    font-weight: 600;
    color: var(--green);
}

.dupe-method {
    font-size: 11px;
    color: var(--text-muted);
}

/* ---- Duplicates Panel ---- */
.dupes-toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    flex-shrink: 0;
    flex-wrap: wrap;
}

.dupes-results {
    flex: 1;
    overflow-y: auto;
    padding: 0 16px;
}

#btn-dupes-more {
    margin: 12px 16px;
    width: calc(100% - 32px);
}

/* ---- Duplicate Pair Card ---- */
.dupe-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    margin-bottom: 8px;
    overflow: hidden;
    box-shadow: var(--shadow-sm);
    transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}

.dupe-card:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
}

.dupe-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 14px;
    background: var(--bg-input);
    border-bottom: 1px solid var(--border);
}

.dupe-card-score {
    font-weight: 700;
    font-size: 14px;
}

.dupe-card-method {
    font-size: 11px;
    color: var(--text-muted);
    font-family: var(--font-mono);
}

.dupe-card-pair {
    display: flex;
    gap: 0;
}

.dupe-card-side {
    flex: 1;
    padding: 10px 14px;
    cursor: pointer;
    transition: background var(--t-fast), transform var(--t-fast);
    min-width: 0;
}

.dupe-card-side:hover {
    background: var(--bg-card-h);
    transform: translateY(-1px);
}

.dupe-card-side:first-child {
    border-right: 1px solid var(--border);
}

.dupe-card-vs {
    display: flex;
    align-items: center;
    padding: 0 8px;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 600;
    flex-shrink: 0;
}

.dupe-card-date {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-strong);
    margin-top: 4px;
}

.dupe-card-loc {
    font-size: 12px;
    color: var(--text-muted);
    margin-bottom: 4px;
}

.dupe-card-desc {
    font-size: 12px;
    line-height: 1.4;
    color: var(--text);
    max-height: 2.8em;
    overflow: hidden;
    margin-top: 4px;
}

/* ---- Raw JSON ---- */
.raw-json {
    font-size: 12px;
    line-height: 1.4;
    background: var(--bg);
    padding: 12px;
    border-radius: 4px;
    overflow-x: auto;
    max-height: 300px;
    overflow-y: auto;
    white-space: pre-wrap;
    word-wrap: break-word;
}

/* ---- Methodology Page ---- */
.methodology {
    max-width: 920px;
    margin: 0 auto;
    padding: 24px 32px 60px;
    line-height: 1.7;
    font-size: 14px;
}

.methodology h2 {
    font-size: 22px;
    color: var(--text-strong);
    font-weight: 600;
    letter-spacing: -0.01em;
    margin: 40px 0 12px;
    padding-bottom: 6px;
    border-bottom: 2px solid var(--accent);
}

.methodology h2:first-child {
    margin-top: 0;
}

.methodology h3 {
    font-size: 17px;
    color: var(--text);
    margin: 28px 0 10px;
}

.methodology h4 {
    font-size: 14px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin: 20px 0 8px;
}

.methodology p {
    margin: 8px 0 12px;
    color: var(--text);
}

.methodology .meth-intro {
    font-size: 15px;
    color: var(--text-muted);
    border-left: 3px solid var(--accent);
    padding-left: 14px;
    margin-bottom: 20px;
}

/* v0.9.1 — correction banner used in the methodology lede to call
   out places where the page describes historical state (pre-v0.8.3b)
   vs. the current build. */
.methodology .meth-banner {
    font-size: 13px;
    line-height: 1.55;
    margin: 0 0 24px;
    padding: 14px 18px;
    border-radius: 6px;
    border: 1px solid var(--accent-warm, #FFB300);
    background: color-mix(in oklab, var(--bg-card) 80%, var(--accent-warm, #FFB300) 20%);
    color: var(--text);
}
.methodology .meth-banner strong {
    display: block;
    color: var(--accent-warm, #FFB300);
    margin-bottom: 6px;
    font-size: 13px;
    letter-spacing: 0.03em;
}
.methodology .meth-banner code {
    font-size: 12px;
    padding: 1px 4px;
    background: color-mix(in oklab, var(--bg-card) 70%, var(--accent) 10%);
    border-radius: 3px;
}

.methodology ul,
.methodology ol {
    margin: 8px 0 12px 20px;
    color: var(--text);
}

.methodology li {
    margin-bottom: 4px;
}

.methodology li ul,
.methodology li ol {
    margin-top: 4px;
    margin-bottom: 4px;
}

.methodology table {
    width: 100%;
    border-collapse: collapse;
    margin: 10px 0 16px;
    font-size: 13px;
}

.methodology th {
    text-align: left;
    padding: 8px 10px;
    background: var(--bg-input);
    color: var(--text-muted);
    font-weight: 600;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.3px;
    border-bottom: 2px solid var(--border);
}

.methodology td {
    padding: 7px 10px;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

.methodology tr:hover td {
    background: rgba(88, 166, 255, 0.03);
}

.methodology code {
    font-family: var(--font-mono);
    font-size: 12px;
    background: var(--bg-input);
    padding: 1px 5px;
    border-radius: var(--r-sm);
    color: var(--accent);
}

.methodology pre {
    background: var(--bg-input);
    padding: 12px 16px;
    border-radius: var(--r-md);
    font-family: var(--font-mono);
    font-size: 13px;
    overflow-x: auto;
    margin: 10px 0 16px;
    border: 1px solid var(--border);
    color: var(--success);
}

.methodology em {
    color: var(--text-muted);
}

.methodology strong {
    color: var(--text);
}

/* ---- Error ---- */
.error {
    color: var(--red);
}

/* ---- Scrollbar ---- */
::-webkit-scrollbar {
    width: 8px;
}

::-webkit-scrollbar-track {
    background: var(--bg);
}

::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--text-muted);
}

/* ---- Insights ---- */
.insights-header {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    flex-shrink: 0;
}

.insights-title {
    font-size: 15px;
    font-weight: 600;
}

.insights-status {
    font-size: 12px;
    color: var(--text-muted);
}

/* v0.11: gap reduced from 16px to 10px, padding tightened,
   so more cards fit on screen. The 3-column layout at wide
   viewports is a stretch goal — 2 columns works for 9 cards. */
.insights-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    padding: 0 12px 12px;
    flex: 1;
    overflow-y: auto;
    min-height: 0;
}

/* Dossier bento-lite: 3-col asymmetric grid at desktop widths with
   hero insight cards (Quality Distribution, Emotion-7, Shape x Movement)
   spanning two columns via .insight-card-wide. Same fallback order
   as the timeline grid. */
@media (min-width: 1100px) {
    .insights-grid {
        grid-template-columns: 1.3fr 1fr 0.8fr;
    }
    .insight-card-wide {
        grid-column: span 2;
    }
}

/* v0.11: cards shrunk from 280px min-height to 180px so more
   fit on screen without scrolling. The wide cards (Shape x
   Movement, 28-class) keep a taller 220px since they have
   more labels to display. Padding reduced from 14px to 10px. */
.insight-card {
    /* Dossier card: square corners, heavy single rule, no shadow. */
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 10px;
    display: flex;
    flex-direction: column;
    box-shadow: none;
}

.insight-card h3 {
    font-family: var(--font-serif);
    font-size: 12px;
    color: var(--text-strong);
    margin-bottom: 8px;
    padding-top: 6px;
    padding-bottom: 6px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-weight: 600;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    box-shadow: 0 2px 0 var(--border);    /* double-rule underline */
}

.insight-chart-wrap {
    flex: 1;
    position: relative;
    min-height: 180px;
}

.insight-card-wide .insight-chart-wrap {
    min-height: 220px;
}

.insights-empty {
    text-align: center;
    color: var(--text-muted);
    padding: 60px 20px;
    font-size: 14px;
}

/* v0.8.6 — allow an insight card to span the full grid row.
   Used by the Shape × Movement matrix so its horizontal
   stacked-bar layout has room to breathe. */
.insight-card-wide {
    grid-column: 1 / -1;
}

/* v0.9.1 — coverage strip at the bottom of each insights card.
   Shows "what fraction of visible rows actually have the column
   this chart is drawn from". The Science reviewer called this the
   single cheapest correction to the tool's biggest honesty
   problem: a user filtering to a narrow subset sees the same
   chart layout regardless of how sparse the underlying data is,
   with no indication that they might be looking at a sliver. */
.insight-coverage-strip {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 12px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.insight-coverage-strip .cov-label {
    flex: 1;
    min-width: 0;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
    text-transform: uppercase;
}
.insight-coverage-strip .cov-pill {
    /* Dossier style: [ CLEARED NN% ] monospace bracket instead of
       a rounded pill. Keeps the cov-hi / cov-mid / cov-midlo / cov-low
       class hooks (pinned by test_v091) but the visible output is a
       stamped clearance tag. */
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    padding: 2px 8px;
    border-radius: 0;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 10px;
    letter-spacing: 0.14em;
    border: 1px solid currentColor;
    background: transparent;
    text-transform: uppercase;
}
.insight-coverage-strip .cov-pill::before {
    content: "[";
    opacity: 0.8;
}
.insight-coverage-strip .cov-pill::after {
    content: "]";
    opacity: 0.8;
}
.insight-coverage-strip .cov-pill.cov-hi    { color: var(--success); }
.insight-coverage-strip .cov-pill.cov-hi::before  { content: "[ CLEARED "; }
.insight-coverage-strip .cov-pill.cov-mid   { color: var(--warning); }
.insight-coverage-strip .cov-pill.cov-mid::before { content: "[ PARTIAL "; }
.insight-coverage-strip .cov-pill.cov-midlo { color: var(--accent-warm); }
.insight-coverage-strip .cov-pill.cov-midlo::before { content: "[ THIN "; }
.insight-coverage-strip .cov-pill.cov-low   { color: var(--accent); }
.insight-coverage-strip .cov-pill.cov-low::before { content: "[ REDACTED "; }
.insight-coverage-strip .cov-n {
    color: var(--text-faint);
    font-size: 10px;
    white-space: nowrap;
}

/* Low-coverage card greys out the whole chart so the user can
   still read it but the visual hierarchy signals "don't
   over-interpret this". Critical coverage (<30%) goes further
   and stamps an oxblood REDACTED chip on the card. */
.insight-card.is-low-coverage .insight-chart-wrap {
    opacity: 0.72;
}
.insight-card.is-critical-coverage .insight-chart-wrap {
    opacity: 0.45;
}
.insight-card.is-critical-coverage {
    position: relative;
}
.insight-card.is-critical-coverage::before {
    /* Oxblood rubber-stamp look, rotated, CSS-only, SVG-free. Sits
       in the card's top-right corner. Appears in the critical-
       coverage branch only; regular cards don't get a stamp. */
    content: "REDACTED — < 30% COVERAGE";
    position: absolute;
    top: 10px;
    right: 10px;
    transform: rotate(-3deg);
    transform-origin: top right;
    display: inline-block;
    padding: 3px 8px;
    font-family: var(--font-serif);
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.18em;
    color: var(--accent);
    border: 2px solid var(--accent);
    background: color-mix(in oklab, var(--bg-card) 86%, var(--accent) 14%);
    text-align: center;
    z-index: 2;
    pointer-events: none;
}

@media (max-width: 900px) {
    .insights-grid {
        grid-template-columns: 1fr;
    }
}

/* =========================================================================
   Phase 4 additions — new components + UX polish
   ========================================================================= */

/* ---- Timeline header polish (view-results button + hint line) ---- */
.timeline-header {
    align-items: center;
    gap: var(--s-3);
}
.timeline-spacer { flex: 1; }
.timeline-hint {
    padding: 0 var(--s-4) var(--s-2);
    font-size: var(--fs-12);
    color: var(--text-faint);
}

/* ---- Search panel: active filter chips ---- */
.active-filters {
    display: none;
    flex-wrap: wrap;
    gap: var(--s-2);
    padding: 0 var(--s-4) var(--s-2);
    align-items: center;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    padding: var(--s-1) var(--s-2);
    background: var(--accent-bg);
    border: 1px solid var(--accent);
    border-radius: var(--r-pill);
    font-size: var(--fs-12);
    color: var(--text);
    line-height: 1.2;
}
.chip-label {
    padding: 0 var(--s-1);
}
.chip-x {
    background: none;
    border: none;
    color: var(--text);
    cursor: pointer;
    padding: 0 var(--s-1);
    font-size: var(--fs-16);
    line-height: 1;
    border-radius: var(--r-sm);
    font-family: inherit;
}
.chip-x:hover { background: rgba(255,255,255,0.08); }

.chip-clear {
    background: none;
    border: 1px dashed var(--border-strong);
    border-radius: var(--r-pill);
    padding: var(--s-1) var(--s-3);
    color: var(--text-muted);
    font-size: var(--fs-12);
    cursor: pointer;
    font-family: inherit;
    transition: color var(--t-fast), border-color var(--t-fast);
}
.chip-clear:hover { color: var(--text); border-color: var(--text-muted); }

/* ---- Search panel: sort dropdown next to the search input ---- */
.search-sort {
    max-width: 160px;
    margin-left: auto;
}

/* ---- Search panel: pagination ---- */
.search-pager {
    display: flex;
    gap: var(--s-1);
    flex-wrap: wrap;
    justify-content: center;
    padding: var(--s-4);
    flex-shrink: 0;
}
.pager-btn {
    padding: var(--s-1) var(--s-3);
    border: 1px solid var(--border);
    background: var(--bg-card);
    color: var(--text-muted);
    border-radius: var(--r-md);
    cursor: pointer;
    font-family: inherit;
    font-size: var(--fs-13);
    min-width: 36px;
    transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.pager-btn:hover:not(.disabled):not(.active) {
    color: var(--text);
    border-color: var(--border-strong);
    background: var(--bg-card-h);
}
.pager-btn.active {
    background: var(--accent);
    color: #0b0f17;
    border-color: var(--accent);
    font-weight: 600;
    cursor: default;
}
.pager-btn.disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.pager-ellipsis {
    color: var(--text-faint);
    align-self: center;
    padding: 0 var(--s-1);
}

/* ---- Skeleton loader for search results ---- */
.result-card.skeleton {
    cursor: default;
    pointer-events: none;
    position: relative;
    overflow: hidden;
    min-height: 96px;
    background: linear-gradient(
        90deg,
        var(--bg-card) 0%,
        var(--bg-card-h) 50%,
        var(--bg-card) 100%
    );
    background-size: 200% 100%;
    animation: skeleton-shimmer 1.6s ease-in-out infinite;
}
.result-card.skeleton:hover { transform: none; box-shadow: none; }
@keyframes skeleton-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ---- Empty state ---- */
.empty-state {
    text-align: center;
    padding: var(--s-12) var(--s-6);
    color: var(--text-muted);
}
.empty-state-icon {
    margin-bottom: var(--s-3);
    opacity: 0.55;
    color: var(--text-muted);
    display: flex;
    justify-content: center;
}
.empty-state-icon .icon { width: 64px; height: 64px; }
.empty-state-title {
    font-size: var(--fs-18);
    color: var(--text);
    font-weight: 600;
    margin-bottom: var(--s-2);
}
.empty-state-detail {
    font-size: var(--fs-14);
}
.empty-state-detail a { color: var(--accent); }

/* ---- Search info: stronger headline treatment ---- */
.search-info {
    font-size: var(--fs-14);
    color: var(--text-muted);
}
.search-info strong {
    color: var(--text);
    font-weight: 600;
}
.search-info em { color: var(--accent); font-style: normal; }

/* ---- Mobile responsive ---- */
@media (max-width: 720px) {
    #header {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 4px 8px;
        padding: 6px 10px;
    }
    .header-left {
        gap: var(--s-2);
        flex: 0 0 auto;
    }
    .header-left h1 { font-size: 14px; }
    .stats-badge { display: none; }
    /* v0.11.2 — show settings gear + help button on mobile but
       hide the theme pill (accessible via gear menu). */
    .tabs-settings {
        display: flex;
        gap: 4px;
        flex: 0 0 auto;
        order: 2;
    }
    .theme-pill { display: none; }
    .tabs {
        flex: 1 1 0;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
        padding-bottom: 0;
        gap: 2px;
    }
    .tabs::-webkit-scrollbar { display: none; }
    .tab {
        white-space: nowrap;
        flex-shrink: 0;
        font-size: 11px;
        padding: 6px 10px;
        min-height: 32px;
    }
    #filters-bar {
        padding: var(--s-2) var(--s-3);
        gap: var(--s-2);
    }
    .filter-group { flex-shrink: 0; }
    .filter-group label { font-size: 11px; }
    .search-bar {
        flex-wrap: wrap;
    }
    .search-bar input[type="text"] {
        min-width: 0;
        flex: 1 1 auto;
    }
    .search-sort {
        max-width: 100%;
        margin-left: 0;
        flex: 1 1 auto;
    }
    .timeline-header {
        flex-wrap: wrap;
    }
    .empty-state { padding: var(--s-8) var(--s-4); }
    .empty-state-icon .icon { width: 48px; height: 48px; }
}

/* ---- Reduced-motion preference ---- */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* =========================================================================
   AI Chat (BYOK) panel + floating widget + Connect docs page
   ========================================================================= */

/* ---- Ask AI panel ---- */
.ai-panel {
    display: flex;
    flex-direction: column;
    flex: 1;
    max-width: 920px;
    width: 100%;
    margin: 0 auto;
    padding: var(--s-4);
    gap: var(--s-3);
    min-height: 0;
}

.ai-panel-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--s-3);
    flex-shrink: 0;
}
.ai-panel-title {
    font-size: var(--fs-18);
    font-weight: 600;
}
.ai-panel-subtitle {
    font-size: var(--fs-13);
    color: var(--text-muted);
    margin-top: var(--s-1);
}

.ai-settings {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s-4);
    flex-shrink: 0;
}
.ai-settings label {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    font-size: var(--fs-12);
    color: var(--text-muted);
}
.ai-settings input,
.ai-settings select {
    font-size: var(--fs-14);
    padding: var(--s-2) var(--s-3);
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    color: var(--text);
}
.ai-settings-row {
    display: flex;
    gap: var(--s-2);
}
.ai-settings-hint {
    font-size: var(--fs-12);
    color: var(--text-muted);
    line-height: 1.55;
}
.ai-settings-hint code {
    background: var(--bg-input);
    padding: 1px 4px;
    border-radius: var(--r-sm);
    font-size: 11px;
}

.ai-messages {
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-2) var(--s-1);
    min-height: 200px;
}

.ai-empty {
    text-align: center;
    padding: var(--s-8) var(--s-4);
    color: var(--text-muted);
}
.ai-empty-icon {
    margin-bottom: var(--s-3);
    opacity: 0.55;
    color: var(--text-muted);
    display: flex;
    justify-content: center;
}
.ai-empty-icon .icon { width: 56px; height: 56px; }
.ai-empty-title { font-size: var(--fs-16); color: var(--text); margin-bottom: var(--s-3); font-weight: 600; }
.ai-empty-detail { font-size: var(--fs-14); }
.ai-empty-suggestions {
    list-style: none;
    margin: var(--s-3) 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}
.ai-empty-suggestions li {
    cursor: pointer;
    padding: var(--s-2) var(--s-3);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--bg-card);
    transition: background var(--t-fast), border-color var(--t-fast);
    font-style: italic;
    color: var(--text);
}
.ai-empty-suggestions li:hover {
    background: var(--bg-card-h);
    border-color: var(--border-strong);
}
.ai-empty-foot {
    display: block;
    margin-top: var(--s-4);
    color: var(--text-faint);
    font-size: var(--fs-12);
}

/* ---- Chat bubbles ---- */
.ai-msg {
    display: flex;
    max-width: 100%;
}
.ai-msg-body {
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-md);
    line-height: 1.55;
    font-size: var(--fs-14);
    max-width: 82%;
    word-wrap: break-word;
}
.ai-msg.user        { justify-content: flex-end; }
.ai-msg.user .ai-msg-body {
    background: var(--accent-bg);
    border: 1px solid var(--accent);
    color: var(--text);
}
.ai-msg.assistant .ai-msg-body {
    background: var(--bg-card);
    border: 1px solid var(--border);
    color: var(--text);
}
.ai-msg.assistant .ai-msg-body p { margin: 0 0 var(--s-2); }
.ai-msg.assistant .ai-msg-body p:last-child { margin-bottom: 0; }
.ai-msg.assistant .ai-msg-body code {
    background: var(--bg-input);
    padding: 1px 4px;
    border-radius: var(--r-sm);
    font-family: var(--font-mono);
    font-size: 12px;
}
.ai-msg.system .ai-msg-body {
    background: transparent;
    border: 1px dashed var(--border);
    color: var(--text-muted);
    font-size: var(--fs-12);
    font-style: italic;
}
.ai-msg.error .ai-msg-body {
    background: rgba(225, 87, 89, 0.12);
    border: 1px solid var(--red);
    color: var(--text);
}
.ai-msg.thinking .ai-msg-body {
    background: var(--bg-card);
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-size: var(--fs-13);
}
.ai-msg.tool .ai-msg-body {
    background: var(--bg-input);
    border: 1px solid var(--border);
    color: var(--text-muted);
    font-size: var(--fs-12);
    font-family: var(--font-mono);
    max-width: 100%;
}
.ai-msg.tool.error .ai-msg-body {
    background: rgba(225, 87, 89, 0.12);
    border-color: var(--red);
    color: var(--text);
}
.ai-tool-name { color: var(--accent); }
.ai-tool-args { color: var(--text); }
.ai-tool-json {
    margin-top: var(--s-2);
    padding: var(--s-2);
    background: rgba(0,0,0,0.3);
    border-radius: var(--r-sm);
    overflow-x: auto;
    font-size: 11px;
    line-height: 1.4;
    max-height: 200px;
}

/* Tool result rendering — search_sightings */
.ai-msg.tool-result .ai-msg-body {
    background: transparent;
    border: none;
    padding: 0;
    max-width: 100%;
    width: 100%;
}
.ai-result-summary {
    color: var(--text-muted);
    font-size: var(--fs-13);
    margin-bottom: var(--s-2);
    padding: 0 var(--s-2);
}
.ai-result-card {
    display: block;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s-3) var(--s-4);
    margin-bottom: var(--s-2);
    text-decoration: none;
    color: inherit;
    transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.ai-result-card:hover {
    background: var(--bg-card-h);
    border-color: var(--border-strong);
    transform: translateY(-1px);
    text-decoration: none;
}
.ai-result-head {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin-bottom: var(--s-1);
    flex-wrap: wrap;
}
.ai-result-date { color: var(--text-strong); font-weight: 600; font-size: var(--fs-14); }
.ai-result-loc {
    font-size: var(--fs-12);
    color: var(--text-muted);
    margin-bottom: var(--s-1);
}
.ai-result-desc {
    font-size: var(--fs-13);
    color: var(--text);
    line-height: 1.5;
}
.ai-result-more {
    text-align: center;
    padding: var(--s-2);
    color: var(--text-muted);
    font-size: var(--fs-13);
}

/* Tool result rendering — count_by bar chart */
.ai-bar-row {
    display: grid;
    grid-template-columns: 110px 1fr 80px;
    gap: var(--s-2);
    align-items: center;
    margin-bottom: var(--s-1);
    font-size: var(--fs-13);
}
.ai-bar-label {
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ai-bar-track {
    height: 18px;
    background: var(--bg-input);
    border-radius: var(--r-sm);
    overflow: hidden;
}
.ai-bar-fill {
    height: 100%;
    background: var(--accent);
    transition: width var(--t-base);
}
.ai-bar-count {
    text-align: right;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* ---- AI input row ---- */
.ai-input-row {
    display: flex;
    gap: var(--s-2);
    flex-shrink: 0;
}
.ai-input-row input[type="text"] {
    flex: 1;
    padding: var(--s-3) var(--s-4);
    font-size: var(--fs-14);
}
.ai-foot {
    text-align: center;
    color: var(--text-faint);
    font-size: var(--fs-12);
    margin-top: var(--s-2);
}
.ai-foot a { color: var(--text-muted); }

/* ---- Floating chat widget ---- */
.chat-fab {
    position: fixed;
    bottom: 24px;
    right: 24px;
    z-index: 9999;
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-3) var(--s-4);
    background: var(--accent);
    color: #0b0f17;
    border: none;
    border-radius: var(--r-pill);
    cursor: pointer;
    font-family: inherit;
    font-size: var(--fs-14);
    font-weight: 600;
    box-shadow: var(--shadow-lg);
    transition: transform var(--t-fast), background var(--t-fast);
}
.chat-fab:hover {
    background: var(--accent-hover);
    transform: translateY(-2px);
}
.chat-fab-icon { font-size: 18px; display: inline-flex; align-items: center; }

.chat-popover {
    position: fixed;
    bottom: 80px;
    right: 24px;
    z-index: 9999;
    width: 360px;
    max-width: calc(100vw - 48px);
    height: 480px;
    max-height: calc(100vh - 120px);
    background: var(--bg-panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-lg);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.chat-popover-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--s-3) var(--s-4);
    border-bottom: 1px solid var(--border);
    background: var(--bg-card);
    flex-shrink: 0;
}
.chat-popover-title { font-weight: 600; font-size: var(--fs-14); }
.chat-popover-actions { display: flex; gap: var(--s-1); }
.btn-icon {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    font-size: var(--fs-16);
    padding: var(--s-1) var(--s-2);
    border-radius: var(--r-sm);
    font-family: inherit;
    line-height: 1;
}
.btn-icon:hover { background: var(--bg-input); color: var(--text); }
.chat-popover-body {
    flex: 1;
    overflow-y: auto;
    padding: var(--s-3) var(--s-4);
}
.chat-popover .ai-input-row {
    padding: var(--s-3) var(--s-4);
    border-top: 1px solid var(--border);
    background: var(--bg-card);
}

@media (max-width: 720px) {
    .chat-fab {
        bottom: 16px;
        right: 16px;
        padding: var(--s-2) var(--s-3);
    }
    .chat-fab-label { display: none; }
    .chat-popover {
        right: 8px;
        bottom: 72px;
        left: 8px;
        width: auto;
        height: 400px;
    }
}

/* ---- Connect docs page ---- */
.connect-page {
    max-width: 820px;
    width: 100%;
    margin: 0 auto;
    padding: var(--s-6) var(--s-4);
}
.connect-page h2 {
    font-size: var(--fs-24);
    color: var(--text-strong);
    margin-bottom: var(--s-3);
    font-weight: 700;
    letter-spacing: -0.01em;
    padding-bottom: var(--s-2);
    border-bottom: 2px solid var(--accent);
}
.connect-page h3 {
    font-size: var(--fs-16);
    color: var(--text);
    margin-bottom: var(--s-2);
    font-weight: 600;
}
.connect-intro {
    color: var(--text-muted);
    font-size: var(--fs-15);
    line-height: 1.6;
    margin-bottom: var(--s-6);
}
.connect-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s-4) var(--s-5);
    margin-bottom: var(--s-4);
    box-shadow: var(--shadow-sm);
    transition: border-color var(--t-fast), box-shadow var(--t-fast);
}

.connect-card:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow-md);
}
.connect-card p {
    color: var(--text-muted);
    font-size: var(--fs-14);
    line-height: 1.6;
    margin: var(--s-2) 0;
}
.connect-card p code {
    background: var(--bg-input);
    padding: 1px 5px;
    border-radius: var(--r-sm);
    font-size: 12px;
    color: var(--text);
}
.connect-code {
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    padding: var(--s-3) var(--s-4);
    overflow-x: auto;
    margin: var(--s-2) 0;
}
.connect-code code {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--text);
    line-height: 1.55;
    background: none;
    padding: 0;
}
.connect-foot {
    margin-top: var(--s-6);
    color: var(--text-faint);
    font-size: var(--fs-12);
    text-align: center;
}
.connect-discovery-list {
    margin: 8px 0 0;
    padding-left: 20px;
    font-size: var(--fs-14);
    line-height: 1.8;
}
.connect-discovery-list code {
    font-size: var(--fs-13);
}

/* =========================================================================
   Sprint 2 additions — geographic filters, map place search, export,
   stats popover
   ========================================================================= */

/* v0.8.7: Country / State / Hynek / Vallee / Collection dropdowns
   were deleted because they had no byte slot in the bulk buffer.
   Their .filter-group CSS inheritance is now dead code. */

/* The Date Range fieldset added in Sprint 1 needs a tiny bit of cleanup
   so the legend doesn't read as a heading and the inputs sit flush. */
.filter-group-date {
    border: none;
    padding: 0;
    margin: 0;
    display: flex;
    align-items: center;
    gap: var(--s-2);
}
.filter-group-date legend {
    font-size: var(--fs-12);
    color: var(--text-muted);
    padding: 0;
    margin: 0;
}

/* ---- Map place search ----
   v0.7.1: moved from top-left to bottom-middle of the map canvas.
   The v0.7 topbar carries the Points/Heatmap/HexBin mode toggle at
   top: 14px, and the old top-left place search was colliding with
   it — the mode buttons were hidden behind the search input. Bottom-
   middle keeps the search reachable without covering the Observatory
   controls or the HUD brackets. The bg uses --bg-panel via a color-mix
   fallback so both SIGNAL (dark) and DECLASS (cream) look right. */
.map-place-search {
    position: absolute;
    bottom: 36px;        /* sits above the Leaflet attribution strip */
    left: 50%;
    transform: translateX(-50%);
    z-index: 1000;
    display: flex;
    align-items: center;
    gap: var(--s-1);
    background: var(--bg-panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    padding: var(--s-1) var(--s-2);
    box-shadow: var(--shadow-md);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    max-width: min(520px, calc(100% - 32px));
}
.map-place-search input[type="text"] {
    width: 240px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    color: var(--text);
    font-size: var(--fs-13);
    padding: var(--s-1) var(--s-2);
    border-radius: var(--r-sm);
}
.map-place-search input[type="text"]:focus {
    border-color: var(--accent);
}
.map-place-search .btn {
    font-size: var(--fs-12);
    padding: var(--s-1) var(--s-2);
    white-space: nowrap;
}

@media (max-width: 720px) {
    /* On narrow viewports the bottom-middle placement still works; we
       just widen the max-width so the input doesn't squeeze out the
       Go / Near me buttons. */
    .map-place-search {
        max-width: calc(100% - 16px);
        flex-wrap: wrap;
    }
    .map-place-search input[type="text"] {
        width: 100%;
        flex: 1 1 100%;
        order: 1;
    }
    .map-place-search .btn { order: 2; flex: 1 1 auto; }
}

/* ---- Search panel: Export + Copy link toolbar row ---- */
.search-actions {
    display: flex;
    gap: var(--s-2);
    padding: 0 var(--s-4) var(--s-2);
    flex-wrap: wrap;
    flex-shrink: 0;
}
.search-actions .btn {
    background: var(--bg-card);
    border-color: var(--border);
}
.search-actions .btn:hover {
    background: var(--bg-card-h);
    border-color: var(--border-strong);
    color: var(--text);
}

/* ---- Stats badge: compact button + click-to-open popover ---- */
.stats-badge-wrap {
    position: relative;
}
.stats-badge {
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--r-md);
    padding: var(--s-1) var(--s-2);
    font-size: var(--fs-12);
    color: var(--text-muted);
    cursor: pointer;
    font-family: inherit;
    transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
    line-height: 1.35;
    text-align: left;
}
.stats-badge:hover {
    background: var(--bg-card);
    color: var(--text);
    border-color: var(--border);
}
.stats-badge[aria-expanded="true"] {
    background: var(--bg-card);
    color: var(--text);
    border-color: var(--border-strong);
}
.stats-sep {
    color: var(--text-faint);
    margin: 0 var(--s-1);
}

/* Stats-badge boot sequence — shown in the HTML before /api/stats has
   resolved. Monospace, amber prompt, green message, blinking cursor. */
.stats-badge-boot {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    color: var(--term-green);
    text-shadow: 0 0 6px var(--term-glow);
    letter-spacing: 0.04em;
    white-space: nowrap;
}
.stats-badge-boot .term-prompt {
    color: var(--term-amber);
    margin-right: 4px;
}
.stats-badge-boot .term-cursor {
    display: inline-block;
    width: 6px;
    height: 0.9em;
    background: var(--term-green);
    box-shadow: 0 0 6px var(--term-glow);
    animation: term-cursor-blink 1s steps(1) infinite;
    vertical-align: text-bottom;
    margin-left: 3px;
}
.stats-popover {
    position: absolute;
    top: calc(100% + var(--s-2));
    left: 0;
    z-index: 9000;
    min-width: 320px;
    max-width: 400px;
    background: var(--surface-1);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-lg);
    padding: var(--s-4);
    font-size: var(--fs-13);
    color: var(--text);
}
.stats-popover[hidden] { display: none; }
.stats-popover-section {
    margin-bottom: var(--s-3);
}
.stats-popover-section:last-child {
    margin-bottom: 0;
}
.stats-popover-title {
    font-size: var(--fs-12);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-faint);
    margin-bottom: var(--s-2);
}
.stats-popover-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: var(--s-1) 0;
    color: var(--text);
}
.stats-popover-row span {
    color: var(--text-muted);
}
.stats-popover-row strong {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.stats-popover-sub {
    padding-left: var(--s-3);
    font-size: var(--fs-12);
    color: var(--text-faint);
}
.stats-popover-table {
    width: 100%;
    border-collapse: collapse;
}
.stats-popover-table td {
    padding: var(--s-1) 0;
    color: var(--text);
}
.stats-popover-table td:last-child {
    text-align: right;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.stats-popover-foot {
    margin-top: var(--s-3);
    padding-top: var(--s-2);
    border-top: 1px solid var(--border);
    font-size: var(--fs-12);
    color: var(--text-faint);
}
.stats-popover-foot a { color: var(--accent); }

@media (max-width: 720px) {
    .stats-popover {
        position: fixed;
        left: var(--s-3);
        right: var(--s-3);
        top: 80px;
        min-width: 0;
        max-width: none;
    }
}

/* =========================================================================
   Sprint 3 additions — feel pass (motion, density, feedback)
   ========================================================================= */

/* ---- Filter bar: Primary + Advanced split ---- */
/* The advanced filters (Collection, Hynek, Vallee) are wrapped in a
   .filters-advanced container that's hidden by default. The
   "More filters" toggle button reveals them. A count badge on the
   toggle shows how many advanced filters are active.

   On mobile (<720px) we add another layer: the entire filter bar
   becomes collapsible behind a "Filters (n)" button, where n is the
   total active count. */

.filters-advanced {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
}
.filters-advanced[hidden] { display: none; }

#btn-more-filters {
    position: relative;
}
#btn-more-filters .filter-count-badge {
    display: inline-block;
    min-width: 18px;
    padding: 0 6px;
    margin-left: var(--s-1);
    font-size: 10px;
    font-weight: 700;
    background: var(--accent);
    color: #0b0f17;
    border-radius: var(--r-pill);
    line-height: 16px;
    text-align: center;
}
#btn-more-filters .filter-count-badge[hidden] { display: none; }

/* v0.10.0 — Reset link replaces the old Apply + Clear buttons.
   Subtle underlined link, not a button, so it doesn't compete
   visually with the filter dropdowns. */
/* v0.10.0 — Cross-filter chip bar. Sits at the top of the
   Timeline and Insights tabs showing "Filtered by: X ✕" when
   a cross-filter is active. */
.cross-filter-chips {
    padding: 8px 16px;
    background: color-mix(in oklab, var(--bg-panel) 85%, var(--accent) 15%);
    border-bottom: 1px solid var(--accent);
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.cross-filter-chips[hidden] { display: none; }
.cross-filter-chip {
    font-size: 12px;
    color: var(--fg);
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 4px 10px;
    background: var(--bg-card);
    border: 1px solid var(--accent);
    border-radius: 999px;
}
.cross-filter-chip strong {
    color: var(--accent);
}
.cross-filter-clear {
    background: none;
    border: none;
    color: var(--fg-dim);
    cursor: pointer;
    font-size: 14px;
    padding: 0 2px;
    line-height: 1;
}
.cross-filter-clear:hover { color: var(--accent); }
.cross-filter-warning {
    font-size: 11px;
    color: #e15759;
    font-weight: 600;
}

/* v0.10.0 — Timeline card header with granularity toggle */
.timeline-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 10px;
}
.timeline-card-header h3 {
    margin-bottom: 0;
}
.timeline-gran-toggle {
    display: flex;
    gap: 2px;
    flex-shrink: 0;
}
.gran-btn {
    background: var(--bg-input, var(--bg-card));
    border: 1px solid var(--border);
    color: var(--fg-dim, #8A94AD);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 3px 10px;
    cursor: pointer;
    transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.gran-btn:first-child { border-radius: 4px 0 0 4px; }
.gran-btn:last-child { border-radius: 0 4px 4px 0; }
.gran-btn:hover {
    border-color: var(--accent);
    color: var(--fg);
}
.gran-btn.active {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--bg);
}

/* v0.11 — Insights section headers + mobile jump-nav */
.insights-section {
    margin-bottom: 24px;
}
.insights-section-title {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--text-muted);
    padding: 12px 16px 6px;
    margin: 0;
    font-weight: 600;
    border-bottom: 1px solid var(--border);
}
.insights-jump-nav {
    display: none;  /* hidden on desktop */
}
@media (max-width: 900px) {
    .insights-jump-nav {
        display: flex;
        gap: 8px;
        padding: 8px 16px;
        position: sticky;
        top: 0;
        z-index: 3;
        background: var(--bg);
    }
}
.jump-pill {
    font-size: 11px;
    padding: 4px 12px;
    border-radius: 999px;
    border: 1px solid var(--border);
    color: var(--text-muted);
    text-decoration: none;
    white-space: nowrap;
    transition: border-color 120ms ease, color 120ms ease;
}
.jump-pill:hover { border-color: var(--accent); color: var(--fg); }
.jump-pill.active { border-color: var(--accent); color: var(--accent); }

/* "± neutral" toggle on the 28-class card */
.insight-toggle {
    font-size: 9px;
    padding: 2px 8px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    float: right;
    text-transform: none;
    letter-spacing: normal;
    font-weight: 400;
    transition: border-color 120ms ease, color 120ms ease;
}
.insight-toggle:hover { border-color: var(--accent); color: var(--fg); }
.insight-toggle[aria-pressed="true"] {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--bg);
}

.filter-reset-link {
    font-size: 11px;
    color: var(--fg-dim, #8A94AD);
    text-decoration: underline;
    cursor: pointer;
    letter-spacing: 0.04em;
    padding: 4px 8px;
    white-space: nowrap;
    transition: color 120ms ease;
}
.filter-reset-link:hover {
    color: var(--accent);
}

/* Mobile filter bar toggle (hidden on desktop) */
#btn-mobile-filters {
    display: none;
}
@media (max-width: 720px) {
    #btn-mobile-filters {
        display: inline-flex;
        align-items: center;
        gap: var(--s-2);
    }
    #filters-bar.is-collapsed > *:not(#btn-mobile-filters):not(#btn-apply-filters):not(#btn-clear-filters) {
        display: none;
    }
    #filters-bar.is-collapsed #btn-apply-filters,
    #filters-bar.is-collapsed #btn-clear-filters {
        display: none;
    }
    #filters-bar:not(.is-collapsed) #btn-mobile-filters .mobile-filters-chevron {
        transform: rotate(180deg);
    }
    .mobile-filters-chevron {
        display: inline-block;
        transition: transform var(--t-fast) ease-out;
    }

    /* v0.9.4 — the hamburger now collapses EVERYTHING above the
       map: filter bar content (already handled by .is-collapsed
       above), the movement pill row, AND the Observatory rail.
       body.mobile-filters-hidden is toggled by the JS handler;
       these rules make the movement row and rail disappear when
       the user hasn't expanded the hamburger, giving the map
       maximum vertical space on first load. The rail stays
       hidden even when its accordion sections are individually
       expanded, because the top-level toggle takes priority. */
    body.mobile-filters-hidden .filter-movement-row {
        display: none;
    }
    body.mobile-filters-hidden .observatory-rail {
        display: none;
    }
    /* Give the map canvas more vertical room when everything is
       collapsed. The observatory-stage grid is already single-
       column on mobile (grid-template-columns: 1fr); when the
       rail is hidden the stage becomes a single-row grid and
       #panel-observatory fills the remaining viewport height
       after the collapsed filter bar and the TimeBrush. */
    body.mobile-filters-hidden .observatory-stage {
        grid-template-rows: 1fr;
    }
}

/* Same rules for touch-primary devices regardless of width */
body.is-touch #btn-mobile-filters {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
}
body.is-touch.mobile-filters-hidden .filter-movement-row {
    display: none;
}
body.is-touch.mobile-filters-hidden .observatory-rail {
    display: none;
}
body.is-touch.mobile-filters-hidden .observatory-stage {
    grid-template-rows: 1fr;
}

/* ---- "Unsaved changes" indicator on the Apply button ---- */
/* Toggled on by text input events, removed when applyFilters runs or
   the button is clicked. A subtle accent ring pulse draws the eye
   without being loud. */
.btn-primary.is-dirty {
    box-shadow: 0 0 0 3px var(--accent-bg);
    animation: dirty-pulse 1.8s ease-in-out infinite;
}
@keyframes dirty-pulse {
    0%, 100% { box-shadow: 0 0 0 3px var(--accent-bg); }
    50%      { box-shadow: 0 0 0 5px var(--accent-bg); }
}

/* ---- Map: hackery loading overlay ----
   Three layers:
   - ::before is a radial sweep that spins once over the viewport,
     laid over the Leaflet tiles with mix-blend-mode: screen so it
     tints live tiles without obscuring them.
   - ::after is the top-edge progress bar from v0.3 (kept because it's
     a good peripheral indicator).
   - .map-scanframe is a decorative HUD frame with corner brackets +
     a glowing title; added/removed via JS alongside .is-loading. */
#map.is-loading {
    position: relative;
}
/* v0.7: the rotating conic-gradient radar sweep (v0.5) was removed
   because it tinted live map tiles and made markers harder to read.
   The top-edge progress bar below + the HUD status pill in the
   Observatory topbar are the new, content-respecting loading
   indicators. The @keyframes map-radar-spin rule is kept because
   test_loading_system.py currently references it; it's no longer
   attached to any element but doesn't hurt. */
@keyframes map-radar-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}
#map.is-loading::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(
        90deg,
        transparent 0%,
        var(--accent) 40%,
        var(--accent-hover) 50%,
        var(--accent) 60%,
        transparent 100%
    );
    background-size: 60% 100%;
    background-repeat: no-repeat;
    animation: map-loading-slide 1.4s ease-in-out infinite;
    z-index: 1200;
    pointer-events: none;
    box-shadow: 0 0 12px rgba(110, 168, 255, 0.45);
}
@keyframes map-loading-slide {
    0%   { background-position: -60% 0; }
    100% { background-position: 160% 0; }
}

/* Corner-bracket HUD that only appears while loading. The brackets are
   drawn with linear-gradients on four absolutely-positioned corners,
   which means no extra DOM and no images. */
#map.is-loading .leaflet-container::before,
#map.is-loading .leaflet-container::after {
    /* we don't actually target .leaflet-container — Leaflet owns it. */
}

.map-scanframe {
    position: absolute;
    inset: 20px;
    pointer-events: none;
    z-index: 1201;
    opacity: 0;
    transition: opacity var(--t-base) ease-out;
}
#map.is-loading .map-scanframe { opacity: 1; }

.map-scanframe::before,
.map-scanframe::after,
.map-scanframe > .mscf-tl,
.map-scanframe > .mscf-tr,
.map-scanframe > .mscf-bl,
.map-scanframe > .mscf-br { content: ""; position: absolute; }

.map-scanframe > .mscf-tl,
.map-scanframe > .mscf-tr,
.map-scanframe > .mscf-bl,
.map-scanframe > .mscf-br {
    width: 28px;
    height: 28px;
    border-color: var(--accent);
    box-shadow: 0 0 10px rgba(110, 168, 255, 0.45);
}
.map-scanframe > .mscf-tl { top: 0; left: 0;     border-top: 2px solid; border-left:  2px solid; }
.map-scanframe > .mscf-tr { top: 0; right: 0;    border-top: 2px solid; border-right: 2px solid; }
.map-scanframe > .mscf-bl { bottom: 0; left: 0;  border-bottom: 2px solid; border-left:  2px solid; }
.map-scanframe > .mscf-br { bottom: 0; right: 0; border-bottom: 2px solid; border-right: 2px solid; }

.map-scanframe-label {
    position: absolute;
    top: -10px;
    left: 36px;
    padding: 2px 8px;
    font-family: var(--font-mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--accent);
    background: var(--bg);
    box-shadow: 0 0 10px rgba(110, 168, 255, 0.35);
}
.map-scanframe-label::before {
    content: "● ";
    color: var(--term-green);
    text-shadow: 0 0 6px var(--term-glow);
    animation: term-pulse 1.4s ease-in-out infinite;
}

/* ---- Chart container: fade during drill-down fetch ---- */
.chart-container {
    position: relative;
    transition: opacity var(--t-base) ease-out;
}
.chart-container.is-loading {
    opacity: 0.45;
    pointer-events: none;
}

/* =========================================================================
   PROGRESSIVE LOADING — keep previous content visible while new data loads
   -----------------------------------------------------------------------
   Strategy:
   - Container gains `is-progressive` (sets position: relative) and
     `is-loading-progressive` (active loading flag).
   - Direct children, except the overlay, dim slightly + blur + saturate
     down. They stay visible — that's the whole point.
   - A `.progressive-overlay` is appended; its child is a centered
     `.loading-terminal` (mounted via the existing helper). Pointer-
     events are off so the user can still interact with the dimmed
     content if they want.
   - When new data arrives, the JS removes the loading flag, the
     children fade back to full opacity, and any new children rendered
     in the swap can carry `.is-new` to stagger-fade in on top.
   ========================================================================= */
.is-progressive {
    position: relative;
}
.is-progressive > *:not(.progressive-overlay) {
    transition:
        opacity var(--t-base) ease-out,
        filter var(--t-base) ease-out;
}
.is-progressive.is-loading-progressive > *:not(.progressive-overlay) {
    opacity: 0.4;
    filter: blur(0.6px) saturate(0.7);
    pointer-events: none;
}

.progressive-overlay {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 50;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--t-base) ease-out;
    /* the centered .loading-terminal inherits its own styling */
}
.is-loading-progressive > .progressive-overlay {
    opacity: 1;
}
/* Compact terminal inside an overlay should still feel like an "incoming
   message" panel — give it a card background so it reads against any
   underlying content. */
.progressive-overlay > .loading-terminal.compact {
    background: var(--term-bg);
    border-color: var(--term-border);
    box-shadow:
        0 0 0 1px rgba(63, 185, 80, 0.08),
        0 8px 24px rgba(0, 0, 0, 0.5);
    padding: var(--s-2) var(--s-4);
    pointer-events: auto;
    /* Slight backdrop blur so the dim+blur on the content shows through
       cleanly without competing with the overlay text. */
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
}

/* Stagger fade-in for newly rendered children (e.g. search result cards
   that just landed). JS sets `--i` per child to delay each one a bit. */
@keyframes stagger-fade-in {
    from {
        opacity: 0;
        transform: translateY(6px);
        filter: blur(2px);
    }
    to {
        opacity: 1;
        transform: none;
        filter: none;
    }
}
.is-new {
    animation: stagger-fade-in 320ms ease-out both;
    animation-delay: calc(var(--i, 0) * 22ms);
}

/* Map: during a refresh the pane stays fully opaque so users can keep
   reading the markers. The v0.7 UX change: the "loading" affordance is
   the HUD status pill in the observatory topbar, not a dim over the
   markers. The rule below is intentionally a no-op transition so
   existing test selectors still resolve. */
#map.is-loading-progressive .leaflet-marker-pane,
#map.is-loading-progressive .leaflet-overlay-pane {
    opacity: 1;
    filter: none;
    transition: opacity var(--t-base) ease-out, filter var(--t-base) ease-out;
}

/* Reduced-motion: skip the blur transitions and the stagger animation,
   keep the dim so the user still gets the "stale" affordance. */
@media (prefers-reduced-motion: reduce) {
    .is-progressive.is-loading-progressive > *:not(.progressive-overlay) {
        filter: none;
    }
    .is-new {
        animation: none;
    }
    #map.is-loading-progressive .leaflet-marker-pane,
    #map.is-loading-progressive .leaflet-overlay-pane {
        filter: none;
    }
}

/* ---- Load All: inline confirmation state ---- */
/* Replaces the jarring native confirm() for large loads. First click
   on Load All when the count is big swaps the button to a pulsing
   "Tap again to confirm" state for 3 seconds; second click actually
   loads. No second click = button reverts. */
.btn-load-all.confirming {
    background: var(--orange);
    color: #0b0f17;
    animation: confirm-pulse 1s ease-in-out infinite;
}
@keyframes confirm-pulse {
    0%, 100% { filter: brightness(1); }
    50%      { filter: brightness(1.15); }
}

/* =========================================================================
   OBSERVATORY (v0.7) — unified dashboard replacing the Map + Timeline tabs
   ========================================================================= */

/* Legacy tab buttons are kept in the DOM for deep-link compat but should
   never display. The [hidden] attribute already handles this; the rule
   below is belt-and-braces for any CSS that tries to un-hide them. */
.tab.legacy-tab { display: none !important; }
.panel.legacy-panel { display: none !important; }

#panel-observatory {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}
#panel-observatory:not(.active) { display: none; }

.observatory-stage {
    display: grid;
    grid-template-columns: 230px 1fr;
    flex: 1;
    min-height: 0;
}

/* --- Left rail --- */
.observatory-rail {
    border-right: 1px solid var(--border);
    background: var(--bg-panel);
    padding: var(--s-5) var(--s-4);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--s-5);
}
/* Dossier sections: divided by a single heavy top-rule, no per-section
   card backgrounds. The first section keeps its native spacing (no
   top rule) so the rail doesn't start with a divider line. */
.rail-section { display: flex; flex-direction: column; gap: var(--s-2); padding-top: var(--s-3); border-top: 1px solid var(--border); }
.rail-section:first-of-type { padding-top: 0; border-top: 0; }
.rail-label {
    font-family: var(--font-serif);
    font-size: 11px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--text-strong);
    padding-bottom: var(--s-1);
    border-bottom: 1px solid var(--border);
}
/* Case-letter prefix on rail section labels — set via data-case
   attribute wired by mountQualityRail() etc. in app.js. Falls back
   gracefully when data-case is absent (older renderers). */
.rail-section[data-case] .rail-label::before,
.rail-section[data-case] .rail-collapse-btn::before {
    content: "CASE " attr(data-case) " // ";
    color: var(--accent);
    font-family: var(--font-serif);
    letter-spacing: 0.14em;
}
/* Top-of-rail summary line — e.g. "01 / 12 FILTERS ACTIVE". Added
   via a lightweight .rail-filter-count element that JS populates
   if present; gracefully invisible if absent. */
.rail-filter-count {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.14em;
    color: var(--text-faint);
    text-transform: uppercase;
    padding-bottom: var(--s-2);
    border-bottom: 1px solid var(--border);
}
/* v0.9.0 — rail collapse button. Visually identical to .rail-label
   on desktop (sections always expanded; button is decorative). On
   touch / narrow viewports the chevron appears and clicking
   toggles the body visibility. */
.rail-collapse-btn {
    all: unset;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-bottom: var(--s-1);
    border-bottom: 1px solid var(--border);
    font-family: var(--font-serif);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--text-strong);
    cursor: default;
    box-sizing: border-box;
}
.rail-collapse-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.rail-chevron {
    display: none;  /* hidden on desktop — sections always expanded */
    transition: transform 150ms ease;
    opacity: 0.85;
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 11px;
}
/* Dossier glyph — swap the inline SVG content for ▾ so the rail
   feels typewriter-stamped. The existing transform: rotate(-90deg)
   below (pinned by test_v090) flips it to ▸ when collapsed. */
.rail-chevron svg { display: none; }
.rail-chevron::before {
    content: "▾";
    display: inline-block;
    line-height: 1;
}
.rail-body {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}
.rail-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.rail-list li {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    font-size: var(--fs-12);
    color: var(--text);
    padding: 3px 0;
}
.rail-list li input[type="checkbox"] { accent-color: var(--accent); }

/* Dossier monospace checkbox — [ ] / [x]. Applied via the generic
   ::before treatment on rail + filter checkboxes so every toggle in
   the rail feels typewriter-stamped. The native input is hidden
   visually but stays in the DOM for keyboard + screen-reader use. */
.rail-list input[type="checkbox"],
.rail-toggle-list input[type="checkbox"],
.movement-chip input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    width: 1.5em;
    height: 1em;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    position: relative;
    display: inline-block;
    flex-shrink: 0;
    font-family: var(--font-mono);
    color: var(--text-muted);
    line-height: 1;
    vertical-align: middle;
}
.rail-list input[type="checkbox"]::before,
.rail-toggle-list input[type="checkbox"]::before,
.movement-chip input[type="checkbox"]::before {
    content: "[ ]";
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0;
    color: var(--text-muted);
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}
.rail-list input[type="checkbox"]:checked::before,
.rail-toggle-list input[type="checkbox"]:checked::before,
.movement-chip input[type="checkbox"]:checked::before {
    content: "[x]";
    color: var(--accent);
    font-weight: 600;
}
.rail-list input[type="checkbox"]:focus-visible,
.rail-toggle-list input[type="checkbox"]:focus-visible,
.movement-chip input[type="checkbox"]:focus-visible {
    outline: 1px solid var(--accent);
    outline-offset: 2px;
}
.rail-list li .rail-ct {
    margin-left: auto;
    color: var(--text-muted);
    font-size: 10px;
    font-variant-numeric: tabular-nums;
}
.rail-big-count {
    font-family: var(--font-sans);
    font-size: 30px;
    font-weight: 600;
    color: var(--text-strong);
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}
.rail-sub {
    font-size: 10px;
    color: var(--text-muted);
    margin-top: var(--s-1);
}
.rail-time-label {
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

/* v0.9.0 — Mobile / touch layout for the Observatory.

   Two parallel triggers:
   (1) body.is-touch from matchMedia feature detect — catches real
       phones, foldables, iPads-in-landscape (wide viewport, coarse
       pointer).
   (2) @media (max-width: 700px) — catches desktop users resizing
       their browser window narrow.

   The rules are duplicated across the two triggers because CSS
   can't OR a class selector with a media query in the same rule.
   Edit both branches when changing mobile behaviour. */

@media (max-width: 700px) {
    .observatory-stage {
        grid-template-columns: 1fr;
        grid-template-rows: auto 1fr;
    }
    #header h1 .site-title-registry { display: none; }
    .classification-bar .classification-divider + span { display: none; }
    .brand-lockup { width: 32px; height: 32px; }
    .observatory-rail {
        max-height: 55vh;
        border-right: none;
        border-bottom: 1px solid var(--border);
        padding: var(--s-3);
        gap: var(--s-3);
    }
    .rail-chevron { display: block; cursor: pointer; }
    .rail-collapse-btn { cursor: pointer; }
    .rail-collapse-btn:hover { color: var(--text-strong); }
    /* Default: everything except Data Quality starts collapsed. */
    .rail-section:not(.rail-quality):not(.is-expanded) .rail-body {
        display: none;
    }
    .rail-section:not(.rail-quality):not(.is-expanded) .rail-chevron {
        transform: rotate(-90deg);
    }
    /* Explicit user-toggled expand beats the default-collapsed
       rule (selector specificity equal; order wins). */
    .rail-section.is-expanded .rail-body { display: flex; }
    .rail-section.is-expanded .rail-chevron { transform: rotate(0); }
    /* Rail Quality is default-expanded; user-toggled collapse. */
    .rail-section.rail-quality.is-collapsed .rail-body { display: none; }
    .rail-section.rail-quality.is-collapsed .rail-chevron {
        transform: rotate(-90deg);
    }
}

body.is-touch .observatory-stage {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
}
body.is-touch .observatory-rail {
    max-height: 55vh;
    border-right: none;
    border-bottom: 1px solid var(--border);
}
body.is-touch .rail-chevron { display: block; cursor: pointer; }
body.is-touch .rail-collapse-btn { cursor: pointer; }
body.is-touch .rail-collapse-btn:hover { color: var(--text-strong); }
body.is-touch .rail-section:not(.rail-quality):not(.is-expanded) .rail-body {
    display: none;
}
body.is-touch .rail-section:not(.rail-quality):not(.is-expanded) .rail-chevron {
    transform: rotate(-90deg);
}
body.is-touch .rail-section.is-expanded .rail-body { display: flex; }
body.is-touch .rail-section.is-expanded .rail-chevron { transform: rotate(0); }
body.is-touch .rail-section.rail-quality.is-collapsed .rail-body { display: none; }
body.is-touch .rail-section.rail-quality.is-collapsed .rail-chevron {
    transform: rotate(-90deg);
}

/* v0.9.0 — Responsive polish for the filter bar, stats badge,
   movement cluster, and timeline brush. Same dual-trigger pattern
   as the Observatory rail: @media (max-width: 700px) for narrow
   viewports + body.is-touch for coarse-pointer devices. */

@media (max-width: 700px) {
    #filters-bar {
        padding: var(--s-2) var(--s-3);
        gap: var(--s-2);
    }
    .filter-group { flex: 1 1 45%; min-width: 140px; }
    .filter-group-date { flex: 1 1 100%; }
    .filter-movement-row {
        padding: 6px 12px;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }
    .filter-movement-row::-webkit-scrollbar { display: none; }
    .movement-cluster { flex-wrap: nowrap; }

    .stats-badge { font-size: 10px; line-height: 1.5; }
    .stats-badge .stats-chip-optional { display: none; }

    .observatory-time-brush {
        height: auto;
        min-height: 140px;
        padding: 4px 8px;
    }
    /* Brush canvas needs an explicit min-height when the
       container switches to height:auto so flex:1 has
       something to resolve against. */
    .brush-wrap { min-height: 50px; }
    .brush-handle { width: 18px; }
}

@media (max-width: 500px) {
    .tabs-nav, .tabs {
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }
    .tabs-nav::-webkit-scrollbar,
    .tabs::-webkit-scrollbar { display: none; }
}

body.is-touch .observatory-time-brush { height: auto; min-height: 140px; }
body.is-touch .brush-wrap { min-height: 50px; }
body.is-touch .brush-handle {
    width: 14px;
    position: relative;
}
/* Expand the hit area without changing the visual — a
   pseudo-element stretches beyond the visible handle so taps
   near-but-not-exactly-on-it still register. */
body.is-touch .brush-handle::before {
    content: "";
    position: absolute;
    inset: -12px -8px;
}
body.is-touch .stats-badge .stats-chip-optional { display: none; }
body.is-touch .filter-movement-row {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
body.is-touch .filter-movement-row::-webkit-scrollbar { display: none; }
body.is-touch .movement-cluster { flex-wrap: nowrap; }


/* v0.8.2 — Data Quality rail. Each toggle has a primary label and a
   small sub-label with the threshold / description. Disabled toggles
   (for derived fields that aren't populated yet) render muted so the
   user can see the feature exists without being confused why their
   click does nothing. */
.rail-toggle-list li {
    display: flex;
    align-items: flex-start;
    gap: var(--s-2);
    padding: 4px 0;
}
.rail-toggle-list li label {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    cursor: pointer;
}
.rail-toggle-list .rail-toggle-sub {
    font-family: var(--font-mono);
    font-size: 9px;
    color: var(--text-muted);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    margin-top: 1px;
}
/* v0.9.1 — bias warning banner shown when the "High quality
   only" toggle is active. Lives inside the Observatory rail
   next to the Data Quality section. Amber styling so the user
   can't miss it, but low-key enough not to dominate the rail. */
.quality-bias-banner {
    position: relative;
    margin-top: var(--s-3);
    padding: 12px 10px 10px;
    border: 1px solid var(--accent);
    border-radius: 0;
    background: color-mix(in oklab, var(--bg-card) 86%, var(--accent) 14%);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 10px;
    line-height: 1.5;
    letter-spacing: 0.02em;
}
.quality-bias-banner::before {
    /* Oxblood "CLASSIFIED BIAS" stamp — serif caps, rotated, CSS-only. */
    content: "CLASSIFIED BIAS";
    position: absolute;
    top: -10px;
    right: 10px;
    padding: 2px 8px;
    transform: rotate(-3deg);
    transform-origin: top right;
    font-family: var(--font-serif);
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.18em;
    color: var(--accent);
    border: 2px solid var(--accent);
    background: var(--bg-panel);
    text-transform: uppercase;
    pointer-events: none;
}
.quality-bias-banner[hidden] { display: none; }
.quality-bias-banner strong {
    display: block;
    font-family: var(--font-serif);
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-size: 10px;
    font-weight: 600;
    margin-bottom: 4px;
}
.quality-bias-banner .quality-bias-link {
    color: var(--accent);
    text-decoration: underline;
}

.rail-toggle-list li.rail-toggle-disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.rail-toggle-list li.rail-toggle-disabled label {
    cursor: not-allowed;
}
.rail-toggle-list li.rail-toggle-disabled input[type="checkbox"] {
    cursor: not-allowed;
}

/* v0.11.1 — Data Quality gear popup for Timeline / Insights tabs.
   Mirrors the Observatory rail's quality toggles in a small
   floating popover anchored to the gear icon. */
.dq-gear-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    margin-left: auto;
}
.dq-gear-btn {
    all: unset;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 120ms, background 120ms;
}
.dq-gear-btn:hover {
    color: var(--text-strong);
    background: var(--surface-2);
}
.dq-gear-btn[aria-expanded="true"] {
    color: var(--accent);
    background: var(--surface-2);
}
.dq-gear-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.dq-gear-popup {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 1200;
    min-width: 260px;
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(0,0,0,0.35);
    padding: 10px 12px;
}
.dq-gear-popup[hidden] { display: none; }
.dq-gear-title {
    font-size: 10px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--border);
}
.dq-gear-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.dq-gear-list li {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 5px 0;
    font-size: 12px;
    color: var(--text);
}
.dq-gear-list li label {
    cursor: pointer;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
.dq-gear-list .rail-toggle-sub {
    font-size: 10px;
    color: var(--text-muted);
}
.dq-gear-list li.rail-toggle-disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.dq-gear-list li.rail-toggle-disabled label,
.dq-gear-list li.rail-toggle-disabled input { cursor: not-allowed; }

/* Active filter count badge on gear icon */
.dq-gear-badge {
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 14px;
    height: 14px;
    border-radius: 7px;
    background: var(--accent);
    color: #fff;
    font-size: 9px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 3px;
    pointer-events: none;
}
.dq-gear-badge:empty { display: none; }

/* --- Center canvas --- */
.observatory-canvas-wrap {
    position: relative;
    min-width: 0;
    /* Reserve vertical space before deck.gl-leaflet mounts. Without this,
       the wrap is 0px tall on first paint (grid row height indeterminate
       until TimeBrush computes below), then jumps to ~600px when JS
       finishes — a 0.58 CLS score. calc keeps it responsive: viewport
       minus classification bar + header + filter bar + TimeBrush
       footer (~480px total). */
    min-height: calc(100vh - 480px);
    background: var(--bg);
    overflow: hidden;
}
.observatory-canvas-wrap #map {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}
/* v0.11: TimeBrush is now a shared <main> footer, visible on
   Observatory + Timeline + Insights. Hidden on Methodology + AI
   where temporal navigation doesn't apply. The hiding is driven
   by a body-level data attribute set in switchTab(). */
body[data-hide-brush] .observatory-time-brush {
    display: none;
}

/* v0.10.0-fix: left offset bumped from 16px to 52px so the
   Points/Heatmap/HexBin toggle doesn't overlap Leaflet's
   +/- zoom controls (which sit at top:10px left:10px and are
   ~26px wide + ~10px padding). */
.observatory-topbar {
    position: absolute;
    top: 14px;
    left: 52px;
    right: 16px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    z-index: 600;
    pointer-events: none;
}
.observatory-topbar > * { pointer-events: auto; }
.mode-toggle {
    display: flex;
    gap: 2px;
    padding: 3px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
}
.mode-toggle .mode-btn {
    background: none;
    border: none;
    color: var(--text-muted);
    font-family: inherit;
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    padding: 6px 12px;
    cursor: pointer;
    transition: color var(--t-fast), background var(--t-fast);
}
.mode-toggle .mode-btn:hover { color: var(--text); }
.mode-toggle .mode-btn.active {
    background: var(--accent);
    color: var(--bg);
}
.mode-toggle .mode-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.observatory-hud {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-muted);
    letter-spacing: 0.04em;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    padding: 7px 12px;
    border-radius: var(--r-sm);
    display: flex;
    align-items: center;
    gap: 6px;
}
.observatory-hud .hud-label { color: var(--text-faint); }

/* v0.11.3 — Points mode controls: color-by + dot size */
.points-controls {
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--surface-1);
    border: 1px solid var(--border);
    padding: 4px 10px;
    border-radius: var(--r-sm);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.points-controls[hidden] { display: none; }
.points-control-label {
    color: var(--text-faint);
    font-size: 9px;
    letter-spacing: 0.12em;
}
.points-control-select {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 10px;
    padding: 2px 6px;
    border-radius: 3px;
    cursor: pointer;
}
.points-control-select:focus { border-color: var(--accent); outline: none; }
.points-control-slider {
    width: 60px;
    height: 4px;
    accent-color: var(--accent);
    cursor: pointer;
}
/* Color legend — bottom-left overlay on the map */
.color-legend {
    position: absolute;
    bottom: 56px;
    left: 12px;
    z-index: 600;
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    padding: 8px 10px;
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-muted);
    max-height: 240px;
    overflow-y: auto;
    pointer-events: auto;
}
.color-legend[hidden] { display: none; }
.color-legend-title {
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 6px;
}
.color-legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 1px 0;
    line-height: 1.4;
}
.color-legend-swatch {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}
@media (max-width: 700px) {
    .points-controls { padding: 3px 6px; gap: 4px; }
    .points-control-slider { width: 40px; }
    .color-legend { bottom: 48px; max-height: 160px; font-size: 9px; }
}
.observatory-hud .hud-pair #hud-lat,
.observatory-hud .hud-pair #hud-lon { color: var(--accent); font-variant-numeric: tabular-nums; }
.observatory-hud .hud-sep { color: var(--text-faint); }
.observatory-hud .hud-status {
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.observatory-hud .hud-status::before {
    content: "● ";
    color: var(--term-green);
    text-shadow: 0 0 4px var(--term-glow);
}

/* Corner brackets — pure CSS edge HUD, stays out of the content. */
.hud-brackets {
    position: absolute;
    inset: 10px;
    pointer-events: none;
    z-index: 550;
}
.hud-brackets .hud-br {
    position: absolute;
    width: 14px;
    height: 14px;
    border: 1px solid var(--accent);
    opacity: 0.55;
}
.hud-brackets .hud-br-tl { top: 0; left: 0;  border-right: none;  border-bottom: none; }
.hud-brackets .hud-br-tr { top: 0; right: 0; border-left: none;   border-bottom: none; }
.hud-brackets .hud-br-bl { bottom: 0; left: 0;  border-right: none; border-top: none; }
.hud-brackets .hud-br-br { bottom: 0; right: 0; border-left: none;  border-top: none; }

/* Coords toggle — small secondary control that lives next to the
   place search at the bottom-middle of the map (v0.7.1). Sits just
   above the place search on the right so All/Original/Geocoded is
   within thumb reach without colliding with the mode toggle. */
.coords-toggle {
    position: absolute;
    bottom: 80px;
    left: 50%;
    transform: translateX(120px);  /* to the right of the place search pod */
    z-index: 500;
}
@media (max-width: 720px) {
    .coords-toggle {
        bottom: 120px;
        left: 50%;
        transform: translateX(-50%);
    }
}
.coords-toggle select {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-12);
    padding: 4px 8px;
    border-radius: var(--r-sm);
}

/* --- Time brush at the bottom --- */
/* v0.10.0: bumped from 110px to 150px to fit the new overview
   mini-map below the main brush. The overview adds ~32px
   (24px canvas + 4px labels + 4px margin). */
.observatory-time-brush {
    height: 150px;
    flex-shrink: 0;
    padding: var(--s-3) var(--s-5);
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    background: var(--bg);
    border-top: 1px solid var(--border);
}
.brush-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
    font-family: var(--font-mono);
}
.brush-readout strong {
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    letter-spacing: 0.02em;
}
.brush-readout #brush-window-count {
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.brush-play-btn, .brush-reset-btn, .brush-mode-btn {
    background: transparent;
    border: 1px solid var(--accent);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.15em;
    padding: 5px 12px;
    cursor: pointer;
    border-radius: var(--r-sm);
    transition: background var(--t-fast), color var(--t-fast);
}
.brush-play-btn:hover, .brush-reset-btn:hover, .brush-mode-btn:hover {
    background: var(--accent);
    color: var(--bg);
}
.brush-play-btn.playing {
    background: var(--warning);
    border-color: var(--warning);
    color: var(--bg);
}
/* v0.11.1 — playback progress bar. Thin strip under the brush
   header showing how far through the range the window has advanced. */
.brush-play-progress {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: rgba(255,255,255,0.08);
    overflow: hidden;
    border-radius: 0 0 4px 4px;
}
.brush-play-progress[hidden] { display: none; }
.brush-play-progress-fill {
    height: 100%;
    width: 0%;
    background: var(--accent);
    transition: width 100ms linear;
    border-radius: 0 0 4px 4px;
}

/* v0.8.1 — Cumulative state gets a dashed border so users can tell
   the toggle is "on" at a glance without reading the label. */
.brush-mode-btn[data-mode="cumulative"] {
    border-style: dashed;
    background: rgba(0, 240, 255, 0.08);
}

/* v0.8.2-cleanup-2 — PLAY speed selector. Sits between the mode
   toggle and the time-window readout. Same visual language as the
   sibling buttons but smaller because it's a select, not a CTA. */
.brush-speed-label {
    font-family: var(--font-mono);
    font-size: 9px;
    letter-spacing: 0.15em;
    color: var(--text-muted);
    margin-left: var(--s-2);
}
.brush-speed-select {
    background: transparent;
    border: 1px solid var(--accent);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.05em;
    padding: 4px 6px;
    cursor: pointer;
    border-radius: var(--r-sm);
    margin-left: 4px;
    transition: background var(--t-fast), color var(--t-fast);
}
.brush-speed-select:hover,
.brush-speed-select:focus {
    background: var(--accent);
    color: var(--bg);
    outline: none;
}
.brush-speed-select option {
    background: var(--bg-panel);
    color: var(--text);
}
/* v0.10.0 — Overview mini-map. Always shows the full 34 AD → 2026
   range with a bilinear scale. Sits below the main brush inside
   .observatory-time-brush. Height: 28px (24px canvas + 4px labels). */
.overview-wrap {
    position: relative;
    height: 28px;
    flex-shrink: 0;
    user-select: none;
    margin-top: 4px;
}
.overview-wrap canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 24px;
    display: block;
    border-radius: 2px;
    background: color-mix(in oklab, var(--bg) 90%, var(--accent) 5%);
}
/* View box — cyan outline showing the main brush's zoom range.
   Positioned by JS via style.left + style.width as percentages. */
.overview-view-box {
    position: absolute;
    top: 0;
    height: 24px;
    border: 1.5px solid var(--accent);
    border-radius: 2px;
    background: rgba(0, 240, 255, 0.08);
    cursor: grab;
    z-index: 2;
}
.overview-view-box.dragging { cursor: grabbing; }
/* Edge handles for resizing the view box (= zoom) */
.overview-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 6px;
    cursor: ew-resize;
    z-index: 3;
}
.overview-handle-l { left: -3px; }
.overview-handle-r { right: -3px; }
/* Selection box — gold, inside the view box, showing the
   playback/filter window. Not directly draggable from the
   overview; the user drags it on the main brush above. */
.overview-selection-box {
    position: absolute;
    top: 2px;
    height: 20px;
    background: rgba(255, 179, 0, 0.35);
    border-left: 1px solid var(--accent-warm, #FFB300);
    border-right: 1px solid var(--accent-warm, #FFB300);
    pointer-events: none;
    z-index: 1;
}
/* Scale labels — positioned absolute at the edges */
.overview-labels {
    position: absolute;
    top: 24px;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    font-size: 8px;
    letter-spacing: 0.08em;
    color: var(--fg-dim, #8A94AD);
    pointer-events: none;
}
.overview-break {
    position: absolute;
    /* 15% of the way across = the 1900 scale break */
    left: 15%;
    transform: translateX(-50%);
    color: var(--accent-warm, #FFB300);
    font-weight: 600;
}

.brush-wrap {
    position: relative;
    flex: 1;
    user-select: none;
    border: 1px solid var(--border);
    background: var(--bg-panel);
    border-radius: var(--r-sm);
}
.brush-wrap canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}
.brush-window {
    position: absolute;
    top: 0;
    bottom: 0;
    background: var(--accent-bg);
    border-left: 2px solid var(--accent);
    border-right: 2px solid var(--accent);
    cursor: grab;
    box-shadow: 0 0 0 1px rgba(0, 240, 255, 0.12);
}
.brush-window.dragging { cursor: grabbing; }
.brush-window:focus-visible {
    outline: 2px solid var(--accent-hover);
    outline-offset: 2px;
}
.brush-handle {
    position: absolute;
    top: 50%;
    width: 8px;
    height: 22px;
    background: var(--accent);
    transform: translateY(-50%);
    cursor: ew-resize;
    border-radius: 1px;
}
.brush-handle-l { left: -5px; }
.brush-handle-r { right: -5px; }
.brush-annotations {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.brush-annotations .brush-ann {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--warning);
    opacity: 0.75;
}
.brush-annotations .brush-ann-label {
    position: absolute;
    top: 2px;
    font-family: var(--font-mono);
    font-size: 9px;
    color: var(--warning);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    white-space: nowrap;
    padding-left: 3px;
}

/* --- Theme toggle in the gear menu --- */
.theme-toggle {
    display: flex;
    gap: var(--s-2);
    padding: var(--s-2);
}
.theme-toggle .theme-opt {
    flex: 1;
    background: var(--bg-card);
    border: 1px solid var(--border);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 11px;
    padding: 8px 10px;
    cursor: pointer;
    border-radius: var(--r-sm);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    transition: border-color var(--t-fast), background var(--t-fast);
}
.theme-toggle .theme-opt:hover { border-color: var(--accent); }
.theme-toggle .theme-opt.active {
    border-color: var(--accent);
    background: var(--accent-bg);
    color: var(--accent);
}
.theme-toggle .theme-opt-name { font-weight: 600; }
.theme-toggle .theme-opt-hint {
    font-size: 9px;
    color: var(--text-muted);
    letter-spacing: 0.08em;
    text-transform: lowercase;
}

/* =========================================================================
   v0.8.4 — Top-nav theme pill
   Compact horizontal SIGNAL / DECLASS toggle that lives directly in the
   main header nav, next to the tab buttons. Shares the .theme-opt class
   with the gear-menu version so initThemeToggle() in app.js binds both.

   v0.12 Dossier redesign: the pill is hidden from view but the DOM
   nodes stay in place (test_v084 + test_v0112 assert their presence
   and the existing initThemeToggle() / setTheme() wiring binds them).
   Both themes now resolve to the same Dossier palette so the toggle
   is a no-op visually — the override below preserves the JS contract
   without exposing a dead control to the user.
   ========================================================================= */
.theme-pill {
    /* Dossier: toggle is hidden — both theme classes resolve to the
       same palette so a live switch would be a no-op. The rule still
       references tokens (kept for test_v084 invariants) and the DOM
       stays in place for test_v0112 + initThemeToggle() wiring. */
    display: none !important;
    align-items: center;
    gap: 0;
    margin: 0 var(--s-2);
    padding: 3px;
    background: var(--bg-panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-pill);
    font-family: var(--font-mono);
    color: var(--accent);
}
.theme-pill .theme-opt-compact {
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.12em;
    padding: 4px 10px;
    cursor: pointer;
    border-radius: calc(var(--r-pill) - 2px);
    text-transform: uppercase;
    transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.theme-pill .theme-opt-compact:hover {
    color: var(--accent);
}
.theme-pill .theme-opt-compact.active {
    background: var(--accent-bg);
    border-color: var(--accent);
    color: var(--accent);
}
.theme-pill .theme-opt-compact:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.theme-pill .theme-opt-name {
    display: inline-block;
}

/* On narrow viewports collapse the SIGNAL / DECLASS labels to just the
   first letter so the pill still fits next to the tabs. */
@media (max-width: 900px) {
    .theme-pill .theme-opt-name {
        font-size: 0;
    }
    .theme-pill .theme-opt-name::first-letter {
        font-size: 10px;
    }
    .theme-pill .theme-opt-compact {
        min-width: 24px;
    }
}

/* --- Hex bin layer color ramp (used by loadHexBins tooltips) --- */
.hex-tooltip {
    font-family: var(--font-mono);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}

/* =========================================================================
   v0.11.2: Cinematic intro overlay
   ========================================================================= */
.intro-overlay {
    position: fixed;
    inset: 0;
    z-index: 10000;
    background: var(--bg);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    opacity: 1;
    transition: opacity 600ms ease-out;
    pointer-events: auto;
}
.intro-overlay.intro-dissolving {
    opacity: 0;
    pointer-events: none;
}
.intro-overlay.intro-done {
    display: none;
}
.intro-content {
    text-align: center;
}
.intro-logo {
    /* Dossier "ACCESS GRANTED" top line — serif caps, letter-spaced,
       parchment cream on charcoal. No glow, no neon — this is an
       official stamp, not a console pulse. */
    font-family: var(--font-serif);
    font-size: clamp(1.5rem, 4vw, 2.5rem);
    font-weight: 700;
    color: var(--text-strong);
    letter-spacing: 0.22em;
    text-transform: uppercase;
    margin-bottom: 32px;
}
.intro-counter {
    /* Mono counter ticking to 614,505. The container + label + value
       pre-existed (DOM is pinned by test_v0112), just restyled. */
    font-family: var(--font-mono);
    font-size: clamp(2rem, 8vw, 6rem);
    font-weight: 600;
    color: var(--text-strong);
    letter-spacing: 0.04em;
    line-height: 1;
}
.intro-counter-label {
    /* Hide the label that used to read "LOADING". The dossier intro
       foregrounds the big counter; the sub-caption ("sightings
       indexed...") now lives in .intro-status. */
    display: none;
}
.intro-counter-value {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    min-width: 7ch;
    display: inline-block;
    text-align: center;
    color: var(--text-strong);
}
.intro-subcaption {
    /* Dossier sub-caption: "sightings indexed · 1947 — 2025 · declassified".
       Lowercase italic-feeling parchment line. */
    margin-top: 18px;
    font-family: var(--font-serif);
    font-style: italic;
    font-size: 13px;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}
.intro-status {
    /* Sub-caption / status ticker — parchment-faint, mono, serif
       letterspacing so it reads as a classification mark. */
    margin-top: 20px;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.28em;
    color: var(--text-muted);
    text-transform: uppercase;
    animation: dossier-pulse 2.4s ease-in-out infinite;
}

/* Oxblood "CLEARED" stamp flourish — appears briefly during the
   dissolve. Purely CSS; respects prefers-reduced-motion. */
.intro-overlay::after {
    content: "STAMPED // CLEARED";
    position: absolute;
    top: 38%;
    left: 50%;
    transform: translate(-50%, 0) rotate(-4deg) scale(0.9);
    padding: 6px 16px;
    font-family: var(--font-serif);
    font-weight: 700;
    font-size: 14px;
    letter-spacing: 0.24em;
    color: var(--accent);
    border: 2px solid var(--accent);
    background: color-mix(in oklab, var(--bg) 70%, var(--accent) 4%);
    opacity: 0;
    pointer-events: none;
    text-transform: uppercase;
}
.intro-overlay.intro-dissolving::after {
    animation: dossier-stamp 600ms ease-out forwards;
}

@keyframes dossier-pulse {
    0%, 100% { opacity: 0.75; }
    50%      { opacity: 1; }
}
@keyframes dossier-stamp {
    0%   { opacity: 0; transform: translate(-50%, 0) rotate(-4deg) scale(1.25); }
    30%  { opacity: 1; transform: translate(-50%, 0) rotate(-4deg) scale(0.95); }
    100% { opacity: 0; transform: translate(-50%, 0) rotate(-4deg) scale(0.95); }
}

@media (prefers-reduced-motion: reduce) {
    .intro-overlay { transition: none; }
    .intro-logo { animation: none; }
    .intro-status { animation: none; }
    .intro-overlay.intro-dissolving::after { animation: none; opacity: 0; }
}

/* =========================================================================
   v0.11.2: Guided tooltip tour
   ========================================================================= */
.tour-backdrop {
    position: fixed;
    inset: 0;
    z-index: 10001;
    background: rgba(0, 0, 0, 0.55);
    pointer-events: auto;
    transition: opacity 200ms;
}
.tour-backdrop[hidden] { display: none; }

.tour-tooltip {
    position: fixed;
    z-index: 10002;
    max-width: 340px;
    background: var(--surface-1);
    border: 1px solid var(--accent);
    border-radius: 0;   /* Dossier: square corners */
    padding: 16px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--text);
    transition: top 300ms ease-out, left 300ms ease-out, opacity 150ms;
}
/* Dossier corner ticks on the tooltip — four L-shaped pseudo rules
   via SVG-free border snippets. ::before and ::after handle the
   top-left + bottom-right corners; the arrow pseudo is reserved. */
.tour-tooltip > .tour-tooltip-corners {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.tour-tooltip::before,
.tour-tooltip::after {
    content: "";
    position: absolute;
    width: 14px;
    height: 14px;
    border: 2px solid var(--accent);
    pointer-events: none;
}
.tour-tooltip::before {
    top: -4px;
    left: -4px;
    border-right: 0;
    border-bottom: 0;
}
.tour-tooltip::after {
    bottom: -4px;
    right: -4px;
    border-left: 0;
    border-top: 0;
}
/* Optional STEP label above the body — added via a
   .tour-tooltip-step span. Renders serif caps with oxblood. */
.tour-tooltip-step {
    display: block;
    font-family: var(--font-serif);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.22em;
    color: var(--accent);
    margin-bottom: 6px;
    text-transform: uppercase;
}
.tour-tooltip[hidden] { display: none; }

.tour-tooltip-body {
    margin-bottom: 12px;
    line-height: 1.6;
    color: var(--text);
}
.tour-tooltip-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.tour-tooltip-progress {
    font-size: 11px;
    color: var(--text-muted);
    font-family: var(--font-mono);
}
.tour-tooltip-actions {
    display: flex;
    gap: 8px;
}
.tour-btn {
    padding: 4px 12px;
    border-radius: 4px;
    font-family: var(--font-mono);
    font-size: 12px;
    cursor: pointer;
    border: 1px solid var(--border);
    transition: background 120ms, color 120ms;
}
.tour-btn-skip {
    background: transparent;
    color: var(--text-muted);
}
.tour-btn-skip:hover {
    color: var(--text);
    background: var(--surface-2);
}
.tour-btn-next {
    background: var(--accent);
    color: var(--bg);
    border-color: var(--accent);
    font-weight: 600;
}
.tour-btn-next:hover {
    filter: brightness(1.15);
}

/* Arrow (CSS triangle via rotated square) */
.tour-tooltip-arrow {
    position: absolute;
    width: 12px;
    height: 12px;
    background: var(--surface-1);
    border: 1px solid var(--accent);
    transform: rotate(45deg);
    pointer-events: none;
}
.tour-tooltip[data-position="bottom"] .tour-tooltip-arrow {
    top: -7px;
    left: 50%;
    margin-left: -6px;
    border-right: none;
    border-bottom: none;
}
.tour-tooltip[data-position="top"] .tour-tooltip-arrow {
    bottom: -7px;
    left: 50%;
    margin-left: -6px;
    border-left: none;
    border-top: none;
}
.tour-tooltip[data-position="left"] .tour-tooltip-arrow {
    right: -7px;
    top: 24px;
    border-left: none;
    border-bottom: none;
}
.tour-tooltip[data-position="right"] .tour-tooltip-arrow {
    left: -7px;
    top: 24px;
    border-right: none;
    border-top: none;
}

/* Mobile: fixed bottom sheet so buttons are always visible */
@media (max-width: 700px) {
    .tour-tooltip {
        position: fixed !important;
        bottom: 12px !important;
        top: auto !important;
        left: 12px !important;
        right: 12px !important;
        max-width: none;
        width: auto;
        max-height: 45vh;
        overflow-y: auto;
    }
    .tour-tooltip-arrow { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .tour-tooltip { transition: none; }
    .tour-backdrop { transition: none; }
}

/* =========================================================================
   v0.11.2: Credits modal
   ========================================================================= */
.credits-overlay {
    position: fixed;
    inset: 0;
    z-index: 9998;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 200ms ease-out;
    pointer-events: none;
}
.credits-overlay.is-open {
    opacity: 1;
    pointer-events: auto;
}
.credits-overlay[hidden] { display: none; }
.credits-modal {
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 32px;
    max-width: 480px;
    width: 90vw;
    position: relative;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.credits-close {
    position: absolute;
    top: 12px;
    right: 12px;
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 22px;
    cursor: pointer;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
}
.credits-close:hover { color: var(--text); background: var(--surface-2); }
.credits-title {
    font-family: var(--font-mono);
    font-size: 18px;
    font-weight: 700;
    color: var(--accent);
    letter-spacing: 0.05em;
    margin-bottom: 6px;
}
.credits-subtitle {
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.5;
    margin-bottom: 20px;
}
.credits-role {
    padding: 12px 0;
    border-top: 1px solid var(--border);
}
.credits-role h3 {
    font-family: var(--font-mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--text-muted);
    margin-bottom: 6px;
}
.credits-handle {
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
}
.credits-handle:hover { text-decoration: underline; }
.credits-sep { color: var(--text-faint); margin: 0 6px; }
.credits-link {
    color: var(--text);
    font-size: 13px;
    text-decoration: none;
}
.credits-link:hover { text-decoration: underline; }
.credits-desc {
    font-size: 12px;
    color: var(--text-muted);
    line-height: 1.5;
    margin-top: 4px;
}
.credits-desc a { color: var(--accent); text-decoration: none; }
.credits-desc a:hover { text-decoration: underline; }
.settings-menu-divider {
    height: 1px;
    background: var(--border);
    margin: 4px 0;
}

/* =========================================================================
   v0.11.2: Mobile-ready fixes (375px phone target)
   =========================================================================
   Addresses: header button visibility, tab/filter touch targets,
   modal detail grid, brush header overflow, chat popover height.
   Layered on top of the existing 720px / 700px / 500px breakpoints. */

/* --- 600px: Modal + detail grid --- */
@media (max-width: 600px) {
    .modal {
        width: 96vw;
        max-height: 90vh;
        border-radius: 8px;
    }
    .detail-grid {
        grid-template-columns: 1fr;
    }
    .modal-header { padding: 10px 14px; }
    .modal-body { padding: 12px 14px; }
    .modal-close { font-size: 20px; width: 32px; height: 32px; }
}

/* --- 500px: Brush header wraps, filters reflow --- */
@media (max-width: 500px) {
    /* Brush header: allow wrapping so PLAY/SPEED/READOUT
       stack into two rows instead of overflowing */
    .brush-header {
        flex-wrap: wrap;
        gap: 4px 8px;
        padding: 4px 8px;
    }
    .brush-readout {
        flex: 1 1 100%;
        font-size: 9px;
        text-align: center;
    }
    .brush-play-btn, .brush-reset-btn, .brush-mode-btn {
        font-size: 9px;
        padding: 4px 8px;
        min-height: 28px;
        min-width: 44px;
    }
    .brush-speed-select {
        font-size: 9px;
        padding: 2px 4px;
        min-height: 28px;
    }
    .brush-speed-label { font-size: 9px; }

    /* Filter groups: drop min-width so they don't overflow */
    .filter-group { min-width: 0; flex: 1 1 42%; }

    /* Tabs: bigger touch targets */
    .tab {
        min-height: 36px;
        padding: 8px 10px;
    }

    /* Header: horizontal layout with scrolling tabs */
    #header {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        padding: 6px 8px;
        gap: 4px;
    }
    .header-left {
        flex: 0 0 auto;
        max-width: 45%;
    }
    .header-left h1 { font-size: 13px; }
    .stats-badge { display: none; }
    .tabs {
        flex: 1 1 0;
        order: 1;
        gap: 2px;
    }
    .tabs-settings {
        order: 2;
        flex: 0 0 auto;
    }
}

/* --- 375px: Tight phone layout --- */
@media (max-width: 400px) {
    #header { padding: 4px 6px; }
    .header-left h1 { font-size: 12px; letter-spacing: 0; }

    /* Tabs shrink further */
    .tab {
        font-size: 11px;
        padding: 6px 8px;
        min-height: 32px;
    }

    /* Brush: auto-height so histogram bars always visible */
    .observatory-time-brush { height: auto; min-height: 130px; }
    .brush-wrap { min-height: 45px; }

    /* Filter dropdowns: full width stacking */
    .filter-group { flex: 1 1 100%; min-width: 0; }

    /* Insights jump nav: scroll not wrap */
    .insights-jump-nav {
        overflow-x: auto;
        flex-wrap: nowrap;
        scrollbar-width: none;
        gap: 4px;
    }
    .insights-jump-nav::-webkit-scrollbar { display: none; }
    .jump-pill { flex-shrink: 0; font-size: 11px; }

    /* Chat popover: cap height to viewport */
    .chat-popover { height: 60vh; max-height: 400px; }
    .chat-fab { min-width: 44px; min-height: 44px; }

    /* DQ gear popup: left-align on narrow screens */
    .dq-gear-popup { right: -8px; min-width: 240px; }
}

/* --- Touch-friendly hit targets (any mobile) --- */
@media (max-width: 700px) {
    /* Minimum 44px touch targets for interactive elements */
    .rail-collapse-btn { min-height: 44px; display: flex; align-items: center; }
    .movement-chip { min-height: 32px; }
    .gran-btn { min-height: 32px; min-width: 44px; }

    /* Brush handles: expand visual + hit area */
    .brush-handle { width: 18px; }
    .brush-handle::before {
        content: "";
        position: absolute;
        inset: -12px -10px;
    }

    /* Observatory rail: enable scrolling if content overflows */
    .observatory-rail { overflow-y: auto; }
}
