/* ── Stacking scale ──────────────────────────────────────────────────────
   One ordered set of named tiers, replacing the ad-hoc 1/2/3/4/5/10/12/100/
   1000 that accumulated in arrival order. Gaps are deliberate: a new surface
   slots between two tiers without renumbering, and nothing needs to "win" by
   picking a bigger number.

   Local stacking inside a panel (a resize grip against its own panel body)
   stays a plain small integer — those never compete with the tiers here,
   because a positioned ancestor with a z-index opens its own context.

   --z-app-header, --z-mobile-takeover and --z-modal have no users yet: the
   first two are reserved for the layout scaffold (#363), and --z-modal names
   the top tier so a future dialog doesn't have to invent one. */
:root {
    --z-map: 1;               /* the map canvas */
    --z-map-overlay: 10;      /* #map-overlay-rail — legend, sliders, hex */
    --z-chart-panel: 20;      /* chart panels, cascaded over the map */
    --z-sidebar: 30;          /* the sidebar (and the legacy floating chat) */
    --z-sidebar-show: 35;     /* re-open affordance, above the collapsed edge */
    --z-app-header: 40;       /* reserved: app chrome / nav band */
    --z-mobile-takeover: 50;  /* reserved: full-viewport panels at phone width */
    --z-tooltip: 60;          /* hover readouts, above all chrome */
    --z-modal: 70;            /* blocking dialogs */
}

/* ── Theme tokens ────────────────────────────────────────────────────────
   Four families, by where the surface sits:

     --chrome-*   the header + sidebar shell, one continuous surface
     --control-*  buttons inside it. Deliberately OPAQUE: a translucent
                  control picks up whatever is behind it, so a row of them
                  reads as inconsistent even when the rules are identical
     --panel-*    floating panels over the map (legend, hex, sliders, charts,
                  tooltips). These must stay readable over any basemap, so
                  they carry their own surface rather than relying on a blur
     --chat-*     chat bubbles, applied in sidebar mode only

   Selected by theme.js as a body class. `light` is the default because it is
   what the sidebar has always been. */
:root,
body.theme-light {
    /* Native controls and scrollbars follow the palette too. */
    color-scheme: light;
    --chrome-bg: #f8f9fa;
    --chrome-bg-raised: #ffffff;
    --chrome-fg: #1a1a2e;
    --chrome-fg-muted: rgba(0, 0, 0, 0.55);
    --chrome-line: rgba(0, 0, 0, 0.1);
    --chrome-hover: rgba(0, 0, 0, 0.06);
    --chrome-accent: rgba(120, 170, 255, 0.8);
    --chrome-accent-wash: rgba(120, 170, 255, 0.12);
    --chrome-carbon: #2e7d32;
    --chrome-shadow: rgba(0, 0, 0, 0.18);
    --control-bg: #2f6fd0;
    /* Direction a control moves when hovered or selected: toward black on
       a light palette, toward white on a dark one. Kept as a token so a
       `primary` override recolours every state, and so `auto` picks the
       right direction live. */
    --control-mix-target: black;
    --control-bg-hover: color-mix(in srgb, var(--control-bg) 88%, var(--control-mix-target));
    --control-border: transparent;
    --control-fg: #ffffff;
    --control-active-border: transparent;
    --control-accent: #2f6fd0;
    --control-disabled-bg: #ccd6e6;
    --control-disabled-fg: #4c5768;
    --focus-ring: #1c4a92;
    --control-focus-fg: #0b62a4;
    --control-focus-bg: rgba(0, 124, 191, 0.12);
    --panel-bg: rgba(255, 255, 255, 0.92);
    --panel-bg-strong: rgba(255, 255, 255, 0.97);
    --panel-fg: #333333;
    --panel-fg-muted: #666666;
    --panel-fg-faint: #767676;
    --panel-line: rgba(0, 0, 0, 0.15);
    --panel-hover: rgba(0, 0, 0, 0.07);
    --panel-shadow: rgba(0, 0, 0, 0.2);
    --chat-surface: rgba(255, 255, 255, 0.55);
    --chat-surface-soft: rgba(255, 255, 255, 0.35);
    --chat-fg: #333333;
    --chat-code-bg: rgba(0, 0, 0, 0.06);
    --chat-tool-bg: rgba(245, 248, 255, 0.6);
    --chat-thinking-bg: rgba(173, 216, 230, 0.35);
    --chat-thinking-fg: #2c5282;
    /* Timeline labels, tool names and disclosure arrows in the chat. */
    --chat-accent-fg: #2c5282;
    --chat-input-bg: rgba(255, 255, 255, 0.45);
    --chat-input-bg-focus: rgba(255, 255, 255, 0.65);
    --danger-fg: #c0392b;
    --map-void-bg: #dfe6ee;
    --map-void-pattern: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='110.9' height='192.0' viewBox='0 0 110.9 192.0'><g fill='none' stroke='%2364748b' stroke-opacity='0.09' stroke-width='1'><path d='M0.0,-64.0 L55.4,-32.0 L55.4,32.0 L0.0,64.0 L-55.4,32.0 L-55.4,-32.0 Z'/><path d='M110.9,-64.0 L166.3,-32.0 L166.3,32.0 L110.9,64.0 L55.4,32.0 L55.4,-32.0 Z'/><path d='M0.0,128.0 L55.4,160.0 L55.4,224.0 L0.0,256.0 L-55.4,224.0 L-55.4,160.0 Z'/><path d='M110.9,128.0 L166.3,160.0 L166.3,224.0 L110.9,256.0 L55.4,224.0 L55.4,160.0 Z'/><path d='M55.4,32.0 L110.9,64.0 L110.9,128.0 L55.4,160.0 L-0.0,128.0 L0.0,64.0 Z'/></g></svg>");
}

body.theme-dark {
    color-scheme: dark;
    --chrome-bg: #111b2e;
    --chrome-bg-raised: #1b2740;
    --chrome-fg: #e8eef7;
    --chrome-fg-muted: rgba(232, 238, 247, 0.62);
    --chrome-line: rgba(255, 255, 255, 0.14);
    --chrome-hover: rgba(255, 255, 255, 0.09);
    --chrome-accent: rgba(120, 170, 255, 0.9);
    --chrome-accent-wash: rgba(120, 170, 255, 0.18);
    --chrome-carbon: #8bc34a;
    --chrome-shadow: rgba(0, 0, 0, 0.45);
    --control-bg: #4272ad;
    /* Direction a control moves when hovered or selected: toward black on
       a light palette, toward white on a dark one. Kept as a token so a
       `primary` override recolours every state, and so `auto` picks the
       right direction live. */
    --control-mix-target: white;
    --control-bg-hover: color-mix(in srgb, var(--control-bg) 88%, var(--control-mix-target));
    --control-border: transparent;
    --control-fg: #eaf1fb;
    --control-active-border: transparent;
    --control-accent: #5b8fd6;
    --control-disabled-bg: #26344d;
    --control-disabled-fg: #97a3b5;
    --focus-ring: #9ec5ff;
    --control-focus-fg: #7cc4f0;
    --control-focus-bg: rgba(0, 124, 191, 0.28);
    --panel-bg: rgba(17, 27, 46, 0.92);
    --panel-bg-strong: rgba(17, 27, 46, 0.97);
    --panel-fg: #e8eef7;
    --panel-fg-muted: rgba(232, 238, 247, 0.62);
    --panel-fg-faint: rgba(232, 238, 247, 0.4);
    --panel-line: rgba(255, 255, 255, 0.16);
    --panel-hover: rgba(255, 255, 255, 0.1);
    --panel-shadow: rgba(0, 0, 0, 0.5);
    --chat-surface: rgba(255, 255, 255, 0.07);
    --chat-surface-soft: rgba(255, 255, 255, 0.04);
    --chat-fg: #e2e8f0;
    --chat-code-bg: rgba(0, 0, 0, 0.35);
    --chat-tool-bg: rgba(255, 255, 255, 0.05);
    --chat-thinking-bg: rgba(56, 120, 160, 0.25);
    --chat-thinking-fg: #bfdbfe;
    --chat-accent-fg: #a9c8f5;
    --chat-input-bg: rgba(255, 255, 255, 0.08);
    --chat-input-bg-focus: rgba(255, 255, 255, 0.14);
    --danger-fg: #ff8a80;
    --map-void-bg: #0c1424;
    --map-void-pattern: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='110.9' height='192.0' viewBox='0 0 110.9 192.0'><g fill='none' stroke='%238fa8c8' stroke-opacity='0.055' stroke-width='1'><path d='M0.0,-64.0 L55.4,-32.0 L55.4,32.0 L0.0,64.0 L-55.4,32.0 L-55.4,-32.0 Z'/><path d='M110.9,-64.0 L166.3,-32.0 L166.3,32.0 L110.9,64.0 L55.4,32.0 L55.4,-32.0 Z'/><path d='M0.0,128.0 L55.4,160.0 L55.4,224.0 L0.0,256.0 L-55.4,224.0 L-55.4,160.0 Z'/><path d='M110.9,128.0 L166.3,160.0 L166.3,224.0 L110.9,256.0 L55.4,224.0 L55.4,160.0 Z'/><path d='M55.4,32.0 L110.9,64.0 L110.9,128.0 L55.4,160.0 L-0.0,128.0 L0.0,64.0 Z'/></g></svg>");
}

/* `auto` follows the viewer's OS setting live, which is why theme.js leaves
   it as its own class instead of resolving it to light or dark at boot. */
@media (prefers-color-scheme: dark) {
    body.theme-auto {
        color-scheme: dark;
        --chrome-bg: #111b2e;
        --chrome-bg-raised: #1b2740;
        --chrome-fg: #e8eef7;
        --chrome-fg-muted: rgba(232, 238, 247, 0.62);
        --chrome-line: rgba(255, 255, 255, 0.14);
        --chrome-hover: rgba(255, 255, 255, 0.09);
        --chrome-accent: rgba(120, 170, 255, 0.9);
        --chrome-accent-wash: rgba(120, 170, 255, 0.18);
        --chrome-carbon: #8bc34a;
        --chrome-shadow: rgba(0, 0, 0, 0.45);
        --control-bg: #4272ad;
        /* Direction a control moves when hovered or selected: toward black on
           a light palette, toward white on a dark one. Kept as a token so a
           `primary` override recolours every state, and so `auto` picks the
           right direction live. */
        --control-mix-target: white;
        --control-bg-hover: color-mix(in srgb, var(--control-bg) 88%, var(--control-mix-target));
        --control-border: transparent;
        --control-fg: #eaf1fb;
        --control-active-border: transparent;
        --control-accent: #5b8fd6;
        --control-disabled-bg: #26344d;
        --control-disabled-fg: #97a3b5;
        --focus-ring: #9ec5ff;
        --control-focus-fg: #7cc4f0;
        --control-focus-bg: rgba(0, 124, 191, 0.28);
        --panel-bg: rgba(17, 27, 46, 0.92);
        --panel-bg-strong: rgba(17, 27, 46, 0.97);
        --panel-fg: #e8eef7;
        --panel-fg-muted: rgba(232, 238, 247, 0.62);
        --panel-fg-faint: rgba(232, 238, 247, 0.4);
        --panel-line: rgba(255, 255, 255, 0.16);
        --panel-hover: rgba(255, 255, 255, 0.1);
        --panel-shadow: rgba(0, 0, 0, 0.5);
        --chat-surface: rgba(255, 255, 255, 0.07);
        --chat-surface-soft: rgba(255, 255, 255, 0.04);
        --chat-fg: #e2e8f0;
        --chat-code-bg: rgba(0, 0, 0, 0.35);
        --chat-tool-bg: rgba(255, 255, 255, 0.05);
        --chat-thinking-bg: rgba(56, 120, 160, 0.25);
        --chat-thinking-fg: #bfdbfe;
        --chat-accent-fg: #a9c8f5;
        --chat-input-bg: rgba(255, 255, 255, 0.08);
        --chat-input-bg-focus: rgba(255, 255, 255, 0.14);
        --danger-fg: #ff8a80;
    --map-void-bg: #0c1424;
        --map-void-pattern: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='110.9' height='192.0' viewBox='0 0 110.9 192.0'><g fill='none' stroke='%238fa8c8' stroke-opacity='0.055' stroke-width='1'><path d='M0.0,-64.0 L55.4,-32.0 L55.4,32.0 L0.0,64.0 L-55.4,32.0 L-55.4,-32.0 Z'/><path d='M110.9,-64.0 L166.3,-32.0 L166.3,32.0 L110.9,64.0 L55.4,32.0 L55.4,-32.0 Z'/><path d='M0.0,128.0 L55.4,160.0 L55.4,224.0 L0.0,256.0 L-55.4,224.0 L-55.4,160.0 Z'/><path d='M110.9,128.0 L166.3,160.0 L166.3,224.0 L110.9,256.0 L55.4,224.0 L55.4,160.0 Z'/><path d='M55.4,32.0 L110.9,64.0 L110.9,128.0 L55.4,160.0 L-0.0,128.0 L0.0,64.0 Z'/></g></svg>");
    }
}

body {
    margin: 0;
    padding: 0;
    /* The sidebar layout had no font of its own — only the floating chat
       declared one — so it fell back to the browser default, which is a
       serif on most desktops. Same system stack the floating chat uses. */
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    /* 0 unless app-header.js enables the band, so every `top` below is
       written the same way whether or not there is a header. */
    --app-header-h: 0px;
}

/* app-header.js sets --app-header-base-h; the device's top inset is added
   here so notched phones and any browser UI that eats into the viewport get
   real clearance instead of the first row of controls hugging the edge.
   Everything below the band reads --app-header-h and ignores the split. */
body.has-app-header {
    --app-header-h: calc(var(--app-header-base-h, 64px) + env(safe-area-inset-top, 0px));
}

#map {
    position: absolute;
    z-index: var(--z-map);
    top: 0;
    bottom: 0;
    width: 100%;
    /* Shows wherever no tile covers the canvas — most visibly the space
       around the globe in globe projection, which was previously bare white.
       The hex tile is a quiet nod to the H3 grid the analytics are built on;
       it is meant to read as texture, not as data, so it stays low-contrast
       and is themed alongside the rest of the chrome. */
    background-color: var(--map-void-bg);
    background-image: var(--map-void-pattern);
    background-repeat: repeat;
}

/* ── App header ──────────────────────────────────────────────────────────
   App chrome: identity and navigation. Map-scoped controls do NOT go here —
   they belong in #map-overlay-rail. See app/app-header.js and #363.

   Two modes on one component. `scrim` (default) floats over a full-bleed
   map, so the header costs no map area; `solid` is opaque and the map starts
   below it. Everything that needs to sit under the band reads
   --app-header-h, which is 0px when there is no header. */
#app-header {
    position: fixed;
    z-index: var(--z-app-header);
    top: 0;
    left: 0;
    right: 0;
    height: var(--app-header-h);
    /* Keep the contents out of the inset; the band itself still spans it, so
       the background reaches the very top of the viewport. */
    padding-top: env(safe-area-inset-top, 0px);
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 0 16px;
    font-family: 'Open Sans', sans-serif;
    background: var(--chrome-bg);
    color: var(--chrome-fg);
}

/* Scrim: a gradient, not a flat tint, so map detail stays readable at the
   very top of the viewport while the text above it keeps its contrast.
   Deliberately always dark-on-map regardless of theme — a light scrim over
   satellite imagery is unreadable. */
#app-header[data-mode="scrim"] {
    background: linear-gradient(to bottom,
        rgba(16, 20, 24, 0.78) 0%,
        rgba(16, 20, 24, 0.55) 60%,
        rgba(16, 20, 24, 0) 100%);
    color: #f1f5f9;
    border-bottom: none;
    /* The band spans the viewport but is mostly empty; let clicks through it
       reach the map, while the controls themselves stay interactive. */
    pointer-events: none;
}

#app-header[data-mode="scrim"] > * {
    pointer-events: auto;
}

/* Neither the edge nor the shadow is set here — both live on ::after so
   they stop at the sidebar. A border-bottom across the full width would
   draw a line through the join, which is the one place header and sidebar
   should read as a single surface rather than two stacked ones. */

/* The edge and shadow the header casts onto the map.
   Header and sidebar are one continuous chrome surface, so anything drawn
   across the sidebar's top edge — a border or a shadow — cuts a seam
   through the middle of it. Carrying both on a pseudo-element lets them
   stop where the sidebar starts: over the map the bar reads as raised and
   separated from the basemap, and against the sidebar the two join with no
   line at all. --sidebar-width is updated live during a drag, so the inset
   tracks a resize without any JS.

   border-top sits on the pseudo-element's top edge, which is the header's
   bottom edge, so it lands exactly where a border-bottom would have. */
#app-header[data-mode="solid"]::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    right: var(--chrome-shadow-inset, 0px);
    height: 10px;
    pointer-events: none;
    border-top: 1px solid var(--chrome-line);
    background: linear-gradient(to bottom,
        var(--chrome-shadow) 0%,
        rgba(0, 0, 0, 0) 100%);
}

body.sidebar-mode {
    --chrome-shadow-inset: var(--sidebar-width, 0px);
}

/* Collapsed, the sidebar is off-screen and the map runs full width again. */
body.sidebar-mode.sidebar-collapsed {
    --chrome-shadow-inset: 0px;
}

/* Only the solid band takes space from the map; the scrim overlays it. */
body.has-app-header.app-header-solid #map {
    top: var(--app-header-h);
}

/* The sidebar always starts below the band — sliding it under a scrim would
   put chrome over its own header. */
body.has-app-header #sidebar,
body.has-app-header #sidebar-show-btn {
    top: var(--app-header-h);
}

/* Anything else anchored to the top of the viewport has to clear the band
   too, or it lands underneath it. The floating-mode layer panel is measured
   from the viewport, so it needs the full offset... */
body.has-app-header #menu {
    top: calc(var(--app-header-h) + 10px);
}

/* ...whereas MapLibre's own controls are measured from #map, which already
   starts below a solid band. Only the scrim case needs the nudge. */
body.has-app-header:not(.app-header-solid) .maplibregl-ctrl-top-left,
body.has-app-header:not(.app-header-solid) .maplibregl-ctrl-top-right {
    margin-top: var(--app-header-h);
}

.app-header-brand,
.app-header-end {
    display: flex;
    align-items: center;
    gap: 14px;
    min-width: 0;
}

.app-header-logo {
    display: block;
    height: 28px;
    width: auto;
    max-width: 180px;
    object-fit: contain;
}

.app-header-logo--partner,
.app-header-logo--institution {
    height: 24px;
}

/* The partner slot holds its width even when empty, so the institution mark
   lands in the same place across the fleet whether or not an app has a
   partner. Without it the header visibly reflows app to app. */
.app-header-partner-slot {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    min-width: 64px;
    min-height: 24px;
}

/* At phone width the reserved gap is dead space that pushes the marks off
   screen, so it collapses; the logos still order the same way. */
@media (max-width: 700px) {
    .app-header-partner-slot:empty {
        display: none;
    }
}

.app-header-logo-link {
    display: flex;
    align-items: center;
}

.app-header-title {
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

#app-header-nav {
    display: flex;
    align-items: center;
    gap: 4px;
}

/* Nav links carry an icon beside the label, not instead of it. */
.app-header-link,
.app-header-menu-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.app-header-link-icon {
    display: inline-flex;
    flex: 0 0 auto;
}

.app-header-link {
    padding: 6px 10px;
    border-radius: 4px;
    font-size: 13px;
    color: inherit;
    opacity: 0.85;
    text-decoration: none;
    white-space: nowrap;
    transition: color 120ms ease, background 120ms ease;
}

.app-header-link:hover {
    opacity: 1;
    background: var(--chrome-hover);
}


/* ── Header menu button + takeover (narrow viewports) ────────────────────
   Hidden above the breakpoint. Below it the nav row is replaced rather than
   compressed — see #app-header-menu. */
#app-header-menu-btn {
    display: none;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 36px;
    height: 36px;
    padding: 0 7px;
    background: none;
    border: none;
    cursor: pointer;
}

#app-header-menu-btn span {
    display: block;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
}

#app-header-menu {
    position: fixed;
    z-index: var(--z-mobile-takeover);
    inset: 0;
    display: flex;
    flex-direction: column;
    padding: calc(16px + env(safe-area-inset-top, 0px)) 16px 16px;
    background: var(--chrome-bg);
    color: var(--chrome-fg);
}

#app-header-menu[hidden] {
    display: none;
}

.app-header-menu-close {
    align-self: flex-end;
    width: 40px;
    height: 40px;
    font-size: 20px;
    line-height: 1;
    background: none;
    border: none;
    color: inherit;
    cursor: pointer;
}

#app-header-menu nav {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-top: 8px;
}

.app-header-menu-link {
    padding: 14px 8px;
    font-size: 17px;
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid var(--chrome-line);
}

/* When the band runs short, the title gives way first and the logos next;
   the nav links and the menu button never do. Images in a flex row keep
   their natural width unless told otherwise, so without this a partner plus
   two institution marks overflowed the end zone and ran under the menu
   button (#381). Each wrapper and each image may shrink, down to a floor;
   object-fit keeps a squeezed mark in proportion. */
.app-header-end {
    flex: 1 1 auto;
    justify-content: flex-end;
}

/* A large shrink weight, so the brand zone — really its title, which
   ellipsizes — absorbs a shortfall before any logo is squeezed. */
.app-header-brand {
    flex-shrink: 100;
}

#app-header-nav,
#app-header-menu-btn {
    flex: 0 0 auto;
}

.app-header-brand > *,
.app-header-end > .app-header-logo-link,
.app-header-end > .app-header-logo-pair,
.app-header-end > .app-header-logo,
.app-header-partner-slot > *,
.app-header-logo-link > *,
.app-header-logo-pair > * {
    flex: 0 1 auto;
    /* A floor, so a compact mark stays legible while a wide wordmark,
       which shrinks in proportion to its width, gives up most of the room. */
    min-width: 24px;
}

.app-header-title {
    min-width: 0;
}

/* The leading mark holds its size; within the brand zone only the title
   gives. */
.app-header-brand > :not(.app-header-title) {
    flex-shrink: 0;
}

.app-header-partner-slot {
    flex-shrink: 1;
}

@media (max-width: 700px) {
    #app-header-nav {
        display: none;
    }
    #app-header-menu-btn {
        display: flex;
    }
    /* The title is the first thing to go; the logos carry identity. */
    .app-header-title {
        display: none;
    }
    #app-header {
        gap: 10px;
        padding: 0 12px;
    }
    .app-header-brand,
    .app-header-end {
        gap: 10px;
    }
    /* Smaller marks, and a hard cap on any one of them, so a wide wordmark
       cannot claim most of a phone's band. */
    .app-header-logo {
        height: 22px;
    }
    .app-header-logo--partner,
    .app-header-logo--institution {
        height: 20px;
        max-width: 96px;
    }
    .app-header-partner-slot {
        min-width: 0;
    }
}

/* ── Map overlay rail ────────────────────────────────────────────────────
   Bottom-left column shared by every map-scoped panel. Anchored at the
   bottom and auto-height, so it grows upward as panels mount; children are
   ordered by the `order` set in overlay-rail.js, not by mount order.

   The rail itself is click-through, so the gaps between panels don't steal
   map interaction. See app/overlay-rail.js for why this exists. */
#map-overlay-rail {
    position: absolute;
    z-index: var(--z-map-overlay);
    bottom: 10px;
    left: 10px;
    max-height: calc(100vh - 20px);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    pointer-events: none;
}

#map-overlay-rail > * {
    pointer-events: auto;
    /* Panels no longer carry their own anchors; the rail places them. */
    position: static;
    flex: 0 0 auto;
}

/* The legend is the only panel that can outgrow the viewport, so it is the
   one that yields — it shrinks and scrolls internally (#legend-content)
   rather than pushing the compact controls off-screen. */
#map-overlay-rail > #legend {
    flex: 0 1 auto;
    min-height: 0;
}

#menu {
    background: var(--panel-bg) !important;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    position: absolute;
    z-index: var(--z-map-overlay);
    top: 10px;
    right: 10px;
    border-radius: 5px;
    min-width: 160px;
    border: 1px solid var(--panel-line);
    font-family: 'Open Sans', sans-serif;
    padding: 12px;
    box-shadow: 0 2px 4px var(--panel-shadow);
}

.menu-section {
    margin-bottom: 12px;
}

.menu-section:last-child {
    margin-bottom: 0;
}

.basemap-toggle-group {
    display: flex;
    gap: 4px;
    padding: 4px 0;
}

.basemap-btn {
    flex: 1;
    padding: 5px 6px;
    font-size: 11px;
    font-weight: 500;
    border: 1px solid var(--control-border);
    border-radius: 4px;
    background: var(--control-bg);
    color: var(--control-fg);
    cursor: pointer;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
    white-space: nowrap;
}

.basemap-btn:hover {
    background: var(--control-bg-hover);
}

.basemap-btn.active {
    font-weight: 600;
}

.menu-section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.globe-btn {
    gap: 5px;
}





.menu-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
}

.section-title {
    display: block;
    font-size: 11px;
    font-weight: 600;
    color: var(--panel-fg-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

#menu-toggle {
    background: none;
    border: none;
    font-size: 18px;
    cursor: pointer;
    color: var(--panel-fg-muted);
    padding: 0 2px;
    line-height: 1;
}

#menu-toggle:hover {
    color: var(--panel-fg);
}

.menu-header-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--panel-fg-muted);
    padding: 0 4px;
    line-height: 1;
    display: flex;
    align-items: center;
}
.menu-header-btn:hover:not(:disabled) {
    color: var(--panel-fg);
}
.menu-header-btn:disabled {
    opacity: 0.35;
    cursor: default;
}

#menu.collapsed #menu-body {
    display: none;
}

/* Hide the "Layers" title when expanded — only show it in the collapsed state */
#menu:not(.collapsed) .menu-header .section-title {
    display: none;
}

#menu:not(.collapsed) .menu-header {
    justify-content: flex-end;
    margin-bottom: 4px;
}

.radio-group label,
.checkbox-group label {
    display: flex;
    align-items: center;
    font-size: 13px;
    color: var(--panel-fg);
    margin-bottom: 6px;
    cursor: pointer;
}

.radio-group label:last-child,
.checkbox-group label:last-child {
    margin-bottom: 0;
}

.radio-group input[type="radio"],
.checkbox-group input[type="checkbox"] {
    cursor: pointer;
    margin: 0;
}

.radio-group span,
.checkbox-group span {
    margin-left: 6px;
}

#menu input[type="checkbox"] {
    cursor: pointer;
}

/* Layer groups */
.layer-group {
    margin-bottom: 8px;
}

.layer-group:last-child {
    margin-bottom: 0;
}

.layer-group-title {
    font-size: 11px;
    font-weight: 600;
    color: var(--panel-fg-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    cursor: pointer;
    user-select: none;
    margin-bottom: 6px;
    list-style: none;
}

.layer-group-title::-webkit-details-marker {
    display: none;
}

.layer-group-title::before {
    content: '▾ ';
    font-size: 10px;
    color: var(--panel-fg-faint);
}

details.layer-group:not([open]) .layer-group-title::before {
    content: '▸ ';
}

.layer-item {
    margin-bottom: 6px;
    padding-left: 8px;
}

.layer-item:last-child {
    margin-bottom: 0;
}

/* Layers with a remove button lay the toggle + button out in a row. */
.layer-item.has-remove {
    display: flex;
    align-items: center;
}

.layer-item.has-remove .layer-toggle {
    flex: 1;
    min-width: 0;
}

.layer-remove-btn {
    flex: 0 0 auto;
    margin-left: 6px;
    padding: 0;
    width: 18px;
    height: 18px;
    line-height: 16px;
    font-size: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    border-radius: 3px;
    color: var(--panel-fg-muted);
    cursor: pointer;
}

.layer-remove-btn:hover {
    background: var(--panel-hover);
    color: var(--danger-fg);
}

.layer-toggle {
    display: flex;
    align-items: center;
    font-size: 13px;
    color: var(--panel-fg);
    cursor: pointer;
}

/* Uploaded-GeoJSON control (opt-in — see upload-manager.js) */
/* Row of panel-level actions (Upload, Save) under the layer list. They sit
   side by side and size to their content rather than each spanning the
   panel — a full-width button reads as a section, not a control. */
/* A grid rather than a wrapping flex row, so buttons line up in columns
   instead of drifting to whatever width their labels happen to be. Three
   across at most; below ~3 × 7.5rem the columns drop to two rather than
   squeezing labels. */
.panel-actions {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(max(7.5rem, calc((100% - 2 * 8px) / 3)), 1fr));
    gap: 8px;
    margin: 10px 0 8px;
}

.panel-actions > .panel-btn,
.panel-actions > .upload-btn {
    width: 100%;
    min-width: 0;
    min-height: 32px;
    padding: 6px 10px;
}

/* One button. Everything that is clickable in a panel — basemap, globe,
   send-back, upload, export — uses this, so a row of them reads as a set.
   Solid fill rather than an outline: a translucent control picks up whatever
   is behind it, which is what made these look inconsistent. */
.upload-btn,
.panel-btn,
.basemap-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    padding: 5px 10px;
    font-size: 12px;
    font-family: inherit;
    color: var(--control-fg);
    background: var(--control-bg);
    border: 1px solid var(--control-border);
    border-radius: 4px;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s, opacity 0.15s;
}


/* Checkboxes and range sliders (the layer toggles and the year slider) take
   the same blue, so the whole panel agrees. */
#layer-controls-container input[type="checkbox"],
.checkbox-group input[type="checkbox"],
.rc-slider,
.anim-controls input[type="range"] {
    accent-color: var(--control-accent);
}


/* Disabled keeps full opacity and swaps colours instead: fading the whole
   element also faded the label against its own fill, leaving it at ~2:1. */
.panel-btn:disabled,
.upload-btn:disabled {
    background: var(--control-disabled-bg);
    color: var(--control-disabled-fg);
    cursor: not-allowed;
}

.upload-btn:hover,
.basemap-btn:hover,
.panel-btn:not(:disabled):hover {
    background: var(--control-bg-hover);
}

/* Toggles — basemap, globe, hex grid — read their state from colour, so
   they cannot share the action colour in both states. On is the full
   control colour; off takes the disabled pair, the same colour a
   send-to-back with nothing to send wears. An off toggle stays clickable,
   so it keeps the pointer and gets a hover nudge toward the on colour.
   Keyed on aria-pressed, which the buttons carry for screen readers anyway,
   so the visible and announced state cannot drift apart. */
.basemap-btn[aria-pressed="false"],
.panel-btn[aria-pressed="false"] {
    background: var(--control-disabled-bg);
    color: var(--control-disabled-fg);
}

.basemap-btn[aria-pressed="false"]:hover,
.panel-btn[aria-pressed="false"]:not(:disabled):hover {
    background: color-mix(in srgb, var(--control-disabled-bg) 75%, var(--control-bg));
    color: var(--control-fg);
}

.basemap-btn[aria-pressed="true"],
.panel-btn[aria-pressed="true"] {
    background: var(--control-bg);
    color: var(--control-fg);
    border-color: var(--control-active-border);
}

.upload-status {
    font-size: 11px;
    color: var(--panel-fg-muted);
    margin: 0 0 6px;
    min-height: 0;
    line-height: 1.35;
}

.upload-status:empty {
    display: none;
}

.upload-status.is-error {
    color: var(--danger-fg);
}

.upload-dragover {
    outline: 3px dashed rgba(59, 130, 246, 0.7);
    outline-offset: -3px;
}

.layer-toggle input[type="checkbox"] {
    margin: 0 6px 0 0;
    cursor: pointer;
}

/* Version selector for versioned layers */
.version-select {
    display: block;
    margin: 4px 0 0 22px;
    font-size: 12px;
    padding: 2px 6px;
    background: var(--control-bg);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid var(--panel-line);
    border-radius: 3px;
    color: var(--panel-fg);
    cursor: pointer;
    max-width: 200px;
}

.version-select option {
    background: var(--panel-bg-strong);
    color: var(--panel-fg);
}

/* Toggle-group row (#349): names members with no data for the selected version */
.toggle-group-note {
    margin: 3px 0 0 22px;
    font-size: 11px;
    color: var(--panel-fg-muted);
}

/* Glass effect for species richness dropdown */
#species-taxon {
    background: var(--control-bg) !important;
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid var(--panel-line);
    border-radius: 3px;
    color: var(--panel-fg);
    padding: 4px 8px;
    cursor: pointer;
}

#species-taxon option {
    background: var(--panel-bg-strong);
    color: var(--panel-fg);
}

/* Legend styles */
#legend {
    background: var(--panel-bg) !important;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border-radius: 5px;
    max-width: 280px;
    max-height: 80vh;
    border: 1px solid var(--panel-line);
    font-family: 'Open Sans', sans-serif;
    box-shadow: 0 2px 4px var(--panel-shadow);
    overflow: hidden;
}

#legend-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 15px;
    background: transparent;
    border-bottom: 1px solid var(--panel-line);
}

#legend-header h3 {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--panel-fg);
}

#legend-toggle {
    background: none;
    border: none;
    font-size: 20px;
    font-weight: bold;
    color: var(--panel-fg-muted);
    cursor: pointer;
    padding: 0;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 3px;
    transition: background-color 0.2s;
}

#legend-toggle:hover {
    background-color: var(--panel-hover);
}

#legend-content {
    padding: 10px 15px 15px;
    max-height: calc(80vh - 60px);
    overflow-y: auto;
}

#legend-content.collapsed {
    display: none;
}

.legend-section {
    margin-bottom: 15px;
}

.legend-section:last-child {
    margin-bottom: 0;
}

.legend-section h4 {
    margin: 0 0 8px 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--panel-fg-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* Grouped legend sections: one heading over its member layers (mirrors the
   layer panel). The group title is the prominent heading; per-layer section
   headings inside become lighter, indented sub-headings. */
.legend-group {
    margin-bottom: 15px;
}

.legend-group:last-child {
    margin-bottom: 0;
}

.legend-group-title {
    margin: 0 0 8px 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--panel-fg-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.legend-group .legend-section {
    margin-bottom: 10px;
    padding-left: 8px;
}

.legend-group .legend-section:last-child {
    margin-bottom: 0;
}

.legend-group .legend-section h4 {
    font-size: 11px;
    font-weight: 500;
    color: var(--panel-fg-muted);
    text-transform: none;
    letter-spacing: 0;
}

.legend-colorbar {
    height: 12px;
    border-radius: 2px;
    margin: 4px 0 2px;
    border: 1px solid var(--panel-line);
}

.legend-labels {
    display: flex;
    justify-content: space-between;
    font-size: 10px;
    color: var(--panel-fg-muted);
    margin-bottom: 4px;
}

/* Hex legend: which H3 resolution the shown value range applies to. */
.legend-hex-res {
    font-size: 10px;
    color: var(--panel-fg-faint);
    font-style: italic;
}

.legend-item {
    display: flex;
    align-items: center;
    font-size: 11px;
    color: var(--panel-fg);
    margin-bottom: 5px;
    line-height: 1.3;
}

.legend-item:last-child {
    margin-bottom: 0;
}

.legend-item span {
    display: inline-block;
    width: 18px;
    height: 18px;
    margin-right: 8px;
    border-radius: 2px;
    border: 1px solid var(--panel-line);
    flex-shrink: 0;
}

/* Scrollbar styling for legend */
#legend-content::-webkit-scrollbar {
    width: 6px;
}

#legend-content::-webkit-scrollbar-track {
    background: transparent;
    border-radius: 3px;
}

#legend-content::-webkit-scrollbar-thumb {
    background: var(--panel-line);
    border-radius: 3px;
}

#legend-content::-webkit-scrollbar-thumb:hover {
    background: var(--panel-fg-muted);
}

/* Hover tooltip */
.map-tooltip {
    display: none;
    position: fixed;
    z-index: var(--z-tooltip);
    background: var(--panel-bg-strong);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    border: 1px solid var(--panel-line);
    border-radius: 4px;
    padding: 8px 10px;
    font-family: 'Open Sans', sans-serif;
    font-size: 12px;
    color: var(--panel-fg);
    box-shadow: 0 2px 6px var(--panel-shadow);
    pointer-events: none;
    max-width: 260px;
}

.map-tooltip table {
    border-collapse: collapse;
    width: 100%;
}

.map-tooltip th {
    font-weight: 600;
    color: var(--panel-fg-muted);
    text-align: left;
    padding: 2px 10px 2px 0;
    white-space: nowrap;
}

.map-tooltip td {
    color: var(--panel-fg);
    padding: 2px 0;
    word-break: break-word;
}

#h3-res-badge {
    font-family: 'Open Sans', sans-serif;
    font-size: 10px;
    color: var(--panel-fg-muted);
    background: var(--panel-bg);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    padding: 2px 7px;
    border-radius: 4px;
    border: 1px solid var(--panel-line);
    pointer-events: none;
}

/* Draw region toggle button */
#draw-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 29px;
    height: 29px;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    color: var(--panel-fg);
    border-radius: 4px;
}

#draw-toggle:hover {
    background: var(--panel-hover);
}

#draw-toggle.active {
    color: var(--control-focus-fg);
    background: var(--control-focus-bg);
}

/* Animated trajectory layer controls */
.anim-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: var(--panel-bg);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--panel-line);
    border-radius: 6px;
    font-family: 'Open Sans', sans-serif;
    font-size: 12px;
    color: var(--panel-fg);
    box-shadow: 0 2px 4px var(--panel-shadow);
}

.anim-controls .anim-label {
    font-weight: 600;
    max-width: 160px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.anim-controls .anim-play {
    border: 1px solid var(--panel-line);
    background: var(--control-bg);
    border-radius: 4px;
    width: 26px;
    height: 26px;
    cursor: pointer;
    font-size: 11px;
    line-height: 1;
}

.anim-controls .anim-play:hover {
    background: var(--control-bg-hover);
}

.anim-controls .anim-time {
    font-variant-numeric: tabular-nums;
    min-width: 120px;
    color: var(--panel-fg-muted);
}

.anim-controls .anim-speed,
.anim-controls .anim-mode {
    border: 1px solid var(--panel-line);
    border-radius: 4px;
    padding: 2px 4px;
    background: var(--control-bg);
    font-size: 12px;
}

/* ── Reactive-parameter controls (temporal / value sliders, #147) ────────
   Same frosted-glass surface as .anim-controls. Placement comes from
   #map-overlay-rail (SLOT.REACTIVE), not from this rule. */
.reactive-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: var(--panel-bg);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--panel-line);
    border-radius: 6px;
    font-family: 'Open Sans', sans-serif;
    font-size: 12px;
    color: var(--panel-fg);
    box-shadow: 0 2px 4px var(--panel-shadow);
}

.reactive-controls .rc-label {
    font-weight: 600;
    max-width: 160px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.reactive-controls .rc-play {
    border: 1px solid var(--panel-line);
    background: var(--control-bg);
    border-radius: 4px;
    width: 26px;
    height: 26px;
    cursor: pointer;
    font-size: 11px;
    line-height: 1;
}

.reactive-controls .rc-play:hover {
    background: var(--control-bg-hover);
}

.reactive-controls .rc-slider {
    width: 160px;
    cursor: pointer;
}

.reactive-controls .rc-value {
    font-variant-numeric: tabular-nums;
    min-width: 48px;
    text-align: right;
    color: var(--panel-fg-muted);
}

/* ── Chart panels (opt-in charting primitive, #277) ──────────────────────
   Floating card anchored bottom-right; multiple charts cascade. */
.chart-panel {
    position: absolute;
    z-index: var(--z-chart-panel);
    /* Anchor to the bottom-right of the MAP area: offset past the sidebar
       (--sidebar-width is 0 in floating mode) plus the per-panel cascade, so
       charts never hide behind a right-docked sidebar. */
    right: calc(var(--sidebar-width, 0px) + var(--chart-cascade, 12px));
    bottom: var(--chart-cascade, 12px);
    width: min(440px, 80vw);
    height: 320px;
    /* Resize from the TOP-LEFT grip (.chart-panel-resize) — the panel is pinned
       bottom-right, so that's the free corner. Native CSS `resize` only grips
       the bottom-right, so it's not used here. */
    min-width: 260px;
    min-height: 200px;
    max-width: 96vw;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    background: var(--panel-bg-strong);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid var(--panel-line);
    border-radius: 8px;
    box-shadow: 0 4px 16px var(--panel-shadow);
    font-family: 'Open Sans', sans-serif;
    overflow: hidden;
}

/* Popped out: a large centered view. right/bottom are released so the
   transform-centering takes over; the ResizeObserver re-plots to the new box. */
.chart-panel.chart-panel--popped {
    right: auto;
    bottom: auto;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(900px, 92vw);
    height: min(640px, 86vh);
}

/* Top-left resize grip. A small corner target with two diagonal ticks; the
   drag itself is handled in chart-renderer.js (_wireResize). */
.chart-panel-resize {
    position: absolute;
    top: 0;
    left: 0;
    width: 18px;
    height: 18px;
    z-index: 1;   /* local to .chart-panel's stacking context */
    cursor: nwse-resize;
    background:
        linear-gradient(-45deg, transparent 0 4px, var(--panel-fg-faint) 4px 5px, transparent 5px 8px, var(--panel-fg-faint) 8px 9px, transparent 9px) top left / 12px 12px no-repeat;
    border-top-left-radius: 8px;
}

.chart-panel-resize:hover {
    background:
        linear-gradient(-45deg, transparent 0 4px, var(--panel-fg-muted) 4px 5px, transparent 5px 8px, var(--panel-fg-muted) 8px 9px, transparent 9px) top left / 12px 12px no-repeat;
}

/* Popped-out is center-anchored via a transform, so the bottom-right pin the
   grip's math assumes doesn't hold — hide it there. */
.chart-panel--popped .chart-panel-resize {
    display: none;
}

.chart-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    /* extra left padding clears the top-left resize grip */
    padding: 8px 12px 8px 22px;
    border-bottom: 1px solid var(--panel-line);
}

.chart-panel-title {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 600;
    font-size: 13px;
    color: var(--panel-fg);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chart-panel-close,
.chart-panel-pop {
    border: none;
    background: transparent;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    color: var(--panel-fg-muted);
    padding: 2px 4px;
    border-radius: 4px;
}

.chart-panel-header {
    flex: 0 0 auto;
}

.chart-panel-close:hover,
.chart-panel-pop:hover {
    background: var(--panel-hover);
    color: var(--panel-fg);
}

.chart-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    padding: 8px 12px 12px;
    overflow: auto;
}

/* ── Geocoder search box (maplibre-gl-geocoder) ──────────────────────────
   Frosted-glass surface to match the app's other map controls (#menu,
   .version-select) instead of the library's solid white — light tint +
   backdrop blur with DARK text, the way the rest of the map chrome reads
   over the (light) basemaps. The geocoder's CDN stylesheet is appended to
   <head> at runtime (after this file), so !important pins the surface colors
   against the later, equal-specificity library rules. Covers both the
   maplibregl- and mapboxgl- class prefixes the control emits. */
.maplibregl-ctrl-geocoder,
.mapboxgl-ctrl-geocoder {
    background: var(--panel-bg) !important;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--panel-line);
    box-shadow: 0 2px 6px var(--panel-shadow);
    border-radius: 10px;
}

.maplibregl-ctrl-geocoder input,
.mapboxgl-ctrl-geocoder input {
    color: var(--panel-fg);
}

.maplibregl-ctrl-geocoder input::placeholder,
.mapboxgl-ctrl-geocoder input::placeholder {
    color: var(--panel-fg-muted);
}

/* Search / loading / close icons */
.maplibregl-ctrl-geocoder--icon,
.mapboxgl-ctrl-geocoder--icon {
    fill: rgba(0, 0, 0, 0.55);
}

/* Suggestions dropdown — lighter, more opaque frosted glass for list legibility */
.maplibregl-ctrl-geocoder .suggestions,
.mapboxgl-ctrl-geocoder .suggestions {
    background: var(--panel-bg-strong) !important;
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid var(--panel-line);
    border-radius: 10px;
    overflow: hidden;
}

.maplibregl-ctrl-geocoder .suggestions > li > a,
.mapboxgl-ctrl-geocoder .suggestions > li > a {
    color: var(--panel-fg);
}

.maplibregl-ctrl-geocoder .suggestions > .active > a,
.maplibregl-ctrl-geocoder .suggestions > li > a:hover,
.mapboxgl-ctrl-geocoder .suggestions > .active > a,
.mapboxgl-ctrl-geocoder .suggestions > li > a:hover {
    background: var(--panel-hover);
    color: var(--panel-fg);
}

/* ── Navigation control (zoom +/- and compass) ───────────────────────────
   Same frosted-glass surface as the search box so the on-map controls match.
   The zoom/compass glyphs keep their natural dark fill — they read well on
   the light glass and stay crisp (no invert / shadow blurring). */
.maplibregl-ctrl-group,
.mapboxgl-ctrl-group {
    background: var(--panel-bg) !important;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--panel-line);
    box-shadow: 0 2px 6px var(--panel-shadow);
    border-radius: 10px;
    overflow: hidden;
}

.maplibregl-ctrl-group button,
.mapboxgl-ctrl-group button {
    background: transparent !important;
}

.maplibregl-ctrl-group button + button,
.mapboxgl-ctrl-group button + button {
    border-top: 1px solid var(--panel-line);
}

.maplibregl-ctrl-group button:hover,
.mapboxgl-ctrl-group button:hover {
    background: var(--panel-hover) !important;
}

/* ── Theme-aware logo variants ───────────────────────────────────────────
   A logo is an image, so it cannot follow the palette: a white-knockout mark
   vanishes on a light bar and a black one vanishes on a dark bar. When a
   config supplies `src_dark`, both are rendered and CSS shows one — chosen
   here rather than in JS so `theme: auto` tracks the OS live. */
.app-header-logo-pair {
    display: flex;
    align-items: center;
}

.app-header-logo--dark {
    display: none;
}

body.theme-dark .app-header-logo--light {
    display: none;
}

body.theme-dark .app-header-logo--dark {
    display: block;
}

@media (prefers-color-scheme: dark) {
    body.theme-auto .app-header-logo--light {
        display: none;
    }
    body.theme-auto .app-header-logo--dark {
        display: block;
    }
}


/* ── Focus ───────────────────────────────────────────────────────────────
   WCAG 2.4.7. One visible ring across the chrome, on :focus-visible so it
   answers the keyboard without outlining every mouse click. Rendered with
   box-shadow as well as outline so it stays visible against a fill of any
   colour, including an app's own brand primary. */
#app-header a:focus-visible,
#app-header button:focus-visible,
#app-header-menu a:focus-visible,
#app-header-menu button:focus-visible,
.panel-btn:focus-visible,
.upload-btn:focus-visible,
.basemap-btn:focus-visible,
#menu button:focus-visible,
#menu select:focus-visible,
#menu input:focus-visible,
#legend button:focus-visible,
#map-overlay-rail button:focus-visible,
#map-overlay-rail input:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: 4px;
}

/* Honour a reduced-motion preference: the transitions here are decorative,
   and the trajectory/slider autoplay is motion the viewer did not start. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}
